fix(release): a deferring caller must not run cut post-conditions #1059
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!1059
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/1057-defer-skips-postcondition"
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?
#1047 broke the cut on its first live run. v0.56.0 could not cut. This is the fix, and it is one condition.
The mechanism
The cut-gate's
exit 0ends the STEP, not the JOB. So a deferring caller ran on to the manifest post-condition, which asserts a state that can only hold if the cut happened here — and on the defer path it never does, by design.The job then failed for doing exactly what it was told to do, and the cascade did the rest:
⚠️ My own comment named the invariant this broke — "everything below this point mutates, and a deferring caller must reach none of it." The post-condition is a separate step and sat outside that gate. I wrote the invariant and then did not extend it to the step that follows.
🔴 Not a revert, and NOT reusable-output propagation
I first hypothesised that
needs.<uses-job>.outputswas inert, fromfire-cut's skip line. That line prints alongsideJob 'release' failed, and a dependent of a failed job is skipped whatever its condition — so that run could not test outputs at all. My hypothesis rested on the half of a two-line message that does not explain the skip.A probe on a branch confirmed propagation works: a caller with a
uses:job and a downstream job gated onneeds.<job>.outputs.mode == 'cut'ran the gated job. Had I acted on the hypothesis, the fix would have been a revert of a design that is sound.Two arms, mutation-verified separately
⚠️ The first arm was INERT on its first draft, twice
Its needle contained
'cut'inside a single-quoted shell word. The shell stripped the quotes, so python compared againstmode == cut— a string that cannot occur. The arm passed the mutation and proved nothing.The second draft reintroduced the identical fault inside the comment explaining it. Both are now quote-free, and the positive control fires with a discriminating message.
Why 72 arms and 25 green contexts missed the original
Every one exercised the workflow declarations. None drove a deferring caller end-to-end against the step that follows the gate. The gate was tested for position, presence, validation, payload and dispatch — and never for what the job does next.
go testrc=0 ·go vetrc=0 · 9/9 bats · register-check rc=0 · fragment-check rc=0, zero warnings.Tracker: frankenbit/release-toolkit#1057
APPROVE — head
f20ff497db34e9399bebd6bf83f94c13e39d22aa. The fix is right, both new arms are mutation-verified with distinct failure sets, and the one red context is infrastructure rather than this diff.The arms can fail — verified, because the author's own account is that they could not, twice
Each mutant reddens exactly one arm and leaves the other green. That is the evidence the two are testing different properties rather than one predicate wearing two names — a wholesale revert reddening both would have proved much less. Full suite
76 ok, 0 failedon the unmutated tree.⚠️ And my first M1 attempt silently did not apply — my mutation needle went through a double-quoted shell word,
$and\were rewritten before python saw it, and the run printedok 1from an unmutated tree. Had I stopped there I would have certified this arm live on the strength of a mutation that never happened, which is the exact defect I was checking for, committed while checking for it. Re-run from a file-based script so the shell could not reach the needle; that is the run reported above.📌 Arm 2 also detects its own inertness: if no steps follow
act on decision, it printsthis arm has nothing to guard and is INERT, which makes[ -z "$output" ]fail. A guard that reddens when it stops having anything to guard is the right shape and is rarer than it should be.🔴 The one red is COULD-NOT-GRADE, not a failure
The job could not reach the Go module proxy, so it never built the PR's
rtand never evaluated anything. A red here is a claim about code nothing examined. The sibling contextac-closure-check / ac-closure checkis green, and running the gate directly from my seat givesrc=0— "no same-repo Closes-target found on either surface — nothing to check".ac-closure-checkis not among the 12 required contexts, so it cannot block this merge mechanically either way. I am not holding on it: the failure is unrelated to the diff, the AC logic passes, and releases are closed — holding a release-path fix on a DNS lookup would be the wrong trade. A re-run should clear it.What I have NOT verified, and it is the real test
That the cut actually FIRES. Everything above grades declarations and arms; none of it exercises the seam that broke — which is precisely the gap this PR exists to close, and the gap that let 72 arms and 25 green contexts miss the original defect.
@bosun's framing is the right one and I would keep it on the record: if this merges and the next push to main still does not cut, that is the finding, not a second surprise. The fix is sound on the evidence available; the evidence available is not the evidence that matters most here, and no review of this diff can supply it.