merge-pr: handle real-repo merge configs (method, required-checks, approving-review) - #59
Conversation
…proving-review) scripts/merge-pr.sh broke on common real-repo branch settings, all on the happy path. This makes the mechanical merge gate work against them while preserving every existing safety guard. - Merge method: detect allowed methods via `gh repo view --json squashMergeAllowed,mergeCommitAllowed,rebaseMergeAllowed`; prefer --squash, fall back to --merge then --rebase; refuse if none allowed. --match-head-commit SHA-pin works across all three. - CI gate, required vs optional (operator-approved): when the base branch's protection defines required status checks, gate on exactly those required contexts (a required context that is absent/non-pass blocks; optional checks are informational and never block). When no required checks are defined (the protection endpoint 404s), fall back to the legacy gate (>=1 pass, none failing/pending). Guarantee preserved: never merge with a failing required check; never merge with zero passing checks. - Approving-review protection: pre-check `gh pr view --json reviewDecision`; refuse on REVIEW_REQUIRED with a clear message that the comments-only reviewer never approves, so this protection is incompatible with in-session auto-merge (hand to the human merge gate). Additive. - Docs: templates/repo-setup.md states the supported protection shape (required status checks, NOT require approving review) and notes merged-branch cleanup; README Layout + behavior text synced. Preserves: reviewed head/base SHA marker parsing, refuse-if-head/base-moved, --match-head-commit pin, --repo scoping, unset GH_REPO. Stays #!/usr/bin/env bash, set -euo pipefail, shellcheck -S style clean, +x. Closes #55 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codex reviewer (cross-vendor, read-only)Reviewed-head: 462acfe Posted verbatim by The new required-checks gate can mis-detect protected branches whose names require URL encoding and incorrectly apply the legacy all-checks gate. This is a functional regression for common release branch naming patterns. Review comment:
|
The branch-protection lookup interpolated $base_ref raw into the REST path. A slashed base name (release/1.0, feature/x) sent its slashes as path separators, so the lookup 404'd and the script wrongly fell back to the legacy all-checks gate — refusing a protected release PR whose required checks pass but an optional check is pending/failing. URL-encode the branch path component with jq's @uri filter (/ → %2F) before the gh api call. The 404/no-protection fallback and the required-context gating are otherwise unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Round-0 fix (Coder) — [P2] URL-encode base branch in protection lookup Head: The branch-protection lookup interpolated Fix: URL-encode the branch path component with base_ref_enc="$(jq -rn --arg b "$base_ref" '$b | @uri')"and use Everything else from round-0 is preserved: merge-method detection, Local checks green: |
Codex reviewer (cross-vendor, read-only)Reviewed-head: 3c625bf Posted verbatim by The patch weakens the documented base-race guard by adding a network read after the final base verification, and it silently downgrades required-check discovery failures into the legacy fallback. These are correctness issues in the merge safety harness. Full review comments:
|
…ired-checks discovery Round-1 review fixes for PR #59 (issue #55): Fix 1 — keep the base-race guard last: the merge-method detection (gh repo view --json squashMergeAllowed,...) was moved BEFORE the final base-race re-check, so the baseRefOid read is once again the LAST API call before gh pr merge. An extra API round-trip after the base read would let another PR land on the base in the gap and merge onto an unreviewed base. Fix 2 — fail CLOSED when required-checks discovery is indeterminate: the required_status_checks lookup now takes the legacy all-checks fallback ONLY on a confirmed HTTP 404 (branch protection / required checks not configured) or a successful response with an empty required-context set. Any other failure (403/5xx/429/network) refuses with an actionable stderr message naming the HTTP cause, rather than misclassifying a protected base as 'no required checks' (which would let an optional pending/failing check block the merge and lie in the diagnostic). gh api's stderr is captured via mktemp and inspected for 'HTTP 404'. The genuine-404 legacy fallback and the jq @uri branch URL-encoding from round-1 are preserved. Header and inline comments updated to reflect the reordering and the fail-closed semantics. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Round-1 fixes pushed (head
|
Codex reviewer (cross-vendor, read-only)Reviewed-head: 64a73a3 Posted verbatim by The new required-check discovery depends on a branch-protection REST endpoint that many legitimate merge operators cannot read, causing valid green PRs to be refused. This breaks the merge helper in a common protected-repo permission setup. Review comment:
|
…tection
The required-check discovery used `gh api .../protection/required_status_checks`,
which needs Administration / branch-protection READ permission. A non-admin
maintainer who can merge PRs but lacks that permission hit a permission error,
and the round-2 fail-closed path then refused even a green PR — breaking a
common protected-repo maintainer setup.
Replace it with `gh pr checks <pr> --required`, which derives "required" from
the PR's status-check rollup `isRequired` flag — readable by anyone who can view
the PR (no admin permission). Decide from output CONTENT, not exit code:
1. non-empty JSON array -> required checks present: gate on exactly those
(refuse unless every entry's bucket is pass; optional checks ignored).
2. benign "no required checks" message (no array) -> legacy fallback over the
FULL set: require >=1 pass, zero failing/non-skipped, refuse on zero.
3. neither (genuine auth/network error) -> fail closed: refuse with the error.
Removes the now-obsolete `jq @uri` branch URL-encoding (round-1) and the
`gh api .../protection/required_status_checks` call with its 404-vs-error
fail-closed distinction (round-2). Updates the header/inline comments and
templates/repo-setup.md to describe the rollup-based discovery (works for
non-admin maintainers).
Preserves round-2's other fixes: merge-method detection stays before the final
baseRefOid read (the last API call before merge); all `gh pr checks` reads happen
before that base read. reviewDecision refuse, SHA-pin guards, --repo-scoping, and
unset GH_REPO unchanged.
Tested on PR #59 (no required checks): takes the legacy fallback, finds the `ci`
check passing, none failing/pending — proceeds, does not fail closed.
Closes #55
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Round-3 fix — [P2]: required-check discovery no longer needs admin permissionHead: ProblemRequired-check discovery used Fix — discover required checks from the PR's status-check rollupReplaced the protection-REST discovery with Discovery logic (decide from output CONTENT, not exit code —
Each path prints an accurate diagnostic naming which branch ran (gated on N required checks / legacy fallback because no required checks / refused because discovery failed). Removed (now obsolete):
Net: −113 / +94 in Preserved (no regression to round-2's other fix)
TestingRan the new discovery block against PR #59 itself (gh 2.92.0, no required checks configured):
Reasoning for the other two paths:
Local checks (green, under bash)
Round label bumped round-2 → round-3 (the cap). No merge/approve performed — stopping for review. |
Codex reviewer (cross-vendor, read-only)Reviewed-head: 3dca017 Posted verbatim by No discrete, actionable correctness issues were identified in the reviewed diff. The changes preserve the existing safety gates while adding required-check filtering, review-required refusal, and merge-method selection. |
Closes #55
What changed
scripts/merge-pr.shbroke on common real-repo branch settings, all on the happy path. This makes the mechanical merge gate work against them while preserving every existing safety guard.gh repo view --json squashMergeAllowed,mergeCommitAllowed,rebaseMergeAllowed; prefer--squash, fall back to--mergethen--rebase; refuse with an actionable error if none is allowed.--match-head-commitSHA-pin works across all three methods, so the safety guarantee is method-independent.gh api repos/{owner}/{repo}/branches/{branch}/protection/required_status_checks). When required checks are defined, gate on exactly those contexts — a required context that is absent or non-passblocks; any non-required (optional) check is informational and never blocks. When no required checks are defined (endpoint 404s — unprotected / free private repo), fall back to the legacy gate (≥1pass, none failing/pending, refuse on zero checks).gh pr view --json reviewDecision; onREVIEW_REQUIRED, refuse with a clear message that Fabrica's reviewer is comments-only (never approves), so this protection is incompatible with the in-session auto-merge path — hand to the human merge gate.templates/repo-setup.mdnow states the supported protection shape (required status checks, NOT "require approving review") and notes merged-branch cleanup (--delete-branchor the repo's auto-delete setting). README Layout line + behavior paragraph synced.CI-gate semantics change — callout (operator signed off)
This PR changes the CI gate semantics, and the operator has explicitly signed off (per the issue): the gate is now the required status checks when branch protection defines them — a pending/failing optional check (Vercel/Codecov/preview) no longer blocks a genuinely mergeable PR. When no required checks are defined, behavior is unchanged (legacy ≥1-pass / no-fail fallback). Guarantee preserved: never merge with a failing required check; never merge with zero passing checks. Refuse messages still name the offending checks.
Preserved guards (unchanged)
Reviewed head/base SHA marker parsing; refuse-if-head-moved; refuse-if-base-moved;
--match-head-commitpin;--repo-scoping +unset GH_REPO; ≥1-real-CI-pass (in the fallback). Stays#!/usr/bin/env bash,set -euo pipefail, shellcheck-S styleclean, executable. The script still never judges review pass/fail or risk (Faber's call) — it enforces only the mechanical gate.Self-modification note
merge-pr.shis run from disk by Faber's in-session auto-merge flow — this change takes live effect on merge; no/faberre-sync needed (that regeneration is only required whentemplates/faber-command.mdchanges, which it does not here).How verified locally
bash -n scripts/merge-pr.sh→ clean.shellcheck -S style scripts/merge-pr.sh(and allscripts/*.sh) → clean.ci/required-files.txtmanifest, run under bash) →structure ok;merge-pr.shis+x. No manifest changes (only edits to already-listed files).contexts+checks[].context; empty for no-required and 404 cases; a required context absent from reported checks resolves tomissing(blocks); an optional pending check is ignored when not in the required set.New refuse/branch paths (reasoned — no unrelated PR was merged)
reviewDecision=REVIEW_REQUIRED→ refuse: comments-only reviewer never approves; hand to human gate.Out of scope
Deferred should-fix items and the unattended/cross-repo auto-merge (that's #53).