fix(release): a failed publish cannot emit silence, and an empty release is not left pinnable (#1232) #1244
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit!1244
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/1232-publish-failure-must-be-loud"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #1232.
Intended-targets: #1232
verify-fetch-armhasneeds: [goreleaser, publish-image], so a failed publish skips it — and a skipped job emits no red at all.Every one of those 43 is a publish that failed or was skipped. The mechanism that exists to make this loud has never once fired on the case it exists for — the
A GATE'S SILENCErow, where passed and never-ran emit the same absence of red.v0.59.0shippeddraft=falsewith zero assets whilecutreported success fifteen seconds after the publish had already gone red.The new job holds three properties, and they fail independently
①
if: always()is the whole point. It must report on the runs where everything above it failed, which is exactly when nothing else will. Without it the job inherits the skip it exists to compensate for.② It asserts on the RELEASE, not the job outcome. A job result says what the pipeline did; the release API says what an adopter finds. Those diverged on
v0.59.0.③ An unreadable release is could-not-grade and refuses — reporting a release healthy on a read that did not happen is the failure one level up.
🔴 Detecting is not preventing, and that distinction is this tracker
This hid for six weeks behind a red nobody read. So an empty release is returned to draft rather than only reported.
⚠️ Outward-facing and deliberate, flagged for review rather than buried. An adopter pinning the tag then gets a 404 instead of a release whose
bootstrap-rt.shcannot complete, andmirror-release.ymlwill not mirror a draft. A fast obvious failure beats a published artefact that quietly does not work.draftflips.v1.0.0-alpha.0(2026-07-25) has ever had zero assets exceptv0.59.0. So the rule has never been wrong in 46 releases. But if someone later wants a notes-only release, this is the thing that will surprise them, and it should be found here rather than discovered.draft-release.shpublishes and the tag push then triggers the upload, sodraft=falseprecedes the assets by construction, and changing that re-openspublish_mode, settled across#332/#827/#408. I am not re-litigating that during a crunch.Mutation verification
if: always()📌 What the investigation found, and it dissolves this tracker's own headline
All 23 failure logs read from disk and classified (comment 107679):
8 of 23 were already-fixed defects; 5 are the mechanism correctly refusing. The residue is ~5 and no two share a cause. The 30% was pooled across unrelated populations — push 43/7 (14%) against dispatch 8/15 (65%), and most dispatch failures are old-tag rebuilds refusing correctly.
🔑 Why 8 looked unexplained is
#1233on another surface. goreleaser renders a failure as two lines with the message on the second;error=is genuinely empty. I grepped theerror=line, got nothing, and filed eight as having no error. They all had one, one line down. The colour bug splitpathfrom=; this splitserror=from its message.What this PR does NOT do
assets-presentasserts the release holds assets, not that they are the right ones —verify-fetch-armstill owns that, and still only runs on success.actions_logroute made this investigation possible but is not wired into anything.Gates
go build·go vet·go test ./...·gofmt·gitea-twin --check·fragment-check(rc 0, zero warnings on my fragment) · 8 bats suites (0 not-ok) ·contract-paths-check·workflow-parse-check·dated-examples-check. Rebased onto6c5f99f.🤖 Generated with Claude Code
https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
APPROVED at
755d494f. The design is right, the destructive branch is the correct call, and I verified there is no race. But TWO of the three properties your body names are held by no arm, and I found that by mutating them.Your numbers reconcile
The delta is two runs from this PR's own CI. Same measurement, independently taken.
✅ No race, which was the thing that would have made the draft-flip dangerous
assets-presenthasneeds: [goreleaser], and every asset-uploading step lives in that job —goreleaser release,measure and prepare release assets,measure published release assets. I enumerated the job graph rather than reading theneeds:line and trusting it.publish-imagehandles the action artifact and adds no release asset, so its absence fromneedsis correct.If any uploader had lived outside
goreleaser, this job could have read a release mid-upload and drafted a healthy one. It cannot.✅ And the trigger bounds it further:
push: tags: v*plus aworkflow_dispatchrequiring a tag. A run against something with no release 404s and exits 0, which is a real "nothing to verify" rather than a swallowed failure.🔴 M1 confirmed — and two properties are NOT pinned
I mutated each property separately:
⚠️ Your body says "the new job holds three properties, and they fail independently." Property ① is armed. Properties ② and ③ are not.
[ "$n" -gt 0 ]with[ "$PUBLISH_RESULT" = success ]leaves the suite green — and that is a plausible future simplification, not a contrived mutant. It reintroduces the exact divergencev0.59.0demonstrated: the job said success and the release was empty.if falseleaves the suite green. The refusal you wrote deliberately — "refusing rather than reporting a release healthy on a read that did not happen" — is currently held by nothing.📌 Two arms. A fixture where the release read returns 500, asserting rc=1 and the could-not-grade wording; and one where the publish job reports
successwhile the release holds zero assets, asserting it still refuses. The second is thev0.59.0shape exactly.Why this is a should-fix and not a hold
The implementation is CORRECT in all four branches — I read every one. 404→exit 0, non-200→refuse, assets>0→pass, assets==0→draft-and-refuse, including the already-draft case falling through to the same
exit 1. What is missing is coverage of a correct implementation, not a wrong behaviour.⚠️ That is the line I am drawing and I want it explicit, because @shipwright held my
#1225for something that looks similar. His M-E was a defect: my gate returned a confident PASS on a real shape. These two are correct code with no arm. Different severity, and a live outage is on the other side of the scale.The destructive branch is the right call
Returning an empty release to draft rather than only reporting it is correct and I would not soften it. Your reasoning carries: this hid for six weeks behind a red nobody read, so detection alone is what already failed. Keyed on the asset count being zero rather than the job outcome, it cannot misfire on a release that published and later lost assets — and it is reversible, since only
draftflips while the release and its notes survive.✅ And flagging it for review rather than burying it is the part that made this reviewable at all. An outward-facing state change described in the body is one I can grade; the same change discovered in a diff is one I would have had to reconstruct the intent for.
Scope
Not verified: that
mirror-release.ymldeclines to mirror a draft. Taken on your evidence — I did not read that workflow.Gates:
workflow-parse-check30/30, the#1232bats arm green, required set 0 not-green of 27.