ci: release on merge — the merge to main IS the release; nobody pushes tags #5

Merged
casey merged 2 commits from ci/release-on-merge into main 2026-10-15 02:57:06 +00:00
Member

A release now happens only because the operator merged it to main. Nobody pushes tags, and nothing dispatches a deploy.

Flow

  1. ./build release cuts and signs on the operator machine, as before. The signing key stays there and never becomes a CI secret. The build packs the bundle once and writes release/MANIFEST (version + bundle sha256) into the release: vX.Y.Z lockstep commit. It creates no tags.
  2. ./deploy only stages the bundle: it uploads to VARASYS/zddc-releases-pending/<ver> and reads back the stored sha256.
  3. On merge, release-on-merge.yml runs these steps in order:
    • decide whether this merge is a release;
    • check the bundle hash against the merged manifest, and check every artifact's signature against the tracked release-keys/current.pem;
    • deploy with deploy-origin.sh, the one swappable step, which holds every deploy credential and the history guard;
    • verify the live site;
    • record the release: add it to the accumulator, create the 7 tags through the API at the release commit, delete the pending copy.
  4. Removed: deploy-releases.yml (and with it the workflow_dispatch path and the force override) and verify-release.yml.

Verified

  • sh .forgejo/scripts/test-release-on-merge.sh: 18 known-answer cases for decide and fetch, all pass. Three planted bugs (tag-at-another-commit check off, hash check off, signature check off) each turn cases red.
  • The real v0.0.57 bundle passes every structural rule fetch applies.
  • release/MANIFEST is seeded with v0.0.57 and the live bundle's sha256, and all 7 v0.0.57 tags exist. So the first run after this merge decides release=false (I checked this against origin).
  • release-keys/current.pem matches a live v0.0.57 signature and the operator machine's key (public halves only).
  • shellcheck -S warning is clean. A cold reviewer on a different model read the diff, and all of its findings are fixed in this commit.

Not verified before a real release: deploy-origin.sh and release-record.sh run against the live origin and the real registry, so the first release is their test. That includes whether PACKAGE_TOKEN may write the accumulator and whether the workflow token may create the tags.

Needs before the first release

  • Repo secret PACKAGE_TOKEN (Forgejo package write) on varasys/zddc, beside STATIC_DEPLOY_KEY and DEPLOY_ORIGIN. A release whose credentials are missing now fails before it deploys anything.
  • ./deploy reads ~/.git-token, which must become a bizon-agent token with package write.

🤖 Generated with Claude Code

A release now happens only because the operator merged it to `main`. Nobody pushes tags, and nothing dispatches a deploy. **Flow** 1. `./build release` cuts and signs on the operator machine, as before. The signing key stays there and never becomes a CI secret. The build packs the bundle once and writes `release/MANIFEST` (version + bundle sha256) into the `release: vX.Y.Z lockstep` commit. It creates no tags. 2. `./deploy` only stages the bundle: it uploads to `VARASYS/zddc-releases-pending/<ver>` and reads back the stored sha256. 3. On merge, `release-on-merge.yml` runs these steps in order: - decide whether this merge is a release; - check the bundle hash against the merged manifest, and check every artifact's signature against the tracked `release-keys/current.pem`; - deploy with `deploy-origin.sh`, the one swappable step, which holds every deploy credential and the history guard; - verify the live site; - record the release: add it to the accumulator, create the 7 tags through the API at the release commit, delete the pending copy. 4. Removed: `deploy-releases.yml` (and with it the `workflow_dispatch` path and the `force` override) and `verify-release.yml`. **Verified** - `sh .forgejo/scripts/test-release-on-merge.sh`: 18 known-answer cases for decide and fetch, all pass. Three planted bugs (tag-at-another-commit check off, hash check off, signature check off) each turn cases red. - The real v0.0.57 bundle passes every structural rule fetch applies. - `release/MANIFEST` is seeded with v0.0.57 and the live bundle's sha256, and all 7 v0.0.57 tags exist. So the first run after this merge decides `release=false` (I checked this against origin). - `release-keys/current.pem` matches a live v0.0.57 signature and the operator machine's key (public halves only). - shellcheck `-S warning` is clean. A cold reviewer on a different model read the diff, and all of its findings are fixed in this commit. **Not verified before a real release:** `deploy-origin.sh` and `release-record.sh` run against the live origin and the real registry, so the first release is their test. That includes whether `PACKAGE_TOKEN` may write the accumulator and whether the workflow token may create the tags. **Needs before the first release** - Repo secret `PACKAGE_TOKEN` (Forgejo package write) on varasys/zddc, beside `STATIC_DEPLOY_KEY` and `DEPLOY_ORIGIN`. A release whose credentials are missing now fails before it deploys anything. - `./deploy` reads `~/.git-token`, which must become a bizon-agent token with package write. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Before: the operator machine cut + signed (./build release, 7 local tags),
./deploy uploaded the bundle and dispatched deploy-releases.yml
(workflow_dispatch, STATIC_DEPLOY_KEY, force=true override), then the 7
tags were pushed by hand and verify-release.yml checked them.

After:
- ./build release fetches origin's tags (refuses a version that already
  has one), cuts and signs as before, packs dist/release-bundle/ ONCE and
  writes release/MANIFEST (version + bundle sha256) into the
  `release: vX.Y.Z lockstep` commit. It creates no tags.
- ./deploy only STAGES: checks HEAD's manifest names the packed bundle,
  uploads it to VARASYS/zddc-releases-pending/<ver>, reads the stored
  sha256 back. It dispatches nothing.
- release-on-merge.yml (push to main only; no workflow_dispatch anywhere):
  release-decide.sh (all 7 tags present = nothing to do; malformed or
  missing manifest, version regress, non-release commit editing the
  manifest, tag at another commit = failed run, never "no release") →
  release-fetch.sh (hash must equal the merged manifest; every artifact of
  the version must verify against the TRACKED release-keys/current.pem and
  the bundle's pubkey.pem must be that key — the hash pins the bytes, the
  signature pins who cut them; the private key stays on the operator
  machine) → deploy-origin.sh (the ONE swappable deploy step: credentials
  + the fail-closed history guard, now with no override) → live check →
  release-record.sh (accumulator + prune, 7 tags via the API at the
  release commit, pending copy deleted; tags last so a re-run is clean).
- deploy-releases.yml and verify-release.yml removed.
- release/MANIFEST seeded with v0.0.57 + the sha256 of the live
  accumulator bundle 20261014-151144 (verified: its canonicals point at
  v0.0.57, and all 7 v0.0.57 tags exist), so the first run after merge
  is release=false. release-keys/current.pem is the live pubkey.pem
  (verified against a live v0.0.57 signature and against the key on the
  operator machine, public halves only).

Known answers: sh .forgejo/scripts/test-release-on-merge.sh — 18 cases
over decide + fetch in a throwaway repo + file:// registry, 18 passed;
planted bugs (tag-elsewhere check off, hash check off, signature check
off) each turned cases red. The real v0.0.57 bundle's file listing passes
every structural rule fetch applies. shellcheck -S warning clean on the
new and changed scripts. Not testable before a real release:
deploy-origin.sh and release-record.sh (they act on the live origin and
the real registry).

Docs: AGENTS.md § Releasing + new § Release on merge (incl. recovery when
a merged release cannot finish) + § Release tagging, ARCHITECTURE.md,
CLAUDE.md, zddc/README.md, stale comments in build, landing/build.sh,
zddc/release.sh.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
In this org REGISTRY_TOKEN is the zot image-registry push password (the
org-level one was deleted 2026-10-14; zddc has none), so the record step
would have deployed and then died. The package-write secret is now
PACKAGE_TOKEN, named in AGENTS.md § Release on merge as a required repo
secret beside STATIC_DEPLOY_KEY and vars.DEPLOY_ORIGIN.

New first step on a release: all three must be present before anything is
fetched or deployed, so a missing credential fails the run with the live
site untouched.

Tag-protection wording dropped (no rule: once bizon-agent is Read, only the
operator and CI on main can create tags).

18/18 known answers still pass; shellcheck -S warning clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
casey merged commit 13c3b76fa2 into main 2026-10-15 02:57:06 +00:00
Sign in to join this conversation.
No reviewers
No labels
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
varasys/zddc!5
No description provided.