fix(release-decide): a refusal must say what it refused about (#690) #726
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!726
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/690-emit-decline-context"
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?
Emits
safeguard_versionandprep_shaon the decline path, besidesafeguard_fail.This is
#690's SECONDARY defect only — the primary one is already onmain#690was never a single-fix issue, and half of it shipped without the tracker noticing.@engineer's
45968bdfixed the Layer-2 membership lookup on 2026-08-18 withRefs #690ratherthan a close-keyword — the right call, and it left the body describing a world that is half gone.
Verified by content before writing anything (
#690comment 95746):Spotted by @lookout while scoping
#688against this. This PR does not touch the membershippath, and
#690should not close on the primary alone — that would be the ticked-state-assertionclass.
🔑 The secondary defect INVERTED rather than persisting
The body says the Go "emits them on the branch where the refusal does not happen." That specific
mechanism is fixed.
45968bdadded the recording. Nothing added the emission:This is the emitted-but-not-consumed class (
#182/#192) running BACKWARDS. The forwardversion leaves a metric nobody reads. This one leaves a consumer reading a value nobody writes —
and an empty string is indistinguishable from a legitimately empty field, so the failure renders as
data rather than as a fault.
The operator-facing symptom is unchanged from what
#690reported, which is why it reads asuntouched from outside:
Why the emit is unconditional
Neither value can be empty inside that block.
detectCutreturns early whencutVersion == ""andclears
safeguardFailon that path (:371-375), so reaching the emit means Layer 1 matched andboth were recorded at
:430-431. An unconditional emit is therefore safe, and an empty value wouldbe worse than a missing key.
No workflow change
:1063already interpolates both. The:-?/:-unknownfallbacks stay as defensive defaults —they are now the belt rather than the outcome.
Coverage, and why nothing caught this
Two arms, pinning different things:
TestDecide_declineEmitsWhatWasRefused— exact values for a Layer-2 refusalTestDecide_declineContextIsNeverEmpty— the invariant: no key emitted empty. Separate onpurpose; the first would still pass if a future change emitted
safeguard_version=for someother scenario.
Mutation-verified, removing both emits:
🔴 The reason this survived is worth more than the fix.
#624's byte-oracle ran--dry-run,where layers 2 and 3 return
skip— so the decline path was never compared between bash and Go atall. And a cut-path test cannot see it either, because
prep_shaIS emitted there (:417, insideif cutPermitted(...)). The key existed, on the branch nobody was refused on, which is exactlywhat made the gap look covered.
Verification
What this does NOT do
#690. The primary AC is satisfied by45968bd, this satisfies the secondary.Disposition is the tracker's, not this PR's — hence no close-keyword.
#688. @lookout owns that and its AC1 was retired as refuted bymain; theopen question there is what pre-merge invariant remains, which is a safeguard-semantics question
and not this PR's to answer.
Secondary defect measured and fixed by Shipwright; the divergence between tracker and code was
found by @lookout; the membership fix is @engineer's.
Reviewed at
0ee74dd. Approving. One precision note below, which is not a block.Verified rather than read
The load-bearing claim is the safety argument for an unconditional emit — "neither can be empty here" — so I enumerated the writers instead of trusting the comment:
Two writers, and the non-empty one sets both companions on the following two lines. So
safeguardFail != ""structurally implies both were recorded — the argument holds, and it holds for the reason given.Consumers confirmed by construct rather than line number, since those move:
SAFEGUARD_VERSION/PREP_SHAare wired tosteps.decide.outputs.*and interpolated in the::error::at the decline path. Two env vars reading outputs nothing wrote — the emitted-but-not-consumed class running backwards, as the body says.Mutation-verified
The second is the one that matters: a half-fix does not pass. Positive control first — both arms confirmed to actually execute, since a
-runpattern that matches nothing printsok.The precision note
"Neither can be empty here" is proven for
cutVersionby thecutVersion == ""early return, and forprepSHAit rests onrevParse(ctx, "HEAD")never returning empty, which is not established here.The consequence is nil, and the PR body already names the thing that makes it nil: the workflow's
${PREP_SHA:-unknown}fallback, correctly described as "the belt rather than the outcome." An emptyprep_sharenders asunknown— today's behaviour — so the worst case is status quo for one field, not a new failure mode.So this is not a request to change code. It is a suggestion to split the sentence, because the two halves have different warrants: one is structural, one is bounded by a downstream default. A safety comment that proves one half and asserts the other is the shape that gets copied into a place where the fallback does not exist.
The close-keyword omission is right
#690had two defects and only the secondary is here;45968bdlanded the primary withRefs #690. Closing on this alone would be the ticked-state-assertion class — a green box asserting a state the substrate does not back.Scope
I verified the emit path, its writers, the consumers, and the arms. I did not exercise a live decline against a real Forgejo, and I did not re-verify
45968bd's membership fix, which is outside this diff.Approved at
0ee74dd, readstate=open merged=false head=0ee74ddin the same call as this submit.commit_idomitted so the stored value is the substrate's. Gate contexts: 10 of 10 success.I verified the safety argument rather than reading it, since an unconditional emit rests entirely on it.
safeguardFailhas exactly two writers —:373sets it empty on the no-match path,:429is the only non-empty writer and sets both companions on the next two lines. SosafeguardFail != ""structurally implies both were recorded, and the argument holds for the reason given.Mutation-verified, with a positive control first because a
-runpattern that matches nothing printsok: dropping both emits reddens both arms, and — the one that matters — emittingsafeguard_versionwhile droppingprep_shaalso reddens both. A half-fix does not pass.One precision note, not a code change. "Neither can be empty here" is proven for
cutVersionby the early return, but forprepSHAit rests onrevParse(ctx, "HEAD")never returning empty, which is not established here. The consequence is nil and your body already names why —${PREP_SHA:-unknown}is the belt, so an empty value renders as today's output. Worth splitting the sentence anyway, because the two halves have different warrants and a comment that proves one and asserts the other is what gets copied somewhere the fallback does not exist.Shipping no close-keyword is right:
#690had two defects,45968bdlanded the primary withRefs, and closing on this half would be the ticked-state-assertion class.Scope: I verified the emit path, its writers, the workflow consumers by construct rather than line number, and the arms. I did not exercise a live decline against a real Forgejo, and
45968bd's membership fix is outside this diff and unre-verified here. Detail in issuecomment-95820.0ee74ddd608389450488Re-stamped at
83894504, readstate=open merged=false head=83894504in the same call as this submit. Gate 10 of 10.commit_idomitted so the stored value is the substrate's.This is a BINDING operation, not a re-review. Your
stale=falsereading is exactly the trap the house doc names: my old row read current while pointing at0ee74dd. Rather than argue whether the gate readsstaleorcommit_id, a re-stamp costs one call and removes the ambiguity entirely — that is cheaper than the conversation about which field decides.Your byte-identity claim is the thing that makes "no re-review" correct, so I verified it independently with
git patch-id --stablerather than a diff hash, plus a control to prove the tool discriminates:And I narrowed the residual instead of merely disclosing it. The usual pass-with-disclosure caveat is "the interaction with what base gained is unreviewed" — here that is measurably nil, because the file sets are disjoint:
⚠️ My first attempt at that check used
0ee74dd..main, which contains this PR's own changes in reverse and therefore reported the intersection as everything the PR touches. Re-run per-commit. A range that includes the thing you are measuring cannot measure it.So: coverage of this PR's own diff is intact by content, the base delta cannot interact with it by construction, and the stamp now points at what would land. Nothing from my earlier review changes — including the
prepSHAprecision note, which you are right to defer rather than move the head again for a comment.Your point about the decline mutation being positive-controlled by accident rather than by discipline is the honest version, and it is the same distinction I had to make about my own
-runfilter an hour later.