feat(automerge): add risk-bypass via approval label or Codex pass#25
Conversation
Two new bypass paths for the risk-tier gate on Claude-authored PRs. Today the gate forces manual click-merge on anything touching auth, secrets, migrations, billing, or production infra (Dockerfile / CI workflows / IaC / deploy scripts) — these add escape hatches that preserve the trust signal while removing the click-merge round-trip. **Option A — `auto-merge-approved` label.** A new input `risk_bypass_label` (default `auto-merge-approved`). When that label is present on a Claude-authored risky PR, the risk gate is overridden and `gh pr merge --auto --squash` is enabled. Distinct from the existing `auto-merge` label, which is a Claude- authorship DETECTION signal — that one only marks the PR as Claude's, risk gate still runs. Workflow: human sees the "blocked — risk-tier paths touched" comment, applies the label from the PR list page (one click, no PR-detail navigation, no merge-button click). The caller already listens on `pull_request.labeled`, so the workflow re-runs immediately and flips auto-merge on. **Option B — Codex Review SUCCESS.** A new input `codex_check_name` (default `review / Codex Review`). When that status check is SUCCESS on the PR's head SHA, the risk gate is overridden. Reads from the check-runs API first, falls back to the commit-status API for callers that post Codex output as a status rather than a check-run. Bypass triggers naturally on the next workflow run after Codex completes — i.e. on `pull_request.synchronize` (any push) or `pull_request.labeled` (any label change). Fully-automatic re-trigger via `workflow_run` was scoped out for now (workflow_run events come without `pull_request` payload, requiring a separate PR- lookup job that wouldn't fit cleanly into the existing reusable shape — follow-up for v1.1). The "blocked" comment is updated to mention both bypass options so the human sees the path forward inline. Marker string unchanged so existing comment-update logic continues to find and edit prior comments rather than spamming new ones. Lint clean: `actionlint` passes, no shellcheck issues. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
| --jq ".check_runs[] | select(.name == \"$CHECK_NAME\") | .conclusion" | head -1) | ||
| if [ -z "$codex_state" ]; then | ||
| codex_state=$(gh api "/repos/${{ github.repository }}/commits/$HEAD_SHA/status" \ | ||
| --jq ".statuses[] | select(.context == \"$CHECK_NAME\") | .state" | head -1) |
There was a problem hiding this comment.
jq injection / silent bypass failure: $CHECK_NAME is shell-interpolated directly into the --jq filter string on both API calls. If the check name ever contains a " (or other jq syntax), jq exits with an error, codex_state is empty, and the bypass silently falls back to bypass=0 — the risk gate stays up with no indication of why.
The safe pattern uses standalone jq --arg instead of gh api --jq:
| --jq ".check_runs[] | select(.name == \"$CHECK_NAME\") | .conclusion" | head -1) | |
| if [ -z "$codex_state" ]; then | |
| codex_state=$(gh api "/repos/${{ github.repository }}/commits/$HEAD_SHA/status" \ | |
| --jq ".statuses[] | select(.context == \"$CHECK_NAME\") | .state" | head -1) | |
| codex_state=$(gh api "/repos/${{ github.repository }}/commits/$HEAD_SHA/check-runs" \ | |
| | jq -r --arg name "$CHECK_NAME" '.check_runs[] | select(.name == $name) | .conclusion' | head -1) | |
| if [ -z "$codex_state" ]; then | |
| codex_state=$(gh api "/repos/${{ github.repository }}/commits/$HEAD_SHA/status" \ | |
| | jq -r --arg name "$CHECK_NAME" '.statuses[] | select(.context == $name) | .state' | head -1) |
The default value "review / Codex Review" has no special chars so this won't trigger today, but any caller that customises codex_check_name to something with a " will get a silent no-op.
|
Flagged 1 issue inline — jq shell-interpolation of |
Caught by Claude Review on this PR. The previous gh-api-with-inline-jq form interpolated $CHECK_NAME into the jq filter via shell expansion: gh api /commits/$SHA/check-runs --jq ".check_runs[] | select(.name == \"$CHECK_NAME\") ..." If CHECK_NAME ever contained a double-quote (custom callers, future variants), the shell interpolation would produce a broken jq filter that silently returned nothing — bypass=0 with no warning. Default "review / Codex Review" works in practice but the failure mode is exactly the kind of "silent fall-through" the Webcat 2026-04-27 incident wrote up as a category to avoid. Fix: pipe gh api through `jq --arg name "$CHECK_NAME"` to bind the variable safely regardless of contents. Same shape applied to both the check-runs query and the commit-status fallback. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
No blocking issues found. One notable limitation: the check-runs API call omits |
…rrors (#27) Two follow-ups to [#25](#25 Option B Codex bypass. Both surfaced when the new reusable ran for the first time on [whois-api-llc/wxa-mcp-server#80](whois-api-llc/wxa-mcp-server#80) — the auto-merge install PR for that repo. Output: ``` gh: Resource not accessible by integration (HTTP 403) jq: error (at <stdin>:0): Cannot iterate over null (null) ##[error]Process completed with exit code 5. ``` **Fix 1 — `checks: read` permission.** The Option B step queries `/repos/{repo}/commits/{sha}/check-runs` to read Codex Review status. The default `GITHUB_TOKEN` on `pull_request` events does NOT carry that permission by default, so the call returns 403 → jq fails on null → workflow crashes. **Fix 2 — crash hardening.** Even with the right permission, transient issues happen (404 on un-propagated commits, network blips, future API contract changes). Wrap each `gh api` call in `|| echo '{}'` and use `jq 'try (...) // empty'` so the step ALWAYS sets `bypass=0` when the API returns invalid JSON. The bypass remains strictly additive: if it can't prove Codex passed, the existing 'blocked comment' path still runs. Today's behaviour was a hard crash that ALSO skipped the blocked comment, leaving the user with a red 'automerge / automerge' check and no helpful message. After this fix: API failures degrade to 'no bypass, post the blocked comment as usual'. ## Test plan - [x] `actionlint` clean. - [x] Verified the failing log on [whois-api-llc/wxa-mcp-server#80 run 25292588477](https://github.com/whois-api-llc/wxa-mcp-server/actions/runs/25292588477) shows the exact 403 from the missing permission. - [ ] Once this lands, re-run the wxa-mcp-server#80 workflow (push fixup or label-trigger) and confirm `automerge / automerge` succeeds with bypass=0 (no Codex on this repo's pre-existing PRs since they're already merged) and the existing blocked-comment posts. **Auto-merge rationale:** **NOT eligible** — touches `.github/workflows/**`. Manual click-merge. **Codex pre-review:** skipped — straightforward fix; the diff is small enough that Codex would add latency without value. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Summary
Adds two bypass paths to the risk-tier gate in `claude-author-automerge.yml`. Today the gate forces manual click-merge on every Claude-authored PR that touches auth / secrets / migrations / billing / Dockerfile / `.github/workflows/**` / IaC / deploy scripts. These additions preserve the gate's trust signal while removing the click-merge round-trip in two well-defined cases.
Option A — `auto-merge-approved` label
New input `risk_bypass_label` (default `auto-merge-approved`). When that label is present on a Claude-authored risky PR, the risk check is overridden and `gh pr merge --auto --squash` fires.
This is a NEW label distinct from the existing `auto-merge` label. The existing one is a Claude-authorship DETECTION signal (marks the PR as Claude's, risk gate still runs). The new one is an explicit "I read this and approve" signal.
Workflow: human sees the existing "blocked — risk-tier paths touched" comment, applies the label from the PR list page (one click, no PR-detail navigation, no merge-button click). The caller already listens on `pull_request.labeled`, so the re-run happens immediately.
Option B — Codex Review SUCCESS
New input `codex_check_name` (default `review / Codex Review`). When that status check is SUCCESS on the PR's head SHA, the risk gate is overridden.
Reads from the check-runs API first, falls back to the commit-status API for callers that post Codex output as a status rather than a check-run.
Re-trigger semantics: the bypass fires on the next `pull_request.synchronize` (any push) or `pull_request.labeled` (any label change) AFTER Codex completes. Fully-automatic `workflow_run` re-trigger was scoped out for v1 because `workflow_run` events come without `pull_request` payload, requiring a separate PR-lookup job that doesn't fit cleanly into the existing reusable. Worth doing in a v1.1 follow-up if anyone hits the limitation in practice.
Updated "blocked" comment
The comment that gets posted when the gate fires now mentions both bypass options inline, so the human sees the path forward without leaving the PR. Marker string `` unchanged — existing comment-update logic continues to edit-in-place rather than spam.
Test plan
Backward compatibility
Auto-merge rationale: NOT eligible — touches `.github/workflows/**` (production infrastructure surface in the fleet policy). This PR will dogfood its own "blocked" comment until merged. Manual click-merge required (or apply the new label after this lands and an updated install-policy script picks up the new defaults).
Codex pre-review: skipped — change is mechanical workflow plumbing. Mirrors the existing risk-check / enable-auto-merge gate shape; no new business logic. (Once this lands, the next Claude PR on a Codex-installed repo will exercise Option B end-to-end.)
🤖 Generated with Claude Code