---
name: fly-fullpath-deploy-data-symlink-gap
description: "RESOLVED 2026-05-16 via quick task full-path-symlink-sync. GH Actions full-path deploy used to image-swap via flyctl deploy without updating /data/client-assets/current/ symlink, so STATIC_ASSETS_DIR resolver served stale Phase 06.5 bundle. Fix: deploy-staging.yml now invokes scripts/client-release.sh post-deploy. Manual remediation no longer needed."
metadata: 
  node_type: memory
  type: project
  originSessionId: f364f65a-829f-4b53-bad0-caac1eb0ffa1
---

> **STATUS: RESOLVED 2026-05-16.** `.github/workflows/deploy-staging.yml`
> full-path job now runs the same tarball + `scripts/client-release.sh`
> atomic-swap sequence as fast-path, immediately after `flyctl deploy →
> rebno-staging`. See `.planning/quick/20260516-full-path-symlink-sync/`
> for the gap-closure quick task. The historical detail below is preserved
> for archaeology; the manual remediation block no longer needs to be run.


GH Actions `deploy-staging.yml` full-path job rebuilds the Docker image (server + bundled client at `/app/public/`) and `flyctl deploy`s it. The machine env carries `STATIC_ASSETS_DIR=/data/client-assets/current`. The server's static-asset resolver (`apps/server/src/static-assets.ts`) prefers the env path over the bundled `/app/public/` fallback whenever the env path exists and is a directory. The Fly volume `/data/client-assets/current` is a symlink populated by the **fast-path** `client-release.sh` script. **Full-path NEVER updates this symlink** — so after a full-path deploy with client changes, the server keeps serving the previous fast-path release.

**Symptom:** new HTML on disk (`/app/public/index.html`) references the new bundle hash, but staging serves the old HTML + old bundle hash because the resolver picks `/data/client-assets/current/` over `/app/public/`. Browser hard-refresh doesn't help. The bug is server-side, not edge/CDN.

**Diagnosis:** in the staging tab DevTools console, paste

```js
({ bundle: document.querySelector('script[type="module"][crossorigin]')?.src })
```

If the bundle filename doesn't match the hash on disk at `/app/public/assets/`, this gap fired.

**Manual remediation** (mirrors fast-path's `client-release.sh` shape):

```bash
flyctl ssh console -a rebno-staging -C "sh -c 'SHA=<40-hex-image-sha> && REL=/data/client-assets/releases/\$SHA && \
  mkdir -p \$REL && cp -r /app/public/. \$REL/ && \
  ln -sfn \$REL /data/client-assets/current.new && \
  mv -T /data/client-assets/current.new /data/client-assets/current'"
```

No machine restart needed — `express.static` resolves the symlink per-request.

**Why:** Discovered during Phase 06.6 operator UAT (2026-05-17). Tests 2/3/4 all "failed" because the operator saw the pre-06.6 client. Browser-side `chatHudInner: null` proved the served HTML was stale. Bundle hash on disk (`index-DjmLGbtm.js`) didn't match what the browser loaded (`index-CbpOzv5W.js`). Full-path deploy's `flyctl deploy` step at 04:49:14Z swapped the image but `/data/client-assets/current` was still pointing at the Phase 06.5 release from `2026-05-16T23:30Z`.

**Fast-path is unaffected** — it uploads a tarball via `flyctl ssh sftp put` and invokes `scripts/client-release.sh` over SSH, which atomic-swaps the symlink. Client-only diffs always take fast-path per the `dorny/paths-filter` rule, so this gap only fires on full-path triggers (any non-client server file changed).

**Worth tracking as gap-closure plan** (Phase 7 candidate or workflow-only fix): add a "Sync /data/client-assets/current" step to the full-path job between `flyctl deploy` and `Playwright CLI-08 two-client smoke`. Could either (a) sftp-upload the same tarball fast-path uses and invoke `client-release.sh`, or (b) `flyctl ssh console -C` an inline copy from `/app/public/` to `/data/client-assets/releases/<sha>/` followed by atomic `mv -T` symlink swap.

Related: [[worktree-merge-cwd-gotcha]] (the other deploy-orchestration gap).
