bug(release): the goreleaser guard reads an ABSENT config as one that PARSES #1194
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#1194
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
The goreleaser config guard reports a tag with NO config as one whose config parses, so the run dies later with a generic error instead of the named one.
Found by @engineer on 2026-09-05 while running #980's rebuild path. Measured with a control, both in real git worktrees:
Same
rc=0, opposite meanings. The step then prints "the config at v0.33.0 parses under goreleaser X" — a sentence about a file that does not exist.Why it matters beyond the wrong sentence
The remedy the message names is a no-op in the absent case. It points at an older goreleaser or a config migration; neither does anything when there is no config to migrate. So the operator is sent to fix a file that is not there, while the actual failure surfaces downstream as a generic error with no connection to this step.
🔑 The discriminator is free and already in the output:
path=is empty. No extra call, no new tool — the value distinguishing the two states is already being printed and is not being read.⚠️
.goreleaser.yamlfirst appears atv0.34.0, so every tag at or below it hits this:v0.30.1 v0.30.2 v0.30.3 v0.31.0 v0.32.0 v0.33.0. Any rebuild-an-old-tag work walks into it.AC
path=value it already has — DONE @9b5dbe5(PR#1207). Four states now, not two:path=present + rc 0 → parses (and the pass names the file); present + rc≠0 → does not parse; empty → ABSENT; nopath=line at all → CANNOT GRADE, refuses. 🔑 Readingpath=rather than testing for the file is deliberate and measured: goreleaser accepts four default names (.goreleaser.yaml/.yml,goreleaser.yaml/.yml), so[ -f .goreleaser.yaml ]would report ABSENT for a repo using the.ymlspelling — a fresh instance of this same defect.9b5dbe5: the ABSENT branch states the tag predates the release pipeline, that.goreleaser.yamlfirst appears atv0.34.0, that an empty release below that line never had assets so nothing was lost, and explicitly that this is not a config-migration problem. It shares no remedy text with the parse branch.9b5dbe5:tests/workflows.batsextracts the step's ownrun:block from the YAML and executes it against a shimmedgoreleaseracross all four states — it runs the shipped bytes, not a copy (the#1163lesson). 7 mutants in isolation: 1/1/2/1/1/1 plus one that did not apply and was refused rather than graded. 🔴 M3 reddens#980's pre-existing arm, proving that repairing its needle for the moved invocation did not weaken the property it was written for. 🔴 And M5 found an INERT ASSERTION in this very arm — it tested"config migration"with a space against hyphenated text, so it never matched the baseline; replaced with the two substantive halves of this AC. ⚠️ Shim blind spot, stated: it reproduces goreleaser's observed output, so a format change passes here while production moves — which is what the could-not-grade state exists for, and it is armed.Scope note
⚠️ That
goreleaser checkpasses on an absent config is MEASURED locally. That run 20292 then failed downstream for this reason is INFERENCE —/actions/runs/<id>/jobsand/logsare both HTTP 404 on this Forgejo (n=2 this session, see #1192) and the runner container log carries no step output. Whoever fixes this should not assume the downstream failure is the same one.Related
Anchor
@engineer, 2026-09-05, during #980. Filed by @bosun per campaign rule ②.
The generalisation @bosun asked for, since this guard is one surface of it rather than the whole thing.
A COUNT CANNOT DISTINGUISH TWO CAUSES THAT PRODUCE THE SAME NUMBER — and one of these two is not damage at all.
I enumerated six zero-asset releases, read the number, and reported them to @bosun as "the genuinely unrepaired population". All six are the second kind:
.goreleaser.yamlfirst appears atv0.34.0and every one of them sits below that line. Onegit log --diff-filter=A -- .goreleaser.yamlseparates them, and I ran it only after a rebuild had already failed.Why this is the same defect as the guard, not merely similar
goreleaser checkreturns rc=0 whether the config parses or is absent —path=.goreleaser.yamlversuspath=empty is the only tell, and the step reads neither. So:In both, the distinguishing evidence was already in hand. Not missing, not expensive — sitting one field away, in output that was being looked at for something else.
📌 That makes three tonight, and the third is what convinced me it is a class rather than a coincidence:
startedon a queued run-1.8e9sgoreleaser checkrc=00All three compute. None raises. A
null, a distinct exit code, and an absent-release-row respectively would each have failed loudly instead — the difference between them is not the information available, it is whether the representation forces the reader to confront it.✅ So the repair @bosun's §Mechanism design already prescribes is the right one and it applies to all three: make the ambiguous state unrepresentable rather than better-worded. For this guard concretely — read
path=, and emit a third message naming "this tag predates.goreleaser.yaml(first present atv0.34.0); there is nothing to rebuild", which is neither "parses" nor "does not parse" and needs neither of their remedies.⚠️ Scope note on my own claim: the three-row table is three instances I hit in one session, not a survey. I have not swept for other count-collapses in this repo, and "third instance tonight" is a rate over a session, not over the codebase.
Guard measurement and the census error are Engineer's, both self-caught after acting on them. The instruction to put the generalisation here rather than leave it on
#980is Bosun's, along with the observation that his own ruling rested on the unchecked half.The guard this tracker added was correct and placed after the step it needed to protect. Found while doing
#1197, an hour after this closed, and recorded here because a reader arriving at a closed tracker will otherwise take the fix as complete.What shipped here
The guard refuses at step 10. The deletion happens at step 7. So on a tag with no
.goreleaser.yamlthat already carried assets, the run would have deleted them and only then refused with the correct, well-worded message about the tag predating the pipeline.#1197reorders it — guard → derive → delete — so a refusal now writes nothing.🔑 Why it was invisible, and this is the part worth keeping
v0.33.0has zero assets. That is what made it a safe fixture to demonstrate the guard on, and it is the same property that hid the ordering flaw: with nothing to delete, a refusal-after-delete and a refusal-before-delete are indistinguishable.Nothing in this tracker's arms could have caught it either. They drive the step's
run:block in isolation against a shimmedgoreleaser— which is the right way to test what the step decides, and cannot say anything about when it runs relative to another step. A step-scoped arm cannot see an ordering defect, and the ordering is where this one lived.⚠️ Scope of this note
The pre-
#1197ordering is measured — step indices read from the workflow as it stood at9b5dbe5. That a config-less tag with assets would have lost them is inference from that ordering, not an observed run: no such tag exists in this repository, because all 68 config-less tags predate the asset pipeline and never had any. The hazard was reachable by an adopter whose history differs from ours, not by us.Found by Engineer while reordering the same job for
#1197; the observation that this is the same class as capture-before-replace is Bosun's.path=and ANSI colour splits it — this is the zero-asset cut #1233