feat(doctor): preflight the two merge-queue governance preconditions ship-it needs (#4303) - #4346
Conversation
…ship-it needs (#4303) ship-it's enqueue passes no merge-method flag because the merge queue owns it (ADR 0132), so it silently depends on two GitHub-side repo settings no repo-scoped script can create: repo-level allow_auto_merge, and a merge_queue rule on the default branch. Nothing preflighted either, so a fresh repo built, reviewed, approved, and enqueued — and the merge never landed. doctor.sh gains two Tier-2 checks. Both resolve the default branch rather than assuming main, and both treat an unreadable response as UNKNOWN with a reason that says the read failed — never as the negative answer (gh api prints its error body to stdout without applying --jq, the #4223 class). Verified live against three states: this repo (both pass), a repo with no queue on a 'trunk' default branch (MISSING PRECONDITION), and a nonexistent repo (READ FAILED). allow_auto_merge turns out to be admin-visible only — absent, not false, for a non-admin token — which is exactly why the pass arm demands a literal true. Each red names which precondition is missing and points at ADR 0132 rather than re-deriving it. The plugin README gains the adopter-facing pointer naming both as repo-admin prerequisites. ship-it's enqueue command is untouched. Fixes #4303 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
No preview deploy
|
|
review-code: advisory — blocking-set PR (manual merge) PR #4346 is §CP — Reviewed-head: @ af23cea Class fan — The central claim — independently re-verified, not taken on reportThe PR rests on this: Confirmed. Exit 0, empty stdout, key genuinely absent from the payload on a non-admin repo; present and The consequence is correctly implemented in the code.
An unreadable read never resolves to a definite answer in either direction. That is the defect class this PR is a live instance of, and it is closed here. Live three-state proof — re-run by me, all three reproduceI executed
The Bonus correctness the AC did not ask for: the branch is percent-encoded ( Acceptance criteria — #4303, one at a time
AC half: 7/7 PASS. Read-discipline checks I was asked to make
Fan-out (ADR 0079) — silent-failure / type-design / test-gap
Shell hygiene and CI
For the control-plane approver — three things to know before you approve1. The disclosed pre-existing defect (#4347) was SEEN, judged, and deliberately left out of scope — I agree with that call. Checks 2b/3b use A confident wrong reason line for a read that actually 404'd — the identical defect class this PR fixes, on a neighbouring check. Leaving it out is correct: it sits on lines this PR does not author, fixing it would widen a §CP diff already under human review, and it is filed as #4347 with its own reproduction. Crucially the new checks do not inherit the pattern — they capture 2. Merge-order collision — real, and the one thing to act on. #4303, #4300, and #4347 all land in 3. Two small residual notes — neither blocks, both yours to weigh.
Bottom line for the approver: the central claim is independently reproduced, the unreadable-read invariant holds in both directions, the default branch is genuinely resolved (proved against a repo whose default branch is |
|
review-skill: advisory — blocking-set PR (manual merge) PR #4346 touches the control plane — Reviewed-head: @ af23cea This is the Skill-rigor checklist (ADR 0073 §1) — all four run
Skill hygiene
Deviation disclosure (§DEV) — cleanThe PR body discloses every departure up front: the issue's "the fix is not code" premise deliberately not followed (triage had already corrected it), no governance state created or mutated, #4300's label gap untouched, the live #4268 acceptance test not run, and the adjacent pre-existing defect found-but-not-fixed. Nothing I found in the diff is undisclosed. For the control-plane approverThe pre-existing defect the author left alone (#4347) — seen, judged, correct to leave. Checks 2b/3b use Sequencing — the one thing to act on. #4303 (this), #4300, and #4347 all land in Verdict: skill-rigor 4/4 PASS, hygiene clean, no gate invariant touched. Combined with the |
What this does
ship-it's enqueue passes no merge-method flag on purpose — the merge queue owns SQUASH (ADR 0132). That makes it silently depend on two GitHub-side repo settings that no in-repo artifact expresses and no repo-scoped stand-up script can create: repo-levelallow_auto_merge, and amerge_queuerule on the default branch. Nothing preflighted either, so a fresh repo could build, review, get an approval, and enqueue — and the merge would never land. That is the finish line, which is exactly where an adopter concludes the product does not work.doctor.shgains two Tier-2 checks (its own charter: gating — a stage fails deep without these), plus the adopter-facing pointer. It still writes nothing: every fix is a command or an admin path it prints.The read discipline — an unreadable state is UNKNOWN, never the negative answer
Both checks distinguish three outcomes, not two: pass, missing-precondition, and read-failed.
gh apiwrites its error body to stdout without applying--jq, so an unguarded capture reads the error as data (the #4223 class). Each read therefore keeps gh's exit status and admits only the literal values it expects — anything else resolves UNKNOWN with a reason line that says the read failed.This is not theoretical here. A live probe turned up a fact worth recording:
allow_auto_mergeis admin-visible only. On a repo the token lacks admin on, GitHub omits the key entirely andgh api --jq '.allow_auto_merge'exits 0 printing nothing — so "absent" is indistinguishable from "false" if you look only at the value. That is precisely why the pass arm demands the literaltrueand the red for that case says could not read, not disabled, and points at needing an admin token.Verified live against all three states
allow_auto_merge✓PASS✓PASS on default branchmaintrunk✗READ FAILED (non-admin token — UNKNOWN, correctly not "disabled")✗MISSING PRECONDITION✗READ FAILED✗READ FAILED (no branch to ask about)The
trunkcase also exercises the no-hardcoded-mainrequirement: the branch is resolved from.default_branch, and a branch containing/is percent-encoded so it stays one path segment.Acceptance criteria
allow_auto_merge, reds whenfalse, prints the exact admin fix as a command (gh api -X PATCH … -F allow_auto_merge=true) and never runs it.repos/{repo}/rules/branches/{branch}for the resolved default branch (never a hardcodedmain) and reds when no applying rule hastype == "merge_queue". Endpoint + rule shape reused from the existing reader inpackages/pipeline-cli/src/tools/merge-intent/github.ts, not re-implemented.allow_auto_merge) instead of re-deriving the why.## Repo-admin prerequisitessection, linked from How to kick it off, thedoctorrow, and the foreign-repo validation section. The "a logged-inghis the only prerequisite" line is corrected to scope that claim to the plugin itself.ship-it/SKILL.mdis untouched — no merge-method flag added. ADR 0132 owns the method through the queue.gh pr merge --autobehavior as verified. No live probe of it was run, and neither check depends on it — both preconditions are independently required whatever error shape a missing queue produces.Deliberately not in scope
doctor.shpreflight is code.doctor.sh, so whichever merges second should rebase.One adjacent defect observed, not fixed here
Probing the unreadable-repo case surfaced the same defect class, pre-existing, in the checks above mine: check
2bdoesWORKFLOWS=$(gh api … --jq '.total_count' 2>/dev/null || echo 0), so on a failed read gh's error body lands on stdout and|| echo 0appends to it — yielding{"message":"Not Found",…}0, a[: integer expression expectedwarning, and then the wrong reason line: "no CI workflows defined" when the truth is that the read failed. Check3b(RUNEV) has the identical shape. That is out of this issue's acceptance criteria and enters through report → triage rather than as drive-by scope here; filed separately.Gates
pnpm typecheck— clean (29/29).pnpm lint:worktree— clean (no biome-handled changed files).pipeline-cli gh-phoenix lint-skillson both changed skill files — clean.bash -n+shellcheckondoctor.sh— clean; the only shellcheck warning (SC2043) is pre-existing in the Tier-3 loop I did not touch.pipeline-cli cp-classify classifyon the changed paths — control-plane [path-match], BLOCKING (human merge).Fixes #4303