Development record
`auto-release` cuts the version tag **before** the gate that could refuse the
auto-release cuts the version tag before the gate that could refuse the
release. A red gate therefore leaves a tag that names a version with no Release
and no binaries: present in git, dated in the CHANGELOG, impossible to install.
This happened at v0.8.0 and cost a tag deletion to recover.
Evidence
Job order in .github/workflows/auto-release.yml:
detect -> tag -> release / verify -> rehearsal -> release -> site
In run 34403815697, tag completed in 7s and release / verify failed at
11m, on TestHooksRunEveryShapeAhoyInstalls/dev_shim
(iss-2609092116378524, since resolved). Three attempts, three failures, and
after each one the repository held a v0.8.0 tag with no Release.
A gate placed after the act it guards cannot prevent the state it exists to prevent. It can only report it.
The recovery path is what turns a failure into a wedge
auto-release already handles "tag exists, Release missing". Its own header:
If the tag exists but its Release is MISSING (e.g. a transient publish failure),
detectsets need_release=true and re-invokesreleaseALONE — built from the tagged commit (release_ref), never the moved-on HEAD — so a flaky publish never permanently wedges the version.
Correct for a flaky publish. Wrong for a failing gate, and detect cannot tell
them apart, because both present as tag-without-Release. When the cause is a
defect:
- The defect is fixed and merged to
main. - The merge pushes to
main, soauto-releasefires. detectfinds tag present, Release missing, setsneed_release=true, and rebuilds from the tagged commit — which does not contain the fix.- It fails again. Landing the fix cannot rescue the release.
The promise that it "never permanently wedges the version" holds only in the transient case. In the non-transient case this path is the wedge.
Workaround, exercised at v0.8.0
Delete the tag before the fixing merge lands, never after: auto-release
triggers on push: branches: [main], so deleting a tag fires nothing, and
deleting afterwards leaves you needing a further push to re-trigger.
fix PR queued
-> delete the tag while the merge queue is still validating
-> the queue lands the merge -> push to main -> detect finds NO tag
-> need_tag=true -> tags the NEW tip, which carries the fix
Record the target first so the deletion is reversible
(gh api repos/<owner>/<repo>/git/refs/tags/vX.Y.Z --jq .object.sha; v0.8.0 was
annotated object 9fecedae on commit 9b3fbfab).
Deleting a published ref is against our own rules and was acceptable here only because nothing outward-facing had shipped from that tag: no Release, no binaries, nothing fetchable, and it was under an hour old. That does not generalise. Once a Release exists the route is closed and the only honest option is a new version.
Remedies, most fundamental first
- Reorder to
detect -> verify -> tag -> build -> publish. A red gate then leaves no tag and nothing to clean up: the release is simply not cut. This removes the wedge rather than making it recoverable. One property must be preserved deliberately:release.ymlchecks outgithub.shaso the gate exercises exactly the commit whose binaries ship, and with no tag yet to resolve, that sha has to be pinned through fromdetectto every later job. - Teach
detectthe difference between a flaky publish and a failing gate. If the previousrelease / verifyfor this tag concluded in failure, refuse loudly and say the tag must be re-cut rather than silently rebuilding the same commit. Worth doing regardless of (1), and it is what loud staging asks for: a recovery path that cannot work must say so instead of repeating. - A
workflow_dispatchretaginput that re-cuts the tag at HEAD once verify passes, making the recovery a reviewable operation rather than hand-run API calls. - Bound the retry, so one wedged tag cannot re-run the same failing build on every later push to main.
Same shape, second instance this release
The attribution gate is a required check whose commit half exempts only merge
MESSAGES. A plain git revert ninety commits back carried no Assisted-by:
trailer, because that is what git revert writes by default, and the branch
could not merge until that one message was corrected: a history rewrite of 206
commits and a re-point of two semantic-gate receipts.
A gate that can only be satisfied by rewriting history is also running too late.
A commit template or a prepare-commit-msg hook would put the trailer in when
the message is written. Recorded here rather than separately because the
generalisation is the same one, and splitting them would lose it.
Acceptance
- Given a release whose deterministic gate fails, when
auto-releaseruns, then no tag exists afterwards and the version stays uncut. - Given a tag whose gate failed, when a later push reaches
main, then the workflow refuses loudly and names the re-cut as the remedy, rather than rebuilding the same commit. - Given a
git reverton this repository, when the message is written, then it carries anAssisted-by:trailer without a later rewrite.
Grounds
- declined: the finding is carried whole by iss-2608231226347380; this would be wrong if iss-2608231226347380 were closed without answering it
Frontmatter
| id | iss-2609100513521322 |
|---|---|
| type | issue |
| lifecycle | wontfix |
| severity | major |
.abcd/work/issues/wontfix/iss-2609100513521322-auto-release-cuts-the-tag-before-the.md
open on GitHub ↗ · commit history ↗
mentions4
- iss-2608231226347380`auto-release.yml` runs `detect` -> `tag` -> `release`, and the semantic
- iss-2609092116378524`TestHooksRunEveryShapeAhoyInstalls/dev_shim` failed the release gate's
- iss-2609251125591539git revert writes no Assisted-by trailer, and the attribution commit gate (now also on the merge queue and on push to main, iss-280) refuses the message, so a plain revert can only be satisfied by rewriting history. Carried from the duplicate iss-2609100513521322's third acceptance: the trailer should be written when the message is written (a commit template or a prepare-commit-msg hook), not demanded after.
- 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.