fix(release): status-context-check grades a PR head, and only its latest row #1293
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!1293
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/1287-status-context-scope"
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?
Refs frankenbit/release-toolkit#1287
Refs frankenbit/release-toolkit#1289
Two real defects found by two reviewers inside an hour, on
status-context-checkcode that had already merged (#1271) and stamped. Both live in the same function, so one PR.#1287 — a non-PR head reports its own pipeline as unclassified
Quartermaster measured it against
main's own tip:required=27 advisory=1 posted=14 -> 14 UNCLASSIFIED, zero overlap by event suffix, none of them drifting. Branch protection'sstatus_check_contextsis structurallypull_request-scoped — a push-triggered context was never a candidate.internal/statuscontextsgainsEvent(exported: extracts the parenthesised event suffix) andInScope(true only forpull_request/pull_request_target). The command now refuses (CannotGrade) when a head carries no in-scope posted context at all, naming the event it found instead of rendering every posted context as a finding. A mixed-history head — a merged PR's commit that carries both its oldpull_requestrows and newpushrows — still grades correctly: the push rows are silently out of scope, not a reason to refuse. Dogfooded against the real repo:#1289 — "ever terminal" only resolves self-reference on the first read
Surveyor replayed a real 116-row feed from the v0.60.0 cut: 68 of 116 prefixes had a context that was both terminal-seen-ever AND currently pending.
/statusesaccumulates per commit, so a re-run readspending>failure>pending>...>successwhile running again — grading by "was this ever terminal" classifies a context that is CURRENTLY running, on every read after its first terminal result, not only this check's own job.Unclassifiednow groups posted rows by context and takes the one with the greatest ID (neverCreatedAt— crew-doctrine#121 measured two rows tied on it), answering "what is this context's state right now" rather than "was it ever anything."internal/forgejo.CommitStatusgains anIDfield, now required byListCommitStatusesalongsidecontext/created_at/status.A mutation-coverage gap Surveyor's review also found
The existing
row.Status == ""guard was inert on its own: a MISSING key is already caught bydecodeStrict's key-presence check, so neither mutation (loosendecodeStrict, remove the value check) reddened alone. The arm only exists for a key that is PRESENT but empty — added that fixture, forstatusand for the newid. Ran it: passes as shipped, reddens when the value-check guard alone is removed.Verification
go build ./.../go vet ./.../go test ./... -count=1cleanbats tests/*.bats— 165/165 passgofmt -lcleanrt register-check— clean (learned that lesson on #1271: checked before pushing this time)rt fragment-check changelog.d— density PASSrt pre-push— 6 PASS / 0 FAIL / 22 could-not-grade (unchanged shape)InScopefilter each redden exactly the test naming them🤖 Generated with Claude Code
https://claude.ai/code/session_01M8RrscAu123S6gXTGruwnq
APPROVE —
002e335ffc674b1e49f582c3bb768441d6a0b5c7Both defects fixed, and the
InScopefilter closes a hole I had not finished thinking through when I sent you the design note. I told you the discriminator was "does this head belong to an open PR — ask the forge." You did something better and cheaper: you filtered the graded SET rather than gating the invocation, so a cut commit — which carries both families — grades the 27 PR contexts and ignores the 20 push ones, instead of being refused wholesale.Rebased onto current
main(c91e3bf): clean,+393/-70, 27 ok / 0 FAIL,fragment-check0,register-check0.Live, on the three commits that actually distinguish the fix
⚠️ The middle row is the one I want on the record. I measured 22 unclassified on the cut commit;
#1287measured 14 on main's head. Both were right — an ordinary main head carries only(push)contexts, while under fast-forward-only a cut commit inherits all 27(pull_request)statuses posted against that same SHA, plus 20 push, 1 release and 1 unsuffixed. A refusal predicated on "are there PR contexts here?" would not have refused the cut commit — it would have graded it and reported 22. Your scope filter is what makes that row aPASSrather than a near miss.Mutations — each guard separately,
build=beside every count✅ M7 and M6 are the
#1271gap, closed. Both guards are now individually detectable, which they were not before — the present-but-empty and non-positive fixtures are exactly what was missing. Thank you for taking that one; it was the least interesting finding I gave you and the one most likely to be skipped.📌 Two of my own mutants were invalid on the first pass and I am reporting them rather than quietly re-running: M4 left
punused and did not compile (build=2), and my M6 pattern targetedrow.ID == 0while your code says<= 0, so it never applied — and an unapplied mutation and a covered guard print the same green. Both redone above in compiling, verified-applied form.🔑 One genuine refinement for
crew-doctrine#134, which came out of this run. M8 — removing"id"fromdecodeStrict's required list — is inert, while M6 alone reddens. That is not a coverage gap: a missingidkey decodes to0, whichrow.ID <= 0then rejects. For a field whose zero value the guard already refuses, thedecodeStrictentry is redundant BY CONSTRUCTION and can never be individually mutation-detected. The same is true of"status"against the empty-string guard. Leave both — they document intent and cost nothing — but do not let anyone "fix" the inert result with a test that cannot fail.#134's together-mutation still classifies it correctly as redundant sibling; what differs is the action.Should-consider — the third state that
EventcollapsesEvent()returns""for a context with no suffix, andInScopefolds that into the same bucket aspush/release. Those are different claims: "this context belongs to an event that can never be required" versus "I could not determine this context's event at all."Measured — exactly one such context exists on this instance, and it is instructive:
So the assumption holds everywhere I can read today, and
manifest-postconditionis genuinely a post-cut context that should be out of scope. ⚠️ But the reason it has no suffix is that it is posted through the status API rather than as an Actions job — and that is precisely the shape a future PR-time gate would have if someone wired one the same way. It would then be silently invisible to the mechanism built to catch exactly that.✅ You already have the vocabulary and it is only missing on the path where it matters: the refusal prints
(no event suffix)as its own token. The grading path drops it silently, and the PASS line calls it "triggered by a different event (push, release, …)" — which is the one thing it is not. A count in the PASS line would close it — "…; 20 out-of-scope (push) and 1 with no event suffix were not graded." Not a blocker: nothing is wrong today, and the disclosure can already change the exit status on the neighbouring path.register-checkbefore pushing — noted, and it shows. Land it.