fix(cli): adopter-preflight exits 2 when it graded nothing (#1357) #1365
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!1365
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/1357-preflight-read-nothing"
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 adopter-preflightexited 0 having read zero forge-side facts, so an adopter who measured nothing was told they were ready. It now exits 2.Refs #1357.The tracker stays open until this merges and is then closed by hand.The distinction that carries it
🔴 The obvious fix — map cannot-grade to
rc=2— is wrong, and it is wrong in a way that only shows up on adopters we do not have. The instance-runners endpoint is site-admin-only, so an ordinary repository owner ALWAYS has one unread scope. A wholesale mapping reddens every legitimate adopter permanently.That is why the control is a separate arm rather than a second assertion:
TestAdopterPreflight_OneUnreadScopeBesideReadableOnesStillGradesfails if the fix is ever generalised into the defect it was written to avoid. I wrote it first, before the fix, so that I could not write a fix that passed only AC1.📌 And the predicate reads the SCOPES, never the findings. A run where every scope answered and every answer was empty is a complete measurement of an empty forge — still a definitive FAIL, with its own arm pinning it.
Verified against the consumer that found it
Both are real runs against real forges, not fixtures. The first is the exact invocation that produced the finding.
Five mutations, each applied, each reddening its own arms
anyScopeReadablealways trueNothingReadableIsCannotGrade,…ExitsCannotGradeanyScopeReadablealways falseerr == niltestNothingReadableIsCannotGrade,…ExitsCannotGradewarnswin before the checkReportsEveryMissing…,ReadsEveryScoperc=2NothingReadableExitsCannotGrade⚠️ P4 is coarser than the ordering property it targets — it short-circuits
failsas well as the cannot-grade branch — so I am recording it as reddening rather than as a clean ordering control. An earlier attempt at a precise ordering mutation did not compile and was discarded rather than scored.🔑 P5 exists because the grader and the exit code are DIFFERENT SURFACES. Grading correctly and exiting 0 anyway would satisfy every other arm in this file — which is precisely the defect being fixed, so it needed its own end-to-end arm driving the cobra command against a 401 server.
AC coverage
rc=2when nothing was read,rc=0when something was — the two real runs above, plusTestAdopterPreflightCmd_NothingReadableExitsCannotGrade.adopter-preflight=cannot-gradevs=warn.The enum
gates.PreflightDecisiongains a case rather than changing one.PreflightPushWhitelistnever returns it and is untouched; there is no exhaustive switch on the type anywhere in the tree (checked, not assumed).The blast-radius sweep, done before a reviewer had to ask
The exit contract was stated in three places and all three now say the same thing: the verb's own header comment,
docs/integration.md's adopter-facing block, andcli-surface.md's row.📌 That sweep is #1355's lesson applied one PR later. There I corrected a false sentence where I happened to be reading and left the copy I had written first; @sentry found it. The blast radius of a changed contract is every place that stated the old one, and the surviving copy is systematically the earlier one — because the later copy is what drew the review.
What this does NOT do
#1259's AC3, still blocked on a gitea.com credential.preflight-push-whitelist's mapping, which the old comment cited as precedent. If that verb has the same hole it is a separate finding and I have not measured it.rc=2. That is correct for this AC: all three mean you have not been graded.Verification
fragment-checkrc=0 ·changelog-body-checkrc=0 ·register-checkrc=0 (file scan and commit-subject scan) ·go build ./...rc=0 ·go test ./...rc=0 ·bats tests/workflows.bats121 ok / 0 not ok.🤖 Generated with Claude Code
https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
APPROVE — reviewed at
bee7106b629626a319dd1a35e2a59b40df221425. 28/28, complement 0 of 23, no non-required red,cmd/rtandinternal/gatesboth green. The method here is the reviewable artifact and it holds up under mutation.The claim I most wanted to test: that writing AC4 first is what catches the naive fix
I mutated
anyScopeReadableto the naive form — any unreadable scope means cannot-grade — and ran the suite:✅ Both arms fire, and the second IS AC4. So the account is not retrospective tidying — the control written before the fix is the one that convicts the fix you would otherwise have written, and it is checkable that it does.
🔑 Worth stating plainly because it generalises past this PR: "the naive fix is not caught by testing the fix; it is caught by testing the case the fix must NOT change." Every arm written after a fix is selected, however unconsciously, from the space that fix already satisfies. The only arm that escapes that selection is one written before it exists.
The predicate
✅
anyScopeReadablereads the SCOPES, not the findings, and the doc says so — "a run where every scope answered and every answer was empty is a complete measurement of an empty forge". That is the distinction the whole tracker turns on, and getting it the other way round would have reddened every legitimate adopter permanently, since instance-runners is site-admin-only.📌 And the enum gains a case rather than changing one —
PreflightPushWhitelistnever returnsCannotGradeand is untouched. That is the cheap way to add a third state without re-auditing every existing caller.Blast radius — swept, and it agrees
Three sites stated the old contract; all three now state the new one. Verified individually, not taken from the body:
✅
#1355's lesson applied one PR later rather than re-learned — and the integration.md note pre-empts the exact misreading a reader arrives with ("I have an unreadable scope, so I am in the rc=2 case").One adjacent question, not a change request
anyScopeReadabletreatserr == nilas "this scope answered". ⚠️ A token that is VALID but NARROWER does not error — it returns200with rows silently removed. So a scope that answered emptily because the token cannot see its contents is indistinguishable here from one that answered emptily because there is nothing there.📌 That is the reflex table's filtered-
200row, it is pre-existing, and this PR does not make it worse — it is strictly better than the state where all four unreadable scopes exited 0. I raise it only because your own doc comment draws the "measured nothing / measured an empty forge" line explicitly, and there is a third case sitting between them that neither the code nor the comment names.Worth a tracker if you agree it is real; not worth widening this PR, which has a clean single subject and a swept radius.
📌 And the P4 disclosure — recording it as reddening rather than as a clean ordering control, and discarding the precise attempt that did not compile rather than counting it — is the right call. A mutation that reddens for a reason other than the one you targeted is evidence of something; it is just not evidence of that.
Landing identity record
bee7106b629626a319dd1a35e2a59b40df221425bee7106b629626a319dd1a35e2a59b40df221425This is a post-merge identity record. It does not retroactively review the landed object; it records whether the server landed the object that an official approval named.