Repository navigation
feat(guardrails): block a PR merge while review threads, change requests or failing checks remain - #7056
Draft
kyle-sexton wants to merge 3 commits into
Draft
feat(guardrails): block a PR merge while review threads, change requests or failing checks remain#7056kyle-sexton wants to merge 3 commits into
kyle-sexton wants to merge 3 commits into
Conversation
…sts or failing checks remain Add the block-unresolved-pr-merge guard (#6463). It matches every common merge path: gh pr merge (--admin and --auto included), a PUT to the REST merge route or the Claude Code cloud ccr/auto_merge route through gh api or curl, the mergePullRequest, enablePullRequestAutoMerge and enqueuePullRequest GraphQL mutations, and an MCP merge_pull_request or enable_pr_auto_merge tool under any server prefix. PR state is read over REST so the guard works where GraphQL is refused; review-thread resolution comes from GraphQL where served, else the cloud ccr/review_threads route. Pending checks block an immediate merge only, since auto-merge and the merge queue wait for checks by design. The guard fails open with a notice on both channels when gh is missing or a read fails. Off switch: block_unresolved_pr_merge_enabled. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L8KdG7fWg2YrErVYWtohmR
Contributor
Author
|
The lanes run once the PR is marked ready. That flip waits for Kyle, because it also starts the review lanes. Until then, the local runs listed under Verification in the PR body are the signal. Generated by Claude Code |
… and prefix gaps From review of #7056: - A GraphQL merge, auto-merge or merge-queue mutation whose PR cannot be resolved (id from $ID, "$PR_ID", $(jq ...), an unreadable node, no gh) now fails closed, with the literal-id fix in the message. Tests cover the source-control 4.2.2 enqueue shape from #7075. - gh pr merge --help and -h are not merges; stay-quiet tests assert no gh call. - All reads share one 35 s budget, each call capped at what is left and at 15 s, enforced by timeout or, where it is missing, a watcher process, so the shared row stays under its 60 s timeout. - Leading reserved words (then, do, {, ...) and the command, time, exec and nohup prefixes are stepped over, and gh pr -R o/r merge is read. - The fail-open notice says the merge will run unchecked. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L8KdG7fWg2YrErVYWtohmR
GUARDRAILS_BUPM_BUDGET_S let the environment shorten the guard's 35 s read budget. settings.json `env` reaches a hook's environment, so a project setting could make every merge run unchecked. The budget is now a constant; the budget tests run a copy of the hook with that one line rewritten, and a new case proves an env var cannot shorten it (fails before this change). Refs: #6463 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L8KdG7fWg2YrErVYWtohmR
60 tasks
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by Kyle · project thread
Closes #6463
Summary
Before: nothing checked a direct
gh pr merge, REST or GraphQL merge, or MCP merge call. An agent could merge (or ask for--admin) without reading an unresolved review thread.After: a new guardrails guard,
block-unresolved-pr-merge, blocks a merge while the PR has an unresolved review thread, a reviewer whose latest decisive review requests changes, or a failed check run or commit status on its head. The block message lists each thread (path:line, first line of the comment, link), each changes-requested reviewer and each failing check, and names the fix.--adminand--autoare held to the same rule.Fix
plugins/guardrails/hooks/block-unresolved-pr-merge.sh, on the existing Bash|PowerShellrun-guardsrow plus a new MCP row (^mcp__.+__(merge_pull_request|enable_pr_auto_merge)$). Matches the scope update's table:gh pr merge(all flags),PUT repos/{o}/{r}/pulls/{n}/mergeviagh apiorcurl, themergePullRequest/enablePullRequestAutoMerge/enqueuePullRequestmutations viagh api graphqlorcurl, and the MCP merge tools. Matching runs on parsed argv at a command position, so a quoted mention (commit message, echo, heredoc) stays quiet. Commands that name neithermergenorenqueueleave after one string test.pulls/{n},pulls/{n}/reviews,commits/{sha}/check-runs,commits/{sha}/status. Thread resolution has no plain REST route: GraphQLreviewThreadswhere served, else the cloud routeGET pulls/{n}/ccr/review_threadsnamed in that 403 body (aCLAUDE_CODE_REMOTEsession tries the cloud route first). The same 403 body namesPUT pulls/{n}/ccr/auto_mergeas the cloud auto-merge path, so that route is matched too.ghis missing or unauthenticated, a read fails, the read budget runs out, or the PR cannot be identified.$ID,"$PR_ID",$(jq ...)(the source-control merge reference 4.2.2 enqueue shape from fix(source-control): confirm a merge-queue PR entered the queue after gh pr merge #7075), a node that can't be read, or no gh. The block message says how to fix it: pass a literal-f id=PR_...or usegh pr merge <n>.timeoutwhere it exists, and by a watcher process where it doesn't (stock macOS). No environment variable changes the budget:settings.jsonenvreaches a hook's environment, so a knob could make every merge run unchecked. The budget tests run a copy of the hook with its one budget line rewritten.then,do,{, ...), and thecommand/time [-p]/exec/nohupprefixes. It readsgh pr -R o/r merge.gh pr merge --help/-hand--disable-autoare not merges. The guard header lists the remaining gaps: wrappers likesudo/env/xargs, child shells,$(...), and--inputmutation files.guard-requires.sh(MCP inputs declared unprimed),hooks.json,plugin.jsonuserConfigblock_unresolved_pr_merge_enabled(default on), README guard/option tables, changelog fragment (minor).Defaults I picked where the issue left a detail open (please confirm):
gh pr readyis not matched. The 2026-10-06 scope update dropped the advisory and its table lists merge paths only; the issue title still says "and gh pr ready". Adding it is one line if wanted.--auto,enablePullRequestAutoMerge,enqueuePullRequest,enable_pr_auto_merge,ccr/auto_merge), since those wait for checks by design. Failed checks, threads and change requests block all paths.Open for Kyle (found by the overnight verifier, not changed here):
5. The fail-closed GraphQL rule also covers gh missing or unauthenticated. A mutation with a literal
PR_...id whose node can't be read is blocked. That covers no gh, a 401, and the cloud GraphQL 403. The issue's acceptance says the guard fails open when gh is missing or unauthenticated. Option A: keep it, since the mutation can't be checked. Option B: fail closed only for a non-literal id and fail open, with the notice, for a literal one.6. Interaction with #7075. Its source-control merge reference 4.2.2 passes the enqueue id through
$(jq ...). With this guard on, that step is always blocked, even on a clean PR (verified against branchfix/6630-merge-confirm-queue-entry). One side must change before both merge: #7075 passes a literal id or usesgh pr merge, or this guard relaxes the rule.Verification
bash plugins/guardrails/hooks/block-unresolved-pr-merge.test.sh-> 162 passed, 0 failed (at 96f4c12). Since the first push, the suite also covers:$ID,"$PR_ID",--input, the full fix(source-control): confirm a merge-queue PR entered the queue after gh pr merge #7075 shape, an unreadable literal node)--help/-hstay-quiet cases that assert no gh callgh pr -R o/r mergetimeoutbinary on PATH (returns in under 8 s), run on a copy with a 2 s budgetGUARDRAILS_BUPM_BUDGET_S=1with a 0.7 s gh still blocks). It failed before the knob was removed.A fake
ghon PATH serves fixtures and logs calls. Covers every merge path, both shells, MCP, GraphQL-refused fallback to the cloud route, current-branch lookup, the fail-open cases (gh missing, 401, no thread source, no node id), and MUST-stay-quiet cases that also assert noghcall (commit message, echo,gh pr view,--disable-auto, GET of the merge route, a GraphQL query, grep,git merge, heredoc, PowerShell quoted mention, an unrelated MCP tool). Also runs throughrun-guards.sh.scripts/affected-tests.sh --base origin/main --run:run-guards.test.sh's declaration checks for the new guard. The 2 selected.mjssuites, run separately withnode --test, also passed.check-killswitch-hoist28/28,check-script-contract51/51,node-notice-rows6/6,check-guardrails-ps-differential21/21.block-hook-bypass, which fails identically onorigin/mainwhen the checkout sits under/tmp. A full affected run on this head is still to do.shellcheck -x,check-killswitch-hoist,check-changelog-fragments --check-required origin/mainandcheck-changelog-parity --checkpass.Live, read-only, in a Claude Code cloud session: the guard against open PRs fix(planning): word interview questions so yes accepts the recommendation #7036 (blocked: failing
ci-status,ci-lanes), chore(release): release 27 plugins #6910 (blocked: pending checks), feat: adopt the 2,000-PRs talk decisions across the marketplace #7009 (allowed); 3.0 to 3.6 s per merge command (five sequential calls), recorded in the guard header and README.shellcheck -x(repo.shellcheckrc) clean;scripts/validate-plugins.sh,scripts/check-changelog-fragments.sh --check-required origin/main,scripts/check-changelog-parity.sh --check,scripts/check-killswitch-hoist.sh, markdownlint on the README and fragment: all pass.This changes what agents may do unattended (it blocks merges), so it needs a human merge decision.
Related
/source-control:babysit-prsmerge gate counts threads the same way (outdated included)🤖 Generated with Claude Code
https://claude.ai/code/session_01L8KdG7fWg2YrErVYWtohmR
Generated by Claude Code
Generated by Claude Code