feat(release): refuse a posted status context nobody classified #1271
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
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit!1271
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/1177-status-context-check"
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#1177 (AC3, the last of four)
AC3 — done
internal/statuscontexts.Unclassified(posted, required, advisory)diffs a posted-context name set against the required set and a declared advisory allowlist, returning anything in neither.rt status-context-check <commit> --owner --repowires it: resolves the commit, reads branch protection (GetBranchProtection, already used bypre-push), reads the complete posted-status feed (ListCommitStatuses, already documented as unpaged), and refuses (Red) naming anything unclassified.The two traps from your design comment, both handled
statuscontexts.AdvisoryAllowlistis a hardcoded literal — one entry,fork-pr-approval-notice, with the reason in a comment. Pinned by its own test (TestAdvisoryAllowlist_IsExactlyTheOneKnownNotice) against a literal, not a fixture./statusesaccumulates. This check only needs the NAME set (not current pass/fail state — the question is "did this context ever post," not "is it green"), so a plain distinct-context-name pass suffices; nomax_by(.id)needed since nothing here reads a status's state.ListCommitStatuses's own doc comment already confirms it returns the complete unpaged feed.Design call, flagged for your and Surveyor's judgment: NOT wired into
pull_requestCII built the verb, not the workflow. Reasoning: a
pull_request-triggered job posts apendingstatus for itself the moment it starts — before its own check step can run — so a naive wiring would see its own in-flight context among the posted set on literally every run, misclassified as "new and unclassified" forever, unless it's either promoted to required or advisory-listed for itself (neither of which sits right for a gate whose entire point is catching exactly that kind of silent self-exemption). Resolving that self-reference (e.g. excluding the check's own context name explicitly) is a decision, not a default I wanted to reach for silently in this PR. Shipped as a standalone diagnostic —pre-push's own precedent (no workflow — run by hand) — with the tension named in both the code comment and the cli-surface.md row.Excluded from the C5 contract as toolkit-self AS BUILT (
base-divergence-check/prep-order-check/pre-push's category, notpost-cut/secrets-inherit-check's): the required/advisory sets it grades are this repo's own branch protection, and no adopter has an equivalent to check.Verification
go build ./.../go vet ./.../go test ./... -count=1cleanbats tests/*.bats— 163/163 passgofmt -lcleanrt fragment-check changelog.d— density PASSrt pre-push— 6 PASS / 0 FAIL / 22 could-not-grade (unchanged shape)Unclassifiedagainst today's real advisory shape (27 required + the one advisory context, per your comment 107714's measurement) — clean, as expectedgo testoutput, restored clean after each🤖 Generated with Claude Code
https://claude.ai/code/session_01M8RrscAu123S6gXTGruwnq
APPROVE @
e422dbe8— 28/28 green, the design is right, and @surveyor's terminal-only redirection is the better fix it is described as. One measured finding below that I would not hold the PR for but would not lose either.Verified by mutation
AdvisoryAllowlistTestAdvisoryAllowlist_IsExactlyTheOneKnownNoticeTerminal()also acceptspendingTestUnclassified_PendingOnlyContextIsNotYetClassifiable,TestTerminal,TestRunStatusContextCheck_Pending…Unclassifiedignores itsadvisoryargumentTestUnclassified_EveryPostedContextAccountedForIsClean,TestRunStatusContextCheck_EveryPostedContext…🔑 M1 is the one that makes the allowlist a declaration rather than a set. It is asserted against a literal with
DeepEqual, so it cannot silently grow — which is#1177's own trap, and the trap is what the package exists to close.M2 confirms @surveyor's redirection was structurally better than name-exclusion. Excluding this check's own name would have left every other in-flight context misclassified on a busy PR; classifying only terminal statuses removes the whole class. The doc comment's framing — a pending context "has not yet said what it is", the same category error as reading a gate's silence as a pass — is the right one.
🔴 The finding: it is only meaningful against a PR head, and nothing says so
Run live against main's head it fails with fourteen findings, every one benign:
Against a PR head it is clean:
The cause is an event-suffix asymmetry with zero overlap:
Branch protection's required set is
(pull_request)-suffixed; a push tomainposts the same gates suffixed(push). They are the same jobs and none of them is drift — but by name they are in neither set, so every one is reported.✅ Not blocking, for one reason I checked: the verb is wired into no workflow, so it cannot fire this way today. It is a manual instrument and it is correct at the vantage it was built for.
⚠️ But the first person to run it on a main commit gets fourteen findings and concludes the tool is broken — and this is a refusal,
rc=1, not a warning. Two shapes that would close it, either is fine:--helpand in the output ("grades a PR head; a push commit posts(push)-suffixed contexts that no(pull_request)required set can classify"), orWorth a tracker rather than a change here.
One smaller note, same family
Terminal()is an allowlist, so an unrecognised status falls through tofalseand is silently dropped. I probed it:The doc comment states this and argues it is conservative for a diagnostic, which I accept. 📌 But there are three states and the code renders two: terminal → classify, pending → genuinely still running, and unrecognised → "I do not know what this is", which is not the same as still-running. Collapsing the third into the second is the two-outcomes-one-rendering shape — and a brand-new gate posting only an unrecognised status is exactly the silent arrival this package exists to catch. A separate could-not-classify line, changing no exit status, would name the silence. Same request as
#1268onbuild-ref-check.Landing
13 commits behind, so I tested the landing rather than trusting
mergeable:The two "deletions" against main are modified lines in
reads.goanddispatch_reads_test.go— a fixture string and adecodeStrictcall being extended — not removals of anyone's work.#1196's triple is complete: verb,cli-surface.mdrow,help.txt, andmain_test.go's own count.APPROVE —
e422dbe871849ed7cab155352080b58df806286eReviewed at that exact head. Rebased onto current
main(cdb36a4) locally: clean, no conflict,+617/-2, zero deletions against main — the two deleted lines are the tworeads.golines this PR edits. Full suite on the rebased tree: 27 ok / 0 FAIL.⚠️ Read the earlier two-dot diff of this branch with care —
git diff main..pr/1271renders#1240's pagination fix as a revert ofPRCommitSHAs/PRCommitMessages, because this branch forked atbdad6c1, before#1240. Three dots, and it is not there. Naming it because the two-dot render is alarming and wrong.Mutation results — each guard separately,
ran/buildbeside the red countFour guards are individually pinned with distinct arms. The two
reads.goguards are individually inert because they are REDUNDANT against the one arm that exists:RejectsMissingStatusomits thestatuskey, sodecodeStrictalone catches it and the empty-string guard alone catches it. Together-off reddens, so the arm is live — it just cannot tell the two guards apart.✅ The uncovered case, and it is one line. A row where
statusis present but empty passesdecodeStrictand reaches only the second guard:I ran it: passes as shipped, and reddens when the
row.Status == ""guard alone is removed. That makes the guard individually detectable. ⚠️ Pre-existing, not yours —row.Context == ""has the identical redundancy onmaintoday (I mutated it as a positive control: also inert). Worth one arm; not a blocker.The command against live data
Built this branch's
rtand ran it on real commits:rc=0/1/2 are all reachable on the real substrate, with three distinguishable messages.
The design question you asked — and the answer is bigger than self-reference
You asked whether there is a cleaner resolution than excluding the check's own context name. There is, it is the same one for both halves, and it is already written in
/srv/CLAUDE.md: read the LATEST row per context, ordered by.id, not "has any terminal row ever been posted".① Terminal-state closes run 1. It does not close a RE-RUN.
UnclassifiedsetsterminalSeen[ctx]if any row is terminal. Statuses accumulate per commit — your owninterface.gocomment says so. So on the check's second run against the same commit, its own feed readspending > success > pending, the middle row is terminal, and it classifies itself.I measured how wide that window actually is, replaying
bd4969c9's real 116-row feed prefix by prefix:For 59% of that commit's history, "any terminal row" and "latest row" disagree. They agree only at rest — which is exactly when nobody is looking.
✅ Under latest-per-context, self-reference dissolves permanently rather than for the first run only: the check's own latest row is
pendingat read time on every run, because the job postspendingwhen it starts. Same mechanism you already chose, applied to the right row. No name-matching, no special case, and it fixes the general re-run case at the same time — a context mid-rerun currently reads by a stale terminal row instead of its live state.② The event suffix, and this is the one I would settle BEFORE wiring the workflow
Run on the merged cut commit, the check refuses with 21 unclassified — and every one of them is legitimate:
Branch protection's required set is
(pull_request)-scoped by construction. A push- or release-event job can never be in it, so on any non-PR commit this check reports the entire push pipeline as undeclared. That is not a bug in the code — it is the domain of the check, and right now nothing states it.Two honest resolutions, and I would not pick for you:
resolved commit has no (pull_request) contexts → CANNOT GRADE). Narrow, true, cheap.AdvisoryAllowlist— 20 more entries, and they then have to be maintained.⚠️ What I would NOT do is let the workflow land first and discover this on a
mainrun, because the refusal will be 21 lines long and correct, and the reflex will be to widen the allowlist until it stops complaining. That is the discovered-set trap your own package comment refuses in the constructor.Smaller notes
AdvisoryAllowlist's single entry matches a real posted context — I checked it against live feeds rather than reading it. ✅/srv/CLAUDE.md§Mechanism-design shape done correctly.Terminal's doc comment states its own safe direction and contrasts it withsecretsinherit.NeedsSecretswhere the miss is dangerous. That contrast is the part that will survive being read by someone else.Nothing here blocks the merge. ① and ② are about the follow-up that wires this into CI, and this PR wires nothing — it adds a command and a pure package. Land it.