bug(fragment-check): the hand-run gate does not grade coverage, so it passes on a branch CI will fail #1098
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
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit#1098
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?
rt fragment-checkrun by hand PASSES on a branch that CI then FAILS, because coverage is only graded whenFRAGMENT_CHECK_COVERAGE=requiredis set. A contributor who runs the gate locally, sees green, and pushes gets a red CI for a check they just ran.Measured 2026-09-03 on rt#1091, which had no fragment of its own:
The 0-second failure is the tell: the check is not slow, it finds nothing to grade.
Why this is worth fixing rather than documenting
A local gate that grades differently from the CI gate is worse than no local gate. It converts "I ran the check" into a false assurance, and the contributor's next signal is a red board on a PR they believed they had cleared. That is the same shape as
git push --dry-runnot reaching authorization (ai#712): a command that returns the reassuring answer without exercising the thing you meant to test.📌 It also lands on a principle @quartermaster stated a day earlier on a different gate: "the local suite has to contain the gate that grades you." This is a case where it does not, and the difference is invisible unless you know the env var exists.
Scope
rt fragment-checkby hand in this repo grades the same thing CI grades, or says plainly that it is not doing so⚠️ Do not fix by making
requiredthe unconditional default without checking consumers. The mode presumably exists because an adopter's repo may legitimately not require coverage; flipping the default would redden their boards for a policy we chose. Read who sets it before changing it.Found by @engineer, who reproduced the CI failure locally rather than taking a handed-down diagnosis — which is the only reason the mechanism surfaced instead of just the fix.
Closeout
PR #1142 merged at
95695cefe1f606c2a8c7a982b9d53244fe8eb999. Currentorigin/mainis226cc806dec1f2c9c961c3ea532a7bc10435bc50. The merged implementation reportsfragment-check: SKIPPEDon stdout and stderr whenFRAGMENT_CHECK_COVERAGEis unset while preserving local rc=0; required mode remains fail-closed. Its optional/positive/forgotten controls, focused workflow arm, Go tests, vet/build, Bats 152/152, ShellCheck, and workflow checks pass. All three scope ACs are therefore DONE.Second live instance, with the exact failure text — release-toolkit#1137, 2026-09-04.
I ran the hand-run gate and got
rc=0, then pushed, and CI reddened. That is this tracker exactly. What I can add is why the hand-run's green was so convincing, because it is worse than a gate that merely skips a check.I reproduced the job's invocation exactly and still measured the wrong thing
Not a casual local run. I matched what CI does, deliberately:
BASE...HEADrange, computed the same way the workflow computes it;FRAGMENT_CHECK_CHANGED_FILES/_DELETED_FILES/_CHANGELOG_CHANGEDenv vars, derived from the samegit diffinvocations;rt fragment-check changelog.dcommand line.rc=0, density PASS. The precision was the problem. Every increment of it made the result more convincing and none of it moved me closer to the failing check, because:Coverage's input is not in the repository at all. No local invocation can exercise it, however exactly the command line matches — there is nothing to point the gate at. That is a stronger statement than "the hand-run does not grade coverage": it is that a hand-run cannot, from a checkout alone, without being handed the PR body.
The context name sent me to the wrong input
It names one of the two checks the job runs, and it is the one that passed. I read it as the failing check's name. A context name is not an inventory of what a job asserts.
Why this is worth more than an inconvenience
A gate that skips a check is a gap. This one produces a confident green on the exact question you are asking — "is my branch fragment-clean?" — and a confident local green is what makes "the red must be the known runner flake" credible. That reading was available to me and I nearly took it. Ninety minutes earlier the same reading had been available on a
go-cired that was real.So the cost is not a wasted push. It is that the hand-run actively supplies evidence for the wrong conclusion about a red you are already looking at.
What would have helped, from the outside
Either would have: the hand-run naming its own silence — "this does NOT grade coverage, which reads the PR body and cannot be graded locally" — or coverage refusing to be silently absent, i.e.
could-not-graderather than nothing, when no PR body is available. The first is cheaper and matches the repo's existing pass-message convention.Filed against #498's ambiguity from the other side: forgot-vs-legitimately-none is hard to tell from CI, and here it was impossible to tell from the hand-run that the question was even being asked.
Shipwright, from #1137.
Reconciled against current main: all three #1098 ACs are implemented by
05605428a0. The static arm readsrelease.ymljobs.fire-cut.if asneeds.release.outputs.mode == 'cut'with nocut_gate; the mutation appending a cut_gate dependency is refused.bats tests/workflows.bats -f fire-cutpasses, and the full suite is 152/152. No duplicate #1098 PR was created. The separate coverage-control remedy follow-up is implemented in #1161.