Development record
`auto-release.yml` runs `detect` -> `tag` -> `release`, and the semantic
auto-release.yml runs detect -> tag -> release, and the semantic
receipt_gate runs inside the release job, after the environment approval.
The tag is therefore created BEFORE the gate that can refuse the release.
When the gate refuses, the result is not a blocked release but a wedged one:
- The tag exists and is immutable by design —
detectstates it is NEVER moved. - The heal path re-releases FROM the tagged commit, resolved to its immutable sha, so every retry checks out the same tree and derives the same content commit.
- That tree can never gain the missing receipts, because the receipts must live in a commit that is an ancestor of the released commit and name its predecessor. Landing them on the default branch afterwards does not change the tagged tree.
So the version is consumed. Recovery requires deleting a tag the workflow treats as immutable, or abandoning the version and moving to the next one. Neither is a path the release machinery offers.
Field hit 2026-08-23: v0.6.2 tagged at b06fa80, receipt gate refused, no Release published, and no sequence of pushes to the default branch can heal it.
The deterministic gates do not have this problem — they run in verify, before
tag. Only the semantic gate sits on the wrong side of the tag. Whether the fix
is to arm receipt_gate in verify, to gate tagging on receipt presence in
detect, or to accept the wedge and document the recovery, is an ADR-shaped
question rather than a patch: it touches the immutability guarantee that makes
the heal path safe. Related: iss-2608231226342272 (the preview is silent about
this gate) and iss-2608231226274000 (the surface never documents the step).
Decision (adr-52, accepted 2026-08-28): arm the receipt gate in verify. Partially implemented: the gate now runs in verify (refusing before build/publish) and the content-commit derivation is receipts-dir based (iss-355). RESIDUAL: the auto-release path's tag job still pushes the tag before invoking release.yml/verify, so to fully stop a version being consumed the tag job must also be gated on the semantic verify. That is a maintainer-verified release-workflow change (CI cannot exercise it outside a real release); this record stays open for it.
Grounds
- pursued: a refused verify (deterministic or semantic) on the auto-release path leaves no tag; shown wrong by any auto-release run that pushes a tag while its release.yml verify job is red, or by a workflow edit that lets the tag job run without verify success
Frontmatter
| id | iss-2608231226347380 |
|---|---|
| type | issue |
| lifecycle | resolved |
| severity | major |
.abcd/work/issues/resolved/iss-2608231226347380-tagging-precedes-the-receipt-gate-so-a.md
open on GitHub ↗ · commit history ↗
mentions8
- adr-52The semantic gate sits on the wrong side of the tag
- dcn-26082800000000032026-08-28 — Release-pipeline gates: two decisions land together. (a) The
- dcn-26092500000000262026-09-25 — On the auto-release path the tag is still made before the `release` environment's approval, and that residual is recorded for the next cycle rather than built now (finding F2 of the workflows-lane review, LOW). release.yml's `tag` job needs `verify`, so a refused gate leaves no tag (iss-2608231226347380), but the job runs before the publish job waits on its human approval and before the four steps between the tag and `gh release create` (the vcs stamp, the archive re-verify, the two attestations). A rejected approval or a red step there leaves a tag with no Release, which the heal path then rebuilds on the next push. The closure shape: delete the `tag` job and create the tag as a step inside the `release` job, immediately before `gh release create`, through `gh api -X POST repos/<owner>/<repo>/git/refs -f ref=refs/tags/<tag> -f sha=<commit>` with the job's own `GH_TOKEN`, so it needs neither `persist-credentials` nor an extra job; a rejection or a red publish step then leaves no tag either. It also removes the auto-path half of iss-2609251125599536: once no auto-path failure can leave a tag without a Release, the only such tag is a hand-pushed one, which `detect` already refuses to rebuild when its own verify failed. Not built in this lane because the chain is unverifiable without a live release, and the order the lane shipped is the one the review walked (implementer of the workflows fix round).
- iss-355a batched merge-queue push makes github.sha the batch tip, so auto-release tags the wrong commit and release.yml's HEAD-caret-2-caret receipt derivation resolves an unrelated PR's commit whenever the release roll is not the last entry in its queue batch; the ancestry guard passes and the run wedges after the immutable tag, the iss-326 outcome by a route itd-93's pre-merge check cannot close because batch composition is decided by the queue
- iss-2608231226274000`commands/launch.md` documents the release cut as three steps over two Go entry
- iss-2608231226342272`abcd launch --dry-run` reports a `gates` array built in
- iss-2609100513521322`auto-release` cuts the version tag **before** the gate that could refuse the
- iss-2609251125599536auto-release's heal path rebuilds a hand-pushed tag whose verify failed on every later push to main. With the tag now made after verify on the auto-release path (iss-2608231226347380), a tag without a Release can still arise from a hand-pushed tag whose release.yml verify refused. detect cannot tell that from a transient publish failure (both are tag-present, Release-missing), so it sets need_release and rebuilds the same tagged commit, which fails again, on each push. Carried from the duplicate iss-2609100513521322's second acceptance: detect should refuse loudly and name the re-cut when the tag's last verify concluded in failure, or bound the retry.