Skip to content

D995 — The release candidate is smoked before it is published

Decision. preview.yml builds the candidate for the runner's own architecture (amd64), runs test/smoke/image.sh on it, and only then logs in and pushes the multi-arch image; a red smoke fails the run before anything reaches ghcr. ci.yml has a docker job that runs the Dockerfile's test stage and the same smoke (plus make demo-smoke on top of that image) on a PR that touches the container's inputs, on the release PR and on main. release-train.yml names a failed candidate build in its skip comment.

Why. Nothing started the image before a maintainer tried it: preview.yml only pushed, the test stage was never built by a workflow, and the final stage's tools (git, sops, age, ssh) and its non-root user were checked by nobody. A broken candidate was found by hand on Saturday or, worse, as a skipped train whose comment said the candidate was "not the head of main". The cost is one more amd64 build in the candidate run (the push reuses its layers from the GitHub cache) and a docker job on PRs that touch the Dockerfile, go.mod/go.sum, deploy/demo/, demo/guestbook/ or the smoke itself.

Alternatives rejected. Smoking the image after the push: a red image would already be published under the candidate tag. Running the smoke on arm64 under QEMU: minutes of emulation for a check that the runtime stage's contents (identical across architectures) already pass. A Go test instead of a shell script: the thing under test is docker run, and deploy/demo/smoke.sh is already the precedent. Adding docker to the required checks: a ruleset change only the maintainer makes; the release PR and the candidate run it anyway, and main-status watches it.

Consequences. The test stage now runs in CI, so a test that depends on the host breaks that job, not only make gate. test/smoke/image.sh and test/lib/mcp.sh are inputs of the docker filter in scripts/ci-filters.sh. The Makefile targets docker and docker-smoke are local conveniences: the workflows use buildx for its layer cache and call the script themselves. A trap for local runs: a git worktree's .git is a file, COPY . . carries it into the image, and git config --global then dies with "not a git repository"; run docker build --target test . from a clone, as CI does.

make demo-smoke had rotted unseen since the write gate (D907): the Worker sends concept_new without index_entry, and the seed's messages journal kept a curated index, so the guestbook write was refused. The seed's messages/_map.md now says index: generated (D301); the live guestbook reaches it only through its private remote, like every change to demo/guestbook/.