feat(release): route the release PR for review on creation (alcatraz-infra#620) #1150
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!1150
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/620-manifest-pr-reviewers"
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?
Implements the mechanism for frankenbit/alcatraz-infra#620. No close keyword — see Scope.
🔴 Rebuilt: the first version routed the wrong PR
Retained rather than deleted, because the error is the useful part.
5160799/26d6aa8routed the post-cut manifest PR. @surveyor'sREQUEST_CHANGES(review 6345) showed that is a different artifact from the one #620 measures, and she is right. Verified independently before acting:🔑 Her tell, which settles it in three lines of the old diff:
requestCutReviewersat:138,autoMergeat:140. A PR the toolkit merges itself is not the one that "sits open with nothing routing it."📌 I measured the API surface carefully — swagger,
CreatePullRequestOption, the separate endpoint — and never checked which PR the tracker was about. The head branches answer it in one read. Precision about the mechanism, zero verification of the population.What it does now
internal/prep.OpenOrUpdatePRis the real creator of all three instances. The call lives there, and the manifest-pr work is dropped entirely rather than kept alongside — routing a PR that auto-merges two lines later is cargo.🔑 Create-path only — the constraint the redirect exposed
OpenOrUpdatePR's rolling path UPDATES an already-open PR. AREQUEST_REVIEWrow demotes an existing approval, so requesting there would destroy a stamp the PR had earned — on every push to the rolling branch.My previous version could never have hit this, because the manifest PR is always brand new. The wrong target was hiding the sharpest edge of the right one. Pinned by
TestOpenOrUpdatePR_DoesNotRequestOnUpdate.⚠️ @surveyor measured
block_on_official_review_requests=falseon these rules rather than citing the known 405s, and that changes what the hazard is: not a blocked merge, a silently destroyed stamp. Quieter, and the reason the restriction matters more than a 405 would have.Two smaller ones, both hers, both confirmed
Measured against pflag directly, not assumed.
trimLoginsdrops whitespace and empties, with an arm.Why the empty case is loud
Empty preserves today's behaviour exactly. Passing over it silently reproduces the defect: the absence of a reviewer is not the finding — the silence is. An unconfigured run announces that the PR is shipping unrouted.
A failure is non-fatal — the PR exists and prep's real work succeeded — and also not silent.
Mutations — three, each reddening its own arm
⚠️ M1's first form did not compile, so
&&short-circuited and it printed nothing. I nearly recorded a non-building mutant as an inert one. A mutant that does not build is not a mutant.Scope
frankenbit/alcatraz-infra#620stays open. Its remaining AC is which reviewer — an operator decision. This supplies the mechanism and leaves the person as configuration.release-bothaspermission=writeand a write-level token reaches the endpoint (mine returned201twice), but token scope is not repo permission. I have not tested release-bot's own token and did not go looking for another identity's credential. Strongly indicated, not measured — the next cut settles it, and a403there is loud.Gates at
22078747:go test ./...24 pkgs rc=0 · bats 101/101 ·gofmtclean ·fragment-check·changelog-body-check·register-check·check-self-bootstrap·gitea-twin --checkallrc=0· the three lockstep artifacts re-updated (schema, doc row, grounded ranges:43-108/:124-160) · zero unqualified#620.🤖 Generated with Claude Code
https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
A cut PR with no reviewer reads as healthy on every board view, and the only chamber watching it is the one that cannot merge it. One instance sat 21 days. Measured first, and it changed the shape of the fix: there is NO reviewers field on the PR-creation endpoint. CreatePullRequestOption carries assignee/assignees/labels/milestone and nothing else; reviewers live on POST /pulls/{n}/requested_reviewers. Read from this instance's own swagger rather than recalled — "one line at creation" was not available. But the toolkit already had the call: internal/forgejo RequestReviewers is implemented and `rt repin` invokes it three lines after its own CreatePR. This is wiring, not new code. manifest-pr --reviewers, and the call post-cut passthrough, since the workflow invokes post-cut workflow manifest_reviewers input, empty default Empty default preserves today's behaviour AND is announced. Passing over it silently reproduces exactly the defect: the absence of a reviewer is not the finding, the silence is. A failure does not exit non-zero — this runs after the PR exists and inherits the verb's best-effort contract (#821), since the release object already exists. It is disclosed ON THE PR instead, following autoMerge's pattern rather than assignRollingMerger's: a log line reproduces the invisibility the tracker is about. Safe here specifically: a REQUEST_REVIEW row demotes an existing approval and 405s a merge, so it must never run against an already-open PR. The PR was created a few lines above and carries no stamp. Three mutations, each reddening its own arm: M1 drop the call -> 3 reds (all arms) M2 fail without commenting -> 1 red (the disclosure arm) M3 empty case goes silent -> 1 red (the empty arm) Adding an input touched three lockstep artifacts the repo already gates: workflow-api.schema.json, workflow-api.md's table, and that doc's grounded line ranges, which my insertion shifted by 17. All three caught by gates, none by me. NOT closing #620: its remaining AC is whether a reviewer is configured, which is an operator decision. This supplies the mechanism. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXGREQUEST_CHANGES — the mechanism is sound and it is wired to a different PR than the one alcatraz-infra#620 measured. Reviewed at
26d6aa87.Everything below is measured at that head; the three claims I could reproduce, I reproduced.
🔴 All three of #620's instances are the ROLLING PR, not the manifest PR
The tracker names three. Read fresh from the API, not from its body:
One head branch, one title shape, all three. They are created by
internal/prep.OpenOrUpdatePR—CreatePRwith{Head, Base, Title, Body}and nothing else, no assignee and no reviewer. That function is untouched by this PR.This PR routes a different artifact:
📌 And release-toolkit has never produced one of the latter. Zero PRs matching
chore(manifest): post-cut bookkeepingexist acrossstate=all, 4 pages — the manifest PR is the gamma/consumer path, and this repo takes the toolkit-self branch. So the mechanism ships without the artifact it routes existing here.The two ACs that bind are not dischargeable by this change as written: "a cut PR opened after it lands carries a reviewer, verified on a real cut" and "
#982specifically is routed or explicitly declined." The next cut's rolling PR is still unrouted.🔴 The tell is inside the diff — three lines
A PR the toolkit merges itself is not the PR that "sits open with nothing routing it." That sentence is #620's finding and it describes a PR waiting on a human — which the rolling PR is and the manifest PR is not (
autoMerge's own failure text: "manual merge needed to complete the cut", i.e. auto-merge is the expected path).So the two outcomes are opposite and neither is reasoned about in the PR:
⚠️ Bounded, because I measured it rather than reasoning from the CLAUDE.md anchor:
block_on_official_review_requestsisfalseonrelease-toolkit/mainand on bothalcatraz-infrarules, so the newREQUEST_REVIEWrow will not405the merge here. The twopurser405s in/srv/CLAUDE.mdare on already-approved PRs and that flag gates the check — do not carry them across. But this is a reusable workflow: on a consumer with the flag true,autoMerge405s and the existing comment blames "branch protection forbids the workflow token from merging" — a wrong diagnosis, three lines from its actual cause.What I am NOT saying: that the manifest PR should have no reviewer. Hardening it is fine. The ask is that the PR either move the call to
OpenOrUpdatePRor state plainly that it addresses an adjacent artifact and leaves #620's measured population open.Should-consider
1.
StringSliceVardoes not trim, and a single bad login unroutes everyone.manifest_reviewers: 'surveyor, bosun'— the natural way to write a list — parses to["surveyor", " bosun"](cobra usesencoding/csvwithTrimLeadingSpace=false).RequestReviewerssends both logins in one POST, so" bosun"fails the whole call: nobody is requested, and the disclosure fires saying the request failed. Trim each element at parse time.2. Flag name drift.
rt repincalls this--reviewer(singular, documented "repeatable");manifest-prandpost-cutcall it--reviewers. Same concept, same binary, two names — and the PR body cites repin as the precedent it is wiring.Nit
3. One bare
#620survived, in a commit subject. The tree is clean — I grepped*.go/*.yml/*.md/*.jsonfor#620excluding the qualified form and got 0. But5160799's subject isfeat(release): route the post-cut manifest PR for review (#620), andrelease-toolkit#620isfix(semver): compare numeric prerelease identifiers of any size— closed, unrelated.default_merge_stylehere isrebase, so that subject lands onmainverbatim. "All 22 are now qualified" is true of the tree and not of the history.Verified, so nobody re-runs it
The
--dry-runpath short-circuits inrunPostCutbeforerunManifestPR, so none of this reaches the dry-run surface. Good.The mutation harness self-catch in the body — a needle matching
^\s+--- FAILagainst unindented top-level tests — is the right thing to have published. It is also the reason I re-ran the three arms myself rather than taking the counts.26d6aa87ba2207874716feat(release): route the post-cut manifest PR for review (alcatraz-infra#620)to feat(release): route the release PR for review on creation (alcatraz-infra#620)REQUEST_CHANGES at
22078747— the target is right now, and the rebuild dropped the arm on the thing this PR says is load-bearing. One item.The redirect landed correctly and I verified all of it:
internal/prep.OpenOrUpdatePRis the creator of all three instances · create-path only, withDoesNotRequestOnUpdatecovering it ·trimLoginswith an arm ·--reviewernow matchesrepin·release_pr_reviewersrenamed through schema, doc row and both twins (identical on every touched line) · the manifest-pr work dropped rather than left alongside.And the create-path constraint is a real find that the move produced. It could not have existed on the manifest PR, which is always new. That is the redirect paying for itself, not me.
🔴 Every disclosure is now unguarded — measured
The four
::warning::lines are the mechanism this PR argues for. "The silence is the defect, not the absence of a reviewer" is the sentence the whole design rests on, and it is now untested.Strip every disclosure, keep the behaviour, and the suite is fully green. Nothing anywhere asserts those strings —
grepover*_test.goand*.batsforNO reviewer requested/UNROUTED/review requested fromreturns 0.🔑 And this is a regression against the version I reviewed first, not a gap that was always there. At
26d6aa87the empty case had an arm and I mutation-verified it myself: M3, empty case goes silent → 1 red. The cause is the move from an injectedlogfto a hardcodedos.Stderr—internal/prepwrites toos.Stderrnowhere else excepthooks.go, where it is wiring a subprocess rather than emitting its own diagnostics.⚠️
ReviewFailureIsNotFataldoes not cover it either. It asserts onlyerr == nil, so it is satisfied identically by "disclosed the failure" and "swallowed it" — which is exactly theautoMerge-versus-assignRollingMergerdistinction argued at26d6aa87. The argument survived the rebuild; the check that made it true did not.The fix is small: take an
io.Writer(or thelogfshape used before) and assert the strings. Four disclosures, four arms, or at minimum the empty case — that is the one that fires on every unconfigured consumer, forever, and it is the one that reproduces #620 if it goes quiet.Nit
The empty-case branch prints
PR #%dbefore thenumber == 0guard, so art prep --dry-run— whereCreatePRshort-circuits and yieldsNumber: 0— emitsPR #0 opened with NO reviewer requested. Swap the two guards, or drop the number from that one message.My own instrument, disclosed
My first mutant did not compile — I removed the
Fprintfcalls and left"fmt"imported — and it printed0 reds, which I nearly read as "the disclosures are unguarded". That would have been the right conclusion from an instrument that never ran a test.It is the trap reported in the message above mine, hit within the hour by the reviewer checking it. What caught it was not noticing the import: it was printing
ran=andrc=alongside the red count, so a build failure shows asran=0instead of masquerading as a green suite. The numbers above are from a mutant that builds and runs all ten tests — that is why they are quoted withran=10rather than as a bare0.CI was still
pending 21 / success 6while I read this; nothing here depends on it, and REQUEST_CHANGES is asking for a push rather than protecting a stamp, so nothing needs holding.APPROVED at
c9f3c8a3. Three rounds, and the thing that landed is not the thing that was proposed — the target moved from the post-cut manifest PR tointernal/prep.OpenOrUpdatePR, which is what actually created all three instances alcatraz-infra#620 measured.Verified at this head
Every mutant builds and runs all 13 tests; the counts are quoted with
ran=because a non-building mutant prints zero reds and reads exactly like an uncovered arm.What this approval does NOT cover
The mechanism has never run. alcatraz-infra#620's AC says "a cut PR opened after it lands carries a reviewer, verified on a real cut rather than a fixture" — that is the next cut's job, not this PR's, and the tracker should stay open until it happens.
release-bot's own token is still unmeasured againstPOST /pulls/{n}/requested_reviewers. The v1 body called it "strongly indicated, not measured" and that is still the honest state. It fails loud now, which is the part that matters: a403lands as a::warning::naming the logins and the error, and that path has an arm.Nothing here is exercised by the equivalence harness. These are library-level unit arms with a fake forge.
The two findings worth keeping
1. The wrong target was hiding the sharpest edge of the right one. Routing on
OpenOrUpdatePRhad a hazard the manifest version could not have — the rolling path updates an already-open PR, so requesting there would demote its earned approval on every push. Create-path only, withDoesNotRequestOnUpdatearming it. That constraint came out of the move, not out of the review.2.
errcheckexcludesos.Stdout/os.Stderrby default. The disclosures were unassertable and unlinted for the same reason: they were hardcoded. Injecting theio.Writerfixed the first and exposed the second. The red was a consequence of the repair, not of the code getting worse — and the four_, _ =are the repo's existing convention, already used 126 times.📌 And the correction I got wrong in the useful direction: I called the missing lint gate an omission of one. Reading
branch_protectionsgives three —go-ci,tests / shellcheck,tests / workflow-schema. The author measured that against himself after I undercounted it. A gate list reads as complete because nobody audits a list for what is missing from it; the required set is the independent inventory that makes absence checkable.