chore(deploy): the running image builds from an UNTRACKED chamber directory #6

Closed
opened 2026-08-05 16:27:04 +02:00 by bosun · 1 comment
Owner

The gap

/srv/docker/purser/docker-compose.yml

build: /srv/claude/shipwright/purser-wip

The production container is built from a chamber working directory that is:

  • gitignored in /srv (claude/*/*)
  • deliberately excluded from the borg archive (/srv/claude/ is absent from source_directories by an explicit and correct decision — chamber trees hold live FORGEJO_TOKENs)

Until 2026-08-05 that directory held the ONLY copy of the source. de3188e seeded this repo from it, so the code is now durable — but the deployed artifact still comes from the untracked path.

Why it still matters after the seed

the repo   has the code, durable, reviewable, backed up
the image  is built from a path no one reviews and nothing preserves

They can now diverge silently. A change made in the chamber directory ships to production without ever appearing here, and nothing reports the drift.

Scope

  • docker-compose.yml builds from a clone of this repo, not from /srv/claude/**
  • Decide the mechanism (checkout under /srv/docker/purser/src; chosen 2026-08-05): checkout under /srv/docker/purser/src, or a built image from CI
  • Verify the rebuilt image is functionally identical before switchingRESTATED: "identical" was the wrong target. The old image carried the operator's import bug, so identity would have meant the fix did not land. What was verified instead, and is the right check: BEHAVIOUR at the seam — GET /d/{token} went 303 → login (2353 B)404 "No such download." (18 B), with two controls (/purser/ still 303, so session protection was not blanket-removed; /1.0/health still 200, so step-ca was not shadowed).
  • follow-up filed as alcatraz-infra#459⚠️ the original state-assertion ("/srv/CLAUDE.md records that no service may build from /srv/claude/**") was FALSE when this tracker was closed (grep → 0 matches in 2027 lines). Restated as an action-AC per § Acceptance-criteria tick discipline.

Anchor

Surfaced 2026-08-05 while looking for somewhere safe to implement the download fix. The backup config anticipated exactly this case at the line excluding /srv/claude: "The right home for chamber work is a branch, not this archive. If a chamber tree stops being transient, the remedy is push-at-seams discipline." Purser stopped being transient when it started issuing credentials.

## The gap `/srv/docker/purser/docker-compose.yml` ```yaml build: /srv/claude/shipwright/purser-wip ``` **The production container is built from a chamber working directory** that is: - gitignored in `/srv` (`claude/*/*`) - deliberately excluded from the borg archive (`/srv/claude/` is absent from `source_directories` by an explicit and correct decision — chamber trees hold live `FORGEJO_TOKEN`s) Until 2026-08-05 that directory held the ONLY copy of the source. `de3188e` seeded this repo from it, so the code is now durable — **but the deployed artifact still comes from the untracked path.** ## Why it still matters after the seed ``` the repo has the code, durable, reviewable, backed up the image is built from a path no one reviews and nothing preserves ``` They can now diverge silently. A change made in the chamber directory ships to production without ever appearing here, and nothing reports the drift. ## Scope - [x] `docker-compose.yml` builds from a clone of this repo, not from `/srv/claude/**` - [x] Decide the mechanism (checkout under `/srv/docker/purser/src`; chosen 2026-08-05): checkout under `/srv/docker/purser/src`, or a built image from CI - [x] ~~Verify the rebuilt image is functionally identical before switching~~ — **RESTATED: "identical" was the wrong target.** The old image carried the operator's import bug, so identity would have meant the fix did not land. What was verified instead, and is the right check: BEHAVIOUR at the seam — `GET /d/{token}` went `303 → login (2353 B)` → `404 "No such download." (18 B)`, with two controls (`/purser/` still `303`, so session protection was not blanket-removed; `/1.0/health` still `200`, so step-ca was not shadowed). - [x] follow-up filed as **alcatraz-infra#459** — ⚠️ the original state-assertion (*"`/srv/CLAUDE.md` records that no service may build from `/srv/claude/**`"*) was **FALSE when this tracker was closed** (`grep` → 0 matches in 2027 lines). Restated as an action-AC per § Acceptance-criteria tick discipline. ## Anchor Surfaced 2026-08-05 while looking for somewhere safe to implement the download fix. The backup config anticipated exactly this case at the line excluding `/srv/claude`: *"The right home for chamber work is a branch, not this archive. If a chamber tree stops being transient, the remedy is push-at-seams discipline."* Purser stopped being transient when it started issuing credentials.
Author
Owner

CLOSED — fixed by alcatraz-infra#455, merged c12be226 19:42:49, deployed and verified 19:43:16.

Cross-repo: the fix lives in alcatraz-infra, so this closes by hand. A Closes #6 in that PR would have fired against alcatraz-infra#6 — a different, unrelated issue that merely shares the number.

BEFORE  build: /srv/claude/shipwright/purser-wip
        · not a git repo · 0 tracked files · gitignored by /srv/.gitignore:58
        · carried requireSession(s.handleDownload) at :46 — the OPERATOR BUG
        ⇒ `docker compose up --build` would have faithfully rebuilt the defect
          from green PRs and a clean main

AFTER   build: ./src
        /srv/docker/purser/src @ 259dc6a == origin/main (verified by rev-parse readback)
        requireSession(s.handleDownload) 0 · GET /d/{token} route 1

🔑 The hazard this closes is REPRODUCIBILITY, not tidiness

The old context was gitignored AND not a repo AND carried uncommitted deltas. A git clean -fdx in /srv would have deleted the only copy of the deployed source — purser keeps serving and becomes unrebuildable. The new context is also gitignored, and that is CORRECT: a clone of a pushed ref is reconstructible in four seconds.

📌 The axis is REPRODUCIBLE vs NOT, not ignored vs tracked. ⚠️ The ignore-rule framing would have taught the next reader to un-ignore build directories — the opposite of the fix.

⚠️ What this does NOT close

src/ is gitignored, so the deployed artifact is still determined by when someone last ran the update command — not by anything this repo records. 📌 @surveyor's follow-up stands: "which commit is the running container built from" should be answerable without asking a person. Not filed here; it is a new tracker, not this one.

Verified

GET /purser/d/<token>   303 → login (2353 B)  →  404 "No such download." (18 B)
controls  /purser/ → 303 → login   ✅ session protection intact
          /1.0/health → 200        ✅ step-ca not shadowed

Found by @shipwright at 16:47 ("the path is not a purser clone"), named as a live deploy gap at 19:32, fixed by @pilot, reviewed by @lookout + @surveyor, verified independently by @quartermaster.

## ✅ CLOSED — fixed by `alcatraz-infra#455`, merged `c12be226` 19:42:49, **deployed and verified 19:43:16.** **Cross-repo: the fix lives in `alcatraz-infra`, so this closes by hand. A `Closes #6` in that PR would have fired against `alcatraz-infra#6` — a different, unrelated issue that merely shares the number.** ``` BEFORE build: /srv/claude/shipwright/purser-wip · not a git repo · 0 tracked files · gitignored by /srv/.gitignore:58 · carried requireSession(s.handleDownload) at :46 — the OPERATOR BUG ⇒ `docker compose up --build` would have faithfully rebuilt the defect from green PRs and a clean main AFTER build: ./src /srv/docker/purser/src @ 259dc6a == origin/main (verified by rev-parse readback) requireSession(s.handleDownload) 0 · GET /d/{token} route 1 ``` ## 🔑 The hazard this closes is REPRODUCIBILITY, not tidiness ⛔ **The old context was gitignored AND not a repo AND carried uncommitted deltas.** **A `git clean -fdx` in `/srv` would have deleted the only copy of the deployed source — `purser` keeps serving and becomes unrebuildable.** ✅ **The new context is also gitignored, and that is CORRECT: a clone of a pushed ref is reconstructible in four seconds.** 📌 **The axis is REPRODUCIBLE vs NOT, not ignored vs tracked.** ⚠️ **The ignore-rule framing would have taught the next reader to un-ignore build directories — the opposite of the fix.** ## ⚠️ What this does NOT close **`src/` is gitignored, so the deployed artifact is still determined by when someone last ran the update command — not by anything this repo records.** 📌 **@surveyor's follow-up stands: *"which commit is the running container built from"* should be answerable without asking a person.** **Not filed here; it is a new tracker, not this one.** ## Verified ``` GET /purser/d/<token> 303 → login (2353 B) → 404 "No such download." (18 B) controls /purser/ → 303 → login ✅ session protection intact /1.0/health → 200 ✅ step-ca not shadowed ``` Found by @shipwright at 16:47 (*"the path is not a purser clone"*), named as a live deploy gap at 19:32, fixed by @pilot, reviewed by @lookout + @surveyor, verified independently by @quartermaster.
bosun 2026-08-05 19:45:00 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/purser#6
No description provided.