diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml deleted file mode 100644 index e5642cb3e..000000000 --- a/.github/workflows/claude-code-review.yml +++ /dev/null @@ -1,96 +0,0 @@ -name: Claude Code Review - -# Uses pull_request_target so the workflow runs as defined on the default branch, -# which allows access to secrets for posting review comments on fork PRs. -# SECURITY: The checkout pins the fork's HEAD SHA (not the branch name) to -# prevent TOCTOU races (force-push between trigger and checkout). The -# claude-code-action sandboxes execution — it does NOT run arbitrary code -# from the checked-out source. - -on: - # Trigger only when explicitly requested: - # - Add the "claude-review" label to a PR, OR - # - Comment "@claude" or "/review" on a PR - pull_request_target: - types: [labeled] - issue_comment: - types: [created] - -# Concurrency convention: see CONTRIBUTING.md → "GitHub Actions — Concurrency Convention". -# Serialize per-PR to avoid racing review comments. -concurrency: - group: ${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number }} - cancel-in-progress: false - -jobs: - claude-review: - # Run only when: - # 1. The "claude-review" label is added to a non-draft PR by a trusted contributor, OR - # 2. A trusted contributor comments "@claude" or "/review" on a PR - if: | - ( - github.event_name == 'pull_request_target' && - github.event.label.name == 'claude-review' && - github.event.pull_request.draft == false && - (github.event.pull_request.author_association == 'OWNER' || - github.event.pull_request.author_association == 'MEMBER' || - github.event.pull_request.author_association == 'COLLABORATOR') - ) || - ( - github.event_name == 'issue_comment' && - github.event.issue.pull_request && - (contains(github.event.comment.body, '@claude') || - contains(github.event.comment.body, '/review')) && - (github.event.comment.author_association == 'OWNER' || - github.event.comment.author_association == 'MEMBER' || - github.event.comment.author_association == 'COLLABORATOR') - ) - runs-on: ubuntu-latest - timeout-minutes: 30 - permissions: - contents: read - pull-requests: write - issues: read - id-token: write - - steps: - # For issue_comment triggers, resolve the PR number, head SHA, and fork repo - - name: Resolve PR context - id: pr - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v7 - with: - script: | - let pr; - if (context.eventName === 'issue_comment') { - const resp = await github.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: context.payload.issue.number, - }); - pr = resp.data; - } else { - pr = context.payload.pull_request; - } - core.setOutput('number', pr.number); - core.setOutput('sha', pr.head.sha); - core.setOutput('repo', pr.head.repo.full_name); - core.setOutput('branch', pr.head.ref); - - - name: Checkout PR head - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - repository: ${{ steps.pr.outputs.repo }} - ref: ${{ steps.pr.outputs.sha }} - fetch-depth: 1 - - - name: Run Claude Code Review - id: claude-review - uses: anthropics/claude-code-action@9469d113c6afd29550c402740f22d1a97dd1209b # v1 - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - github_token: ${{ secrets.GITHUB_TOKEN }} - allowed_non_write_users: '*' - show_full_output: true - plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' - plugins: 'code-review@claude-code-plugins' - prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ steps.pr.outputs.number }}' diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 553d3ab0d..cfba3ecbc 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -1,8 +1,17 @@ name: Claude Code +# Label-triggered code-review requests use pull_request_target so the workflow +# runs as defined on the default branch, which allows access to secrets for +# posting review comments on fork PRs. SECURITY: PR checkouts pin the fork's +# HEAD SHA (not the branch name) to prevent TOCTOU races. +# The claude-code-action sandboxes execution; it does not run arbitrary code +# from the checked-out source. + on: issue_comment: types: [created] + pull_request_target: + types: [labeled] pull_request_review_comment: types: [created] issues: @@ -21,7 +30,10 @@ jobs: if: | ( github.event_name == 'issue_comment' && - contains(github.event.comment.body, '@claude') && + ( + contains(github.event.comment.body, '@claude') || + (github.event.issue.pull_request && contains(github.event.comment.body, '/review')) + ) && (github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'COLLABORATOR') @@ -46,6 +58,14 @@ jobs: (github.event.issue.author_association == 'OWNER' || github.event.issue.author_association == 'MEMBER' || github.event.issue.author_association == 'COLLABORATOR') + ) || + ( + github.event_name == 'pull_request_target' && + github.event.label.name == 'claude-review' && + github.event.pull_request.draft == false && + (github.event.pull_request.author_association == 'OWNER' || + github.event.pull_request.author_association == 'MEMBER' || + github.event.pull_request.author_association == 'COLLABORATOR') ) runs-on: ubuntu-latest timeout-minutes: 30 @@ -63,33 +83,48 @@ jobs: with: script: | // Determine if this event is PR-related - let prNumber = null; + let pr = null; if (context.eventName === 'issue_comment' && context.payload.issue.pull_request) { - prNumber = context.payload.issue.number; + const resp = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.issue.number, + }); + pr = resp.data; } else if (context.eventName === 'pull_request_review_comment') { - prNumber = context.payload.pull_request.number; + pr = context.payload.pull_request; } else if (context.eventName === 'pull_request_review') { - prNumber = context.payload.pull_request.number; + pr = context.payload.pull_request; + } else if (context.eventName === 'pull_request_target') { + pr = context.payload.pull_request; } - if (!prNumber) { + if (!pr) { core.setOutput('is_pr', 'false'); return; } - const resp = await github.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: prNumber, - }); - const pr = resp.data; - core.setOutput('is_pr', 'true'); - core.setOutput('number', String(prNumber)); + core.setOutput('number', String(pr.number)); core.setOutput('sha', pr.head.sha); core.setOutput('repo', pr.head.repo.full_name); core.setOutput('branch', pr.head.ref); + - name: Resolve Claude mode + id: mode + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v7 + with: + script: | + const body = (context.payload.comment?.body ?? '').toLowerCase(); + const isCodeReview = + (context.eventName === 'pull_request_target' && + context.payload.label?.name === 'claude-review') || + (context.eventName === 'issue_comment' && + Boolean(context.payload.issue?.pull_request) && + body.includes('/review')); + + core.setOutput('code_review', isCodeReview ? 'true' : 'false'); + - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -98,6 +133,7 @@ jobs: fetch-depth: 1 - name: Run Claude Code + if: steps.mode.outputs.code_review != 'true' id: claude uses: anthropics/claude-code-action@9469d113c6afd29550c402740f22d1a97dd1209b # v1 with: @@ -109,3 +145,16 @@ jobs: # This is an optional setting that allows Claude to read CI results on PRs additional_permissions: | actions: read + + - name: Run Claude Code Review + if: steps.mode.outputs.code_review == 'true' + id: claude-review + uses: anthropics/claude-code-action@9469d113c6afd29550c402740f22d1a97dd1209b # v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + github_token: ${{ secrets.GITHUB_TOKEN }} + allowed_non_write_users: '*' + show_full_output: true + plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' + plugins: 'code-review@claude-code-plugins' + prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ steps.pr.outputs.number }}'