Skip to content

fix(ci): claude-review on pull_request_target + orchestrator waits - #109

Merged
EricAndrechek merged 1 commit into
mainfrom
claude-review-simplify
May 12, 2026
Merged

fix(ci): claude-review on pull_request_target + orchestrator waits#109
EricAndrechek merged 1 commit into
mainfrom
claude-review-simplify

Conversation

@EricAndrechek

Copy link
Copy Markdown
Member

Summary

Replaces the workflow_run-triggered Claude review with a pull_request_target-triggered one, switches the trust gate from "PR author association" to "HEAD commit author/committer permission", and makes the orchestrator wait for Claude review before assigning a reviewer.

Closes the chain of regressions we've been working through (#105, #108) and gives the PR a visible Claude review check.

What was broken

The previous design — trigger Claude review on workflow_run after CI succeeds — kept producing new failures because workflow_run runs with a different token context and the check doesn't attach to the PR:

Symptom Root cause
Unknown JSON field: authorAssociation Not a valid gh pr view --json field. Patched in #105.
Resource not accessible by integration (GraphQL statusCheckRollup) GraphQL navigates checkSuite.workflowRun across every context; the workflow's GITHUB_TOKEN can't read that on PRs with other integrations' checks. Patched in #108.
author_association: NONE for jfwoods The integration GITHUB_TOKEN collapses private org membership to NONE on REST. No good workaround at that field.
No Claude review indicator on the PR workflow_run-triggered checks attach to the source workflow run, not the PR head commit.
Reviewer assigned before Claude review finished Orchestrator was triggered by CI completion in parallel with Claude review, not after it.

What this PR does

claude-review.yml: rebuild around pull_request_target

  • Triggers on pull_request_target (open/sync/reopen/ready_for_review) plus the existing issue_comment and workflow_dispatch paths.
  • Job renamed ReviewClaude review — that's the check name that appears on the PR and that the orchestrator looks for. Keep them in sync.
  • Workflow-level if: filters Dependabot at the cheap level. All other gating moves inside the job.
  • Trust gate is now: HEAD commit's author OR committer must have ≥ read permission on the repo. Implementation:
    • gh api repos/$REPO/commits/$head_sha → pull author.login, committer.login.
    • For each candidate (skipping web-flow and dependabot[bot]), gh api repos/$REPO/collaborators/$candidate/permission.
    • First candidate whose permission is admin/maintain/write/triage/read wins.
  • This means: if Jack opens a fork PR and I push a fixup, the next HEAD is mine → committer = me, admin → gate passes. If Jack pushes again with no admin involvement → both author and committer = Jack, no perm → gate skips. The check still posts success so the orchestrator can move on (drive-by PRs still get a human reviewer assigned, just without AI auto-review).
  • Required-check snapshot (the part that fetched check-runs and gated on CI success) is gone. Claude can review red CI; it can read the failing diff and say so. The merge gate is still the branch ruleset, not this workflow.
  • ~80 lines of gate scaffolding deleted overall.

project-orchestrator.yml: wait for Claude review

Claude review is not added to the branch ruleset's required_status_checks — it's advisory. The orchestrator just waits for it locally.

Behavioural changes worth flagging

  • Drafts are now reviewed. Previously the gate skipped drafts; the new gate doesn't filter on draft state. The orchestrator's existing draft → ready auto-flip still works (gated on bot-clean, which now includes Claude). Easy to add back a draft-filter inside the gate step if it turns out to be noisy.
  • Claude reviews red CI. It can read the failing diff and comment on it. Saves the "wait for CI, then review" round-trip; costs a few API calls on PRs that get force-pushed before CI finishes.
  • Drive-by fork PRs skip Claude but the check still appears. Conclusion is success with a notice in the step log explaining the skip. The orchestrator treats this as "Claude weighed in" and proceeds to assign a human reviewer.

Test plan

  • Internal PR pushed by a private-org member: Claude review check appears on the PR; orchestrator waits for it before assigning.
  • Fork PR by a drive-by contributor: Claude review posts success (skipped); orchestrator still assigns a reviewer.
  • Admin commits a fixup onto a fork PR: next push re-runs Claude review against the new HEAD with trust via committer.
  • @claude / /review re-trigger via comment still works.
  • Dependabot PR: filtered at workflow-level if:, no Claude check on PR, dependabot-automerge path handles it as before.
  • PR feat(mq)!: thread context through Ack/Nak with synchronous DoubleAck #88-style cross-integration check list: orchestrator's bot-clean no longer hits GraphQL Resource-not-accessible.

🤖 Generated with Claude Code

… orchestrator

claude-review.yml previously triggered on workflow_run of CI. That
choice forced a chain that kept producing regressions:

  - `authorAssociation` isn't valid on `gh pr view --json` (fixed in
    #105 with a REST fallback)
  - `statusCheckRollup` blows up on PRs with cross-integration checks
    (fixed in #108 by switching that lookup to REST)
  - REST `author_association` returns `NONE` for private org members
    when called by the integration GITHUB_TOKEN, so private members
    were always being treated as untrusted
  - workflow_run-triggered checks don't attach to the PR head commit,
    so the PR had no visible "Claude is running" indicator
  - orchestrator's bot-clean fires in parallel with Claude review
    (both triggered by CI completion), so reviewers got pinged before
    Claude's feedback arrived

Replace the workflow_run trigger with pull_request_target (+ keep the
existing issue_comment and workflow_dispatch paths). With
pull_request_target:

  - The job appears in the PR's Checks panel as "Claude review".
  - The webhook payload carries the PR number and head SHA directly
    (no `gh pr view` round-trip on this path).
  - Secrets and write tokens are available (Anthropic action requires
    CLAUDE_CODE_OAUTH_TOKEN; `pull_request` would deny it on fork PRs).
  - The job runs immediately on PR open/sync, independent of CI.

Trust gate moves from "PR author's REST author_association" to "HEAD
commit's author or committer has at least read permission on the
repo". This:

  - Fixes the private-member NONE issue — the
    collaborator/permission endpoint is accessible to the integration
    token and returns the user's actual permission.
  - Honours the "I pushed a fixup onto someone else's PR, trust the
    new HEAD" intuition: the committer field reflects who applied
    the commit, so an admin's fixup on a fork PR flips the gate
    from skip to proceed.
  - Treats GitHub Web UI commits correctly (web-flow as committer,
    real user as author → trust via author).

The gate exits 0 with skip=true when neither candidate is trusted,
so the job still concludes success and the "Claude review" check
appears on the PR — which lets the orchestrator's bot-clean wait
work cleanly for PRs that legitimately shouldn't be auto-reviewed.

Orchestrator changes:

  - workflow_run.workflows: [CI] → [CI, "Claude PR review"]
    Re-fires the orchestrator when Claude completes, so a CI-first
    bot-clean (Claude still pending → false) is followed by a
    Claude-completion bot-clean (all green → assign reviewer).
  - Bot-clean's required-checks list gains "Claude review".
  - Bot-clean switches from `gh pr view --json statusCheckRollup` to
    REST `check-runs`, pre-empting the same cross-integration GraphQL
    failure that #108 fixed in claude-review.yml. REST enum values
    are lowercase.

Claude review is *not* added to the branch ruleset's required checks
— it's advisory. The orchestrator just waits for it locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@gemini-code-assist

Copy link
Copy Markdown

Note

Gemini is unable to generate a summary for this pull request due to the file types involved not being currently supported.

@github-actions github-actions Bot added github_actions Pull requests that update GitHub Actions code area/infra CI, build, deploy, Docker, release labels May 12, 2026
@EricAndrechek EricAndrechek changed the title fix(ci): simplify claude-review to pull_request_target + chain orchestrator after it fix(ci): claude-review on pull_request_target + orchestrator waits May 12, 2026
@EricAndrechek
EricAndrechek merged commit c02c0fc into main May 12, 2026
5 of 7 checks passed
@EricAndrechek
EricAndrechek deleted the claude-review-simplify branch May 12, 2026 12:41
EricAndrechek added a commit that referenced this pull request May 12, 2026
## Summary

Follow-up to #109. The Anthropic action's OIDC token exchange rejects
`pull_request_target`'s OIDC claims and we hit `App token exchange
failed: 401 Unauthorized - Invalid OIDC token` on the first run. This is
upstream issue
[anthropics/claude-code-action#713](anthropics/claude-code-action#713)
(open). Their docs (`docs/solutions.md` > "Automatic PR Code Review")
use `pull_request`, not `pull_request_target`. Switching to that.

## What changes

- Trigger: `pull_request_target` → `pull_request`. Same event types.
- `if:` and the in-script case dispatch use `pull_request` instead of
`pull_request_target`.
- Checkout comment updated: `pull_request` defaults to the PR's *merge*
ref; we keep explicitly pinning to PR HEAD so the action sees the
commit, not a merge artefact.

## Trade-off (worth knowing)

`pull_request` runs from a fork PR with a **read-only** GITHUB_TOKEN and
**no access to repo secrets**. Practical impact:

- Internal PRs (the dominant case on this repo) — fine. Secrets
accessible.
- Fork PRs from drive-by contributors — trust gate still skips them
cleanly, conclusion=success, orchestrator's bot-clean isn't disrupted.
- Fork PRs where an admin pushes a fixup commit — HEAD-committer trust
gate passes, but the action then fails to authenticate because
`CLAUDE_CODE_OAUTH_TOKEN` is empty in fork-PR contexts. This case wasn't
supported before either (workflow_run was the prior trigger and fork PRs
don't populate `workflow_run.pull_requests`), so this is a wash, not a
regression.

## Why not solve fork support some other way

The upstream pattern for fork-PR review is a two-workflow split: a
`pull_request` workflow that posts a "pending" status, and a
`workflow_run`/`pull_request_target` follow-up that runs the review with
elevated tokens. That's exactly the chain we just removed in #109
because it kept breaking. Not worth re-introducing for a case we don't
actually have.

## Test plan

- [ ] Next push to PR #7 (or any internal PR): `Claude review` check
appears and completes successfully.
- [ ] Orchestrator waits for the Claude check before assigning.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EricAndrechek added a commit that referenced this pull request May 12, 2026
## Two related noise sources

Both produce red workflow runs on PR pushes that don't reflect actual
problems and were flooding the inbox.

### 1. `claude-review.yml` failing on PRs that edit Claude's own files

The Anthropic action runs a self-validation step that fails when its own
workflow file (`.github/workflows/claude-review.yml`) or the prompt
template (`.github/prompts/pr-review.md`) is in the PR's diff. Every PR
touching Claude's wiring (#105, #108, #109, #110, #111, #113, the merge
commit of #115) has logged a noisy red Claude check that resolves itself
once the change merges.

**Fix:** in `claude-review.yml`'s gate step, query the PR's changed
files; if either path is in the diff, set `skip=true` and exit 0. The
check shows success, the action never runs, the change still takes
effect on the next PR's review.

### 2. `dependabot-automerge.yml push failure` ghost runs on every
branch push

#115 added `reviewers: ${{ replace(env.ADMINS, ',', ' ') }}` to convert
the comma-separated `ADMINS` env var to the space-separated form the
composite expects. Problem: `replace()` isn't a GitHub Actions
expression function. The valid list is `contains`, `startsWith`,
`endsWith`, `format`, `join`, `toJSON`, `fromJSON`, `hashFiles`, plus
status checks. An unknown function fails workflow validation, and GitHub
records a failed run with the file path (not the workflow's `name:`) as
the display name and no jobs — every push to any branch since #115
merged.

**Fix:** replace the bad expression with a real bash step that uses
parameter expansion (`${ADMINS//,/ }`) to write `ADMINS_SPACE` to
`$GITHUB_ENV`. The composite's `with:` then references
`env.ADMINS_SPACE`. Both steps gated on `update-type ==
version-update:semver-major` so they only run when actually needed.

## Test plan

- [ ] This PR's own Claude check: success (skip-gate catches the
self-modification).
- [ ] After merge, branch pushes no longer trigger the ghost
`.github/workflows/dependabot-automerge.yml push failure` runs.
- [ ] Next major-version Dependabot PR: both admins assigned correctly
via `ADMINS_SPACE`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/infra CI, build, deploy, Docker, release github_actions Pull requests that update GitHub Actions code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant