release: move publication orchestration into tools/release.zig, pin rotated subkey
Gates / package (push) Successful in 8m51s
Release / guard (push) Successful in 1m33s
Gates / test-aarch64 (push) Successful in 4m24s
Gates / frontend (push) Successful in 44s
Gates / package (push) Successful in 33s
Gates / container (push) Successful in 35s
Release / gates (push) Successful in 8m24s
Gates / test-aarch64 (push) Successful in 7m56s
Gates / frontend (push) Successful in 1m31s
Gates / test (push) Failing after 16m8s
Gates / container (push) Successful in 5m11s
CI / gates (push) Failing after 30m3s
Gates / test (push) Successful in 2m7s
Release / publish (push) Failing after 10m20s

This commit is contained in:
2026-08-08 20:22:04 +02:00
parent 32cd9b8e3e
commit 266dde7396
9 changed files with 3042 additions and 1202 deletions
+47
View File
@@ -580,6 +580,53 @@ was reproduced before it was fixed.
would have turned a licence check into an unpinned fetch. Reproduced: it
fetched `vite@8.2.0` over the pinned `8.1.5`.
23. **`actions/checkout` destroys the annotated tag object.** Found by the first
live dry run, not by review: on a tag ref, checkout fetches the *commit* SHA
into `refs/tags/<tag>`, so the signed tag reads as lightweight and the guard
refuses it as unannotated. Both jobs that read the tag object — signature
verification in the guard, the tagger date in the publish job — now force-
refetch `refs/tags/$TAG` from origin first. The same run also proved the
fail-closed secret guard for real: the first dry-run attempt ran with no
secrets configured (they were on the wrong repository) and stopped in the
guard with nothing built or pushed.
24. **Publication orchestration moved out of workflow shell into
`tools/release.zig`.** Ruling 5 already moved the packaging asserts out of
CI shell for one reason — "checks that only exist inside a workflow file are
the brittleness this exists to remove" — and the release job was the larger
half of the same problem, left in place. Three live failures came out of it,
and each was found by executing the workflow, which is the most expensive
place to find anything: `actions/checkout` replacing the annotated tag object
(deviation 23), the refetch that fixed it having no credentials because
`persist-credentials` is off, and the multiline armored subkey escaping the
runner's log masker, which masks per line.
Twelve subcommands, one per step group: `guard-tag`, `guard-ancestry`,
`guard-releases`, `resolve`, `changelog`, `image`,
`verify-image-binaries`, `sign`, `draft`, `latest`, `publish`, `scrub`. Every
behaviour recorded in deviations 10 to 15 and 23 is carried over unchanged —
probe-adopt, the VALIDSIG last field, the subkey-only import and signing
probe, the array-shape guard on the releases payload, the `:latest` label
read, the publish re-read, the per-home `gpgconf --kill`, the tag refetch.
What is new is that the semver ordering, VALIDSIG field selection, challenge
parsing, changelog extraction, checksum-line parsing, colon-format parsing
and payload-shape guard are 25 unit tests in `zig build test` rather than
shell that only ever runs on a tag push. `release.yml` keeps the triggers,
the concurrency group, the job graph, the SHA pins, the two pinned
fingerprints and the fail-closed secret presence check — which stays as
shell, deliberately, so that it runs before the tool is even compiled.
The same reasoning applies to the `jq` pipeline of the bundled-package gate,
which moved to `web/scripts/bundledPackages.mjs` with its own vitest
coverage and an `npm run assert-bundled` entry point.
**Secret contract change:** `RELEASE_GPG_SUBKEY` becomes
`RELEASE_GPG_SUBKEY` and holds `base64 -w0` of the armored
`--export-secret-subkeys` output rather than the armored text. Manual
prerequisite 1 and 3 change accordingly. The tool decodes it in memory and
writes it to a mode-600 file inside the temporary `GNUPGHOME`. A single-line
secret is one the masker can actually mask.
### Not verified, and why
- **No workflow has ever executed.** `release.yml` and `gates.yml` were validated