|
| 1 | +name: Claude Code Review (run) |
| 2 | + |
| 3 | +# Stage 2 (privileged): triggered when the "Claude Code Review" workflow above |
| 4 | +# completes. workflow_run executes in the base-repo context, so it has access |
| 5 | +# to secrets and id-token even for PRs from forks. This is what makes reviewing |
| 6 | +# external contributors' PRs possible. |
| 7 | +# |
| 8 | +# Security: the triggering workflow file is fork-controlled, so the uploaded |
| 9 | +# artifact is untrusted. We validate the PR number is purely numeric before |
| 10 | +# using it, and we only check out the base ref (never the PR head into the |
| 11 | +# workspace root) so untrusted code is never executed here. See |
| 12 | +# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ |
| 13 | +on: |
| 14 | + workflow_run: |
| 15 | + workflows: ["Claude Code Review"] |
| 16 | + types: [completed] |
| 17 | + |
| 18 | +jobs: |
| 19 | + gate: |
| 20 | + # Only react to PR runs that finished successfully. |
| 21 | + if: > |
| 22 | + github.event.workflow_run.event == 'pull_request' && |
| 23 | + github.event.workflow_run.conclusion == 'success' |
| 24 | + runs-on: ubuntu-latest |
| 25 | + permissions: |
| 26 | + actions: read |
| 27 | + outputs: |
| 28 | + number: ${{ steps.read.outputs.number }} |
| 29 | + found: ${{ steps.check.outputs.found }} |
| 30 | + steps: |
| 31 | + - name: Check for PR-number artifact |
| 32 | + id: check |
| 33 | + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v7 |
| 34 | + with: |
| 35 | + script: | |
| 36 | + const { data } = await github.rest.actions.listWorkflowRunArtifacts({ |
| 37 | + owner: context.repo.owner, |
| 38 | + repo: context.repo.repo, |
| 39 | + run_id: context.payload.workflow_run.id, |
| 40 | + }); |
| 41 | + core.setOutput('found', data.artifacts.some(a => a.name === 'pr-number') ? 'true' : 'false'); |
| 42 | +
|
| 43 | + - name: Download PR number |
| 44 | + if: steps.check.outputs.found == 'true' |
| 45 | + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v4 |
| 46 | + with: |
| 47 | + name: pr-number |
| 48 | + run-id: ${{ github.event.workflow_run.id }} |
| 49 | + github-token: ${{ secrets.GITHUB_TOKEN }} |
| 50 | + |
| 51 | + - name: Read and validate PR number |
| 52 | + id: read |
| 53 | + if: steps.check.outputs.found == 'true' |
| 54 | + run: | |
| 55 | + num="$(cat pr-number.txt)" |
| 56 | + # Artifact comes from the fork-controlled trigger workflow — never trust it blindly. |
| 57 | + if ! [[ "$num" =~ ^[0-9]+$ ]]; then |
| 58 | + echo "Refusing to proceed: PR number is not numeric ($num)" >&2 |
| 59 | + exit 1 |
| 60 | + fi |
| 61 | + echo "number=$num" >> "$GITHUB_OUTPUT" |
| 62 | +
|
| 63 | + claude-review: |
| 64 | + needs: gate |
| 65 | + if: needs.gate.outputs.found == 'true' |
| 66 | + runs-on: ubuntu-latest |
| 67 | + permissions: |
| 68 | + contents: read |
| 69 | + pull-requests: write # post the review on the PR |
| 70 | + issues: read |
| 71 | + id-token: write |
| 72 | + statuses: write # mirror the review result onto the PR head commit |
| 73 | + concurrency: |
| 74 | + group: claude-review-run-${{ needs.gate.outputs.number }} |
| 75 | + cancel-in-progress: true |
| 76 | + steps: |
| 77 | + # workflow_run runs are detached and never show up in the PR's checks, so |
| 78 | + # publish a commit status on the PR head SHA. This surfaces a "Claude Code |
| 79 | + # Review" check next to the regular CI jobs. |
| 80 | + - name: Report review pending |
| 81 | + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v7 |
| 82 | + with: |
| 83 | + script: | |
| 84 | + await github.rest.repos.createCommitStatus({ |
| 85 | + owner: context.repo.owner, |
| 86 | + repo: context.repo.repo, |
| 87 | + sha: context.payload.workflow_run.head_sha, |
| 88 | + context: 'Claude Code Review', |
| 89 | + state: 'pending', |
| 90 | + description: 'Review in progress', |
| 91 | + target_url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, |
| 92 | + }); |
| 93 | +
|
| 94 | + # Default checkout (base ref) only — do NOT check out the untrusted PR head. |
| 95 | + - name: Checkout repository |
| 96 | + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4 |
| 97 | + with: |
| 98 | + fetch-depth: 1 |
| 99 | + |
| 100 | + - name: Run Claude Code Review |
| 101 | + id: review |
| 102 | + continue-on-error: true # always report status below, even on failure |
| 103 | + uses: anthropics/claude-code-action@v1 |
| 104 | + with: |
| 105 | + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} |
| 106 | + # Provided explicitly so the review can post on fork PRs; also required |
| 107 | + # for allowed_non_write_users to take effect. |
| 108 | + github_token: ${{ secrets.GITHUB_TOKEN }} |
| 109 | + # Review PRs from external contributors who lack write access. The diff |
| 110 | + # is fetched read-only and never executed; the subprocess env is scrubbed. |
| 111 | + allowed_non_write_users: "*" |
| 112 | + # review dependency bumps opened by Dependabot |
| 113 | + allowed_bots: "dependabot[bot]" |
| 114 | + plugin_marketplaces: "https://github.com/anthropics/claude-code.git" |
| 115 | + plugins: "code-review@claude-code-plugins" |
| 116 | + prompt: "/code-review:code-review ${{ github.repository }}/pull/${{ needs.gate.outputs.number }}" |
| 117 | + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md |
| 118 | + # or https://code.claude.com/docs/en/cli-reference for available options |
| 119 | + |
| 120 | + - name: Report review result |
| 121 | + if: always() |
| 122 | + env: |
| 123 | + OUTCOME: ${{ steps.review.outcome }} |
| 124 | + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v7 |
| 125 | + with: |
| 126 | + script: | |
| 127 | + const ok = process.env.OUTCOME === 'success'; |
| 128 | + await github.rest.repos.createCommitStatus({ |
| 129 | + owner: context.repo.owner, |
| 130 | + repo: context.repo.repo, |
| 131 | + sha: context.payload.workflow_run.head_sha, |
| 132 | + context: 'Claude Code Review', |
| 133 | + state: ok ? 'success' : 'failure', |
| 134 | + description: ok ? 'Review complete' : 'Review failed', |
| 135 | + target_url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, |
| 136 | + }); |
| 137 | + if (!ok) core.setFailed('Claude Code Review failed'); |
0 commit comments