fix(release): derive the expected asset set from the tag, and refuse before deleting (#1197) #1219
No reviewers
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!1219
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/1197-derive-expected-asset-set"
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 #1197.
Intended-targets: #1197
SOURCE decides what is PRODUCED; PIPELINE decided what was REQUIRED. Both call sites hardcoded the current three-name set, while what a build emits is decided by the tag's own
.goreleaser.yaml.So every rebuild of a pre-signature tag deleted the release's assets, uploaded two, and failed verify demanding three. Measured on
v0.49.0, whose originals survived only because a capture had been taken first.The derivation is narrow, and the narrowness is measured
rt release-assets expectedreads the SOURCE config and emits the set. Across all 45 config-carrying tags the archive and checksum name templates are identical (45/45rt-{{ .Os }}-{{ .Arch }}andchecksums.txt);signs:is the only axis that varies. So the derivation reads all three from the config and refuses on any shape it has not been shown rather than guessing.Corpus check — the whole real population, not a sample:
That last line is the point: the derivation agrees with a differently-computed number, not only with itself.
🔑 Refusing is the contract, not a limitation.
prepareDELETES before the build runs, so a name guessed wrong is an asset removed and not put back. Six refusal cases each assert their own message.The ORDER was the other half of the bug
The destructive step ran ahead of both the config guard and any knowledge of the expected set. A set derived after the deletion cannot prevent it; a guard that refuses after it has already lost the assets.
⚠️ This also repairs a latent hazard in
#1194's own guard — the one I shipped an hour ago. It refused after the delete. Onv0.33.0that was invisible because there were no assets to lose; on a config-less tag that had some, it would have destroyed them and then refused.Two existing arms repaired rather than relaxed
#980's needle matched prose in a newecholine —release-assets invoked outside the pipeline rt. The arm is right to be strict about invocation; the echo was reworded. Relaxing the needle would have removed a real detector to accommodate a message.#513's required the signature sidecar via a literal, which cannot survive a derived set. Its guarantee is unchanged and is now a chain, with each link asserted separately:Without the middle link the other two prove nothing together, which is why it is a Go test against this repository's own config rather than a comment.
An arm caught a real fragility, not just a test problem
Writing
path=to$GITHUB_OUTPUTunguarded turned a passing config into a failed step wherever that variable is unset. Actions always sets it; the arm that drives the block directly does not, and the difference is a genuine failure mode rather than a harness artefact. Guarded.Mutation verification — 5 mutants, each in isolation
#513's chain and#1197's orderingsigns:M1 reddening two arms is the useful one: it shows the fail-closed guarantee and the ordering guarantee are now defended by different assertions rather than by one that happens to cover both.
What this PR does NOT do
goreleaser release. That is#980's AC4 and it stays open —#1197unblocks it rather than discharging it.{{ .Os }}/{{ .Arch }}are substituted, from a single-platform build. Anything else refuses. Where a template evaluator would be right instead: a repo shipping several archives, or a checksum name carrying{{ .Version }}— neither exists in these 113 tags.#1194already names them.signs:, so the derived set is the same three names it always was — asserted, not assumed.📌
ac-closure-checkstaleness again (rt#1211), second instance. This PR posted red because#1197had two unticked ACs; both are now DEFERRED →#980, which genuinely owns them, and the verb returns rc 0 locally. The posted status will not move on its own — the gate grades the issue and re-fires only on pull_request events. This body edit is the touch that re-fires it, recorded rather than done silently.Gates
go build·go vet·go test ./...·gofmt·golangci-lint(0 issues) ·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— every rc captured directly. Re-run after rebasing onto9b5dbe5.🤖 Generated with Claude Code
https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
APPROVED at
d864b3f7.The ordering half is the part I graded hardest, because it is the half a reviewer is most likely to take on the diff. It holds, and it holds for a better reason than the diff shows.
The corpus claim reproduces under a different method
Your
45 / 38 / 7is the load-bearing empirical claim — the entire narrowness argument rests onsigns:being the only axis that varies. I recomputed it independently: paginated the tag list to completion, fetched.goreleaser.yaml/.goreleaser.ymlat every tag, and grepped^signs:on the decoded file.Exact, from a different instrument. That is corroboration rather than agreement-manufacturing: I did not read your derivation to produce it.
The ordering test asserts a RELATION, which is the thing that matters
Your body concedes the ordering is asserted on step index and I want to say why that is the right instrument here rather than a compromise. Step order is the mechanism for a workflow YAML; a behavioural arm would need a live runner, which is
#980AC4 and correctly still open. What makes it a real test is that it assertsderives < destroys,guards < destroys,destroys < builds— relations, not presence — andidx()exits 1 with a named message when a step is absent, so it fails closed.I mutated it rather than taking your table. Textual block swaps in the YAML, one at a time:
S2 reddens with the exact diagnostic. The arm can fail in the world where the bug lived.
Position is not blocking, so I checked blocking separately
An ordering assertion is satisfied by a guard that is early and inert, so I graded the property the index cannot express:
⚠️ My first sweep for
continue-on-errorused a broken alternation, ugrep errored, and my|| echo "none"turned the tool refusal into a clean "none". I re-ran it as fixed strings with stderr asserted empty and a positive control. The finding above is from the second run. Recording it because it is the same shape as the defect this PR fixes: a step that cannot do its job, reporting success.Should-consider — one gap, and it is adjacent to the one you closed
The ordering arm asserts index but not
if:. A future edit adding a condition to the guard or the derivation passes every assertion in the test while restoring the hazard exactly:guards < destroysis still true. The guard just does not run. Both are unconditional today — I verified it — so this is a fence, not a bug. One line in the same test: assert that GUARD, DERIVE and DELETE carry noif:, or carry the same one. Cheap, and it defends the property rather than the arrangement.Nit — the
exit 0you added is load-bearing and also makes the step non-extensibleThe
exit 0is genuinely required: underset -ethe[ -n ]test returning 1 as the last command fails a passing config, which is the fragility your arm caught. But it also means anything appended below that line never runs, and will look like it does. Theif/fiform needs no terminal exit and has no dead-code edge:On repairing
#980's and#513's arms rather than relaxing themBoth calls are right and the second is the one worth naming.
#513's literal could not survive a derived set, and you replaced one assertion with a three-link chain each of which is asserted separately — including the middle link as a Go test against this repo's own config. Without that middle link the outer two prove nothing jointly, which is exactly the "N independent layers" trap, and you built the layer that makes them actually independent instead of writing a comment claiming they are.Scope — what I did not grade
#980AC4 owns it.|| truesites ininstall goreleaserand the guard — both are version-probe idioms predating this PR.Required set: 0 not-green of 26,
enable_status_check=true.