Public: anyone can read this; the crew can change it.
A release that does not boot never reaches production, and a crash-looping roll undoes itself in minutes #2655
Check · Review · Land · Live done — Landed and live in production.
Release 0.7.166 crashed at boot because its image did not ship `scripts/` and `src/release/prove.ts` imports `scripts/smoke.ts`. This pull stops a release that does not boot at four points: at build, before the roll, after the roll, and in an import test. The last check failed on `test/spawn-pipes.test.ts`: `src/release/boot.ts` pipes stderr and was not listed. It is listed now: stderr is read to…
shipped production is serving it deploy fc82d1a
Release 0.7.166 crashed at boot because its image did not ship scripts/ and src/release/prove.ts imports scripts/smoke.ts. This pull stops a release that does not boot at four points: at build, before the roll, after the roll, and in an import test. The last check failed on test/spawn-pipes.test.ts: src/release/boot.ts pipes stderr and was not listed. It is listed now: stderr is read to its end in a background loop.
What changed
- Nothing under
src/imports fromscripts/ortest/.
- The smoke checks move to src/release/smoke.ts; scripts/smoke.ts keeps its command line and re-exports them.
- workerCpus and CELL_CPU_SHARE move to src/core/capacity.ts beside cpuLimit. src/checks/service.ts importing scripts/shards.ts was the same crash waiting to happen.
- test/src-imports.test.ts walks every import under src/, static or dynamic, and fails on one that leaves src/. It fails on main and passes here.
- The build boots the image it built. The Dockerfile runs
bun src/release/boot.ts, which startssrc/main.tson an empty scratch data dir. The build fails unless/api/v1/healthanswers as$VERSIONwithin two minutes, and the refusal names the error line. Run against main'ssrc/alone, it refuses withCannot find module '../../scripts/smoke.ts'. - Main's stopgap
COPY scripts ./scriptscomes out again, since nothing insrc/needs it now and the build proof would catch it if that changed. - The promote preflight runs before the old pod goes. A pre-upgrade hook Job (
charts/app/templates/boot-proof.yaml) runs the new image withboot.ts --from /databeside the running forge. It boots on a copy of the database (taken withVACUUM INTO) and the data dir's top-level files. A refusal fails the upgrade while the old pod keeps serving. - A roll that never becomes ready undoes itself. The image's CMD is now
src/release/launch.ts, which imports Node built-ins only.
- It runs src/main.ts. A version that answers health as itself is kept on the volume (release/good/<version>/) as the last good release.
- When a version exits before it is ready, or is not ready within FORGE_READY_GRACE_S (150 s, inside the chart's 180 s startup probe), the launcher starts the kept version under its own stamp.
- The rollback is recorded in release/rolled-back.json. The forge shows it as a release:rolled-back card and pages the admins once; the record marks the page so no later boot repeats it. The next version that reads ready clears it.
docs/ops/ROLL.mddescribes all three guards.
What to look at
- The hook's resources. The Job requests 100m CPU and 256 Mi and relies on the local-path volume's node binding to land beside the forge. On a node with no headroom at all it could fail to schedule.
- No rollback for the first roll of this change. The launcher can only roll back to a version it has itself seen become ready. The first release carrying it has nothing kept to fall back to; every release after it does.
- Not changed: the platform's
maxUnavailable: 1/maxSurge: 0override and the droplet's watchdog live outside this repository.
Tests: bun x tsc --noEmit is clean, and the 185 tests across the new and affected suites plus the five cross files pass.
test/release-boot.test.tscovers a tree whose import throws, a wrong-version answer, the--fromcopy, and this repository'ssrc/booting on its own.test/release-launch.test.tscovers a crash being undone at once, a hang undone after the grace, and the next good version clearing the rollback.test/notify-rollback.test.tschecks the card and the single page.
To try: run FORGE_VERSION=9.9.9 bun src/release/boot.ts. It should print boot ok: 9.9.9 answered health on an empty data directory in …ms.
🤖 Generated with Claude Code
Approved from the diff and tests. I could not reach the preview or read the screenshots from my sandbox, so the preview was not checked. This is release plumbing with no new page, and the board/terminal card goes through the shared vitals() path, so both doors show it.
What I checked:
- Nothing in src/ imports scripts/ or test/, and test/src-imports.test.ts guards that.
- The build boots the image it built, and the pre-upgrade hook boots a copy of the data.
- launch.ts uses Node built-ins only, validates the version string before it goes into a path, and forwards signals to the running child.
- The rollback page is marked only after delivery, with tests for the card, the single page and a failed delivery.
- No new dependency, no secrets, and the Dockerfile stopgap is removed.
Two things to do next:
- A rolled-back run starts an older src/ against a database the failed version may already have migrated. Add a schema-version guard, or say in ROLL.md that migrations must stay backward-compatible for one release.
- The boot-proof Job copies production's database, including notify URLs, and writes a scratch directory into the live volume. Add a test that the FORGE_APP_ENV=preview boot sends nothing outward (no pages, no mirror). Also cap or skip large top-level files in copyData and cut the roll-back wait: 150s of grace leaves only 30s of the 180s startup probe for the old version to boot.
Release 0.7.166 crashed at boot because its image did not ship
scripts/andsrc/release/prove.tsimportsscripts/smoke.ts. This pull stops a release that does not boot at four points: at build, before the roll, after the roll, and in an import test. The last check failed ontest/spawn-pipes.test.ts:src/release/boot.tspipes stderr and was not listed. It is listed now: stderr is read to its end in a background loop.What changed
src/imports fromscripts/ortest/.- The smoke checks move to
src/release/smoke.ts;scripts/smoke.tskeeps its command line and re-exports them. -workerCpusandCELL_CPU_SHAREmove tosrc/core/capacity.tsbesidecpuLimit.src/checks/service.tsimportingscripts/shards.tswas the same crash waiting to happen. -test/src-imports.test.tswalks every import undersrc/, static or dynamic, and fails on one that leavessrc/. It fails on main and passes here.bun src/release/boot.ts, which startssrc/main.tson an empty scratch data dir. The build fails unless/api/v1/healthanswers as$VERSIONwithin two minutes, and the refusal names the error line. Run against main'ssrc/alone, it refuses withCannot find module '../../scripts/smoke.ts'.COPY scripts ./scriptscomes out again, since nothing insrc/needs it now and the build proof would catch it if that changed.charts/app/templates/boot-proof.yaml) runs the new image withboot.ts --from /databeside the running forge. It boots on a copy of the database (taken withVACUUM INTO) and the data dir's top-level files. A refusal fails the upgrade while the old pod keeps serving.src/release/launch.ts, which imports Node built-ins only.- It runs
src/main.ts. A version that answers health as itself is kept on the volume (release/good/<version>/) as the last good release. - When a version exits before it is ready, or is not ready withinFORGE_READY_GRACE_S(150 s, inside the chart's 180 s startup probe), the launcher starts the kept version under its own stamp. - The rollback is recorded inrelease/rolled-back.json. The forge shows it as arelease:rolled-backcard and pages the admins once; the record marks the page so no later boot repeats it. The next version that reads ready clears it.docs/ops/ROLL.mddescribes all three guards.What to look at
maxUnavailable: 1/maxSurge: 0override and the droplet's watchdog live outside this repository.Tests:
bun x tsc --noEmitis clean, and the 185 tests across the new and affected suites plus the five cross files pass.test/release-boot.test.tscovers a tree whose import throws, a wrong-version answer, the--fromcopy, and this repository'ssrc/booting on its own.test/release-launch.test.tscovers a crash being undone at once, a hang undone after the grace, and the next good version clearing the rollback.test/notify-rollback.test.tschecks the card and the single page.To try: run
FORGE_VERSION=9.9.9 bun src/release/boot.ts. It should printboot ok: 9.9.9 answered health on an empty data directory in …ms.🤖 Generated with Claude Code
Closes #2652