From 43bbf76ea46d6ce1770f074b083ec8d16716cb05 Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 16 Jun 2026 16:53:58 +0200 Subject: [PATCH] chore: review fork PRs via workflow_run two-stage flow (#30930) --- .github/workflows/claude-code-review-run.yml | 98 ++++++++++++++++++++ .github/workflows/claude-code-review.yml | 58 ++++-------- 2 files changed, 117 insertions(+), 39 deletions(-) create mode 100644 .github/workflows/claude-code-review-run.yml diff --git a/.github/workflows/claude-code-review-run.yml b/.github/workflows/claude-code-review-run.yml new file mode 100644 index 000000000..0036a94ac --- /dev/null +++ b/.github/workflows/claude-code-review-run.yml @@ -0,0 +1,98 @@ +name: Claude Code Review (run) + +# Stage 2 (privileged): triggered when the "Claude Code Review" workflow above +# completes. workflow_run executes in the base-repo context, so it has access +# to secrets and id-token even for PRs from forks. This is what makes reviewing +# external contributors' PRs possible. +# +# Security: the triggering workflow file is fork-controlled, so the uploaded +# artifact is untrusted. We validate the PR number is purely numeric before +# using it, and we only check out the base ref (never the PR head into the +# workspace root) so untrusted code is never executed here. See +# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ +on: + workflow_run: + workflows: ["Claude Code Review"] + types: [completed] + +jobs: + gate: + # Only react to PR runs that finished successfully. + if: > + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + permissions: + actions: read + outputs: + number: ${{ steps.read.outputs.number }} + found: ${{ steps.check.outputs.found }} + steps: + - name: Check for PR-number artifact + id: check + uses: actions/github-script@v7 + with: + script: | + const { data } = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }); + core.setOutput('found', data.artifacts.some(a => a.name === 'pr-number') ? 'true' : 'false'); + + - name: Download PR number + if: steps.check.outputs.found == 'true' + uses: actions/download-artifact@v4 + with: + name: pr-number + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Read and validate PR number + id: read + if: steps.check.outputs.found == 'true' + run: | + num="$(cat pr-number.txt)" + # Artifact comes from the fork-controlled trigger workflow — never trust it blindly. + if ! [[ "$num" =~ ^[0-9]+$ ]]; then + echo "Refusing to proceed: PR number is not numeric ($num)" >&2 + exit 1 + fi + echo "number=$num" >> "$GITHUB_OUTPUT" + + claude-review: + needs: gate + if: needs.gate.outputs.found == 'true' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write # post the review on the PR + issues: read + id-token: write + concurrency: + group: claude-review-run-${{ needs.gate.outputs.number }} + cancel-in-progress: true + steps: + # Default checkout (base ref) only — do NOT check out the untrusted PR head. + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code Review + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + # Provided explicitly so the review can post on fork PRs; also required + # for allowed_non_write_users to take effect. + github_token: ${{ secrets.GITHUB_TOKEN }} + # Review PRs from external contributors who lack write access. The diff + # is fetched read-only and never executed; the subprocess env is scrubbed. + allowed_non_write_users: "*" + # review dependency bumps opened by Dependabot + allowed_bots: "dependabot[bot]" + plugin_marketplaces: "https://github.com/anthropics/claude-code.git" + plugins: "code-review@claude-code-plugins" + prompt: "/code-review:code-review ${{ github.repository }}/pull/${{ needs.gate.outputs.number }}" + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 1762d59cc..d0e0945b5 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -1,52 +1,32 @@ name: Claude Code Review +# Stage 1 (unprivileged): runs on every PR, including forks. Fork PRs get a +# read-only token and no secrets, so the review itself cannot run here. This +# job only records the PR number; the privileged "Claude Code Review (run)" +# workflow picks it up via workflow_run and performs the actual review. on: pull_request: types: [opened, synchronize, ready_for_review, reopened] - # Optional: Only run on specific file changes - # paths: - # - "src/**/*.ts" - # - "src/**/*.tsx" - # - "src/**/*.js" - # - "src/**/*.jsx" + +concurrency: + group: claude-review-${{ github.event.pull_request.number }} + cancel-in-progress: true jobs: - claude-review: + prepare: # Skip draft PRs if: github.event.pull_request.draft == false - concurrency: - group: claude-review-${{ github.event.pull_request.number }} - cancel-in-progress: true - - # Optional: Filter by PR author - # if: | - # github.event.pull_request.user.login == 'external-contributor' || - # github.event.pull_request.user.login == 'new-developer' || - # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - issues: read - id-token: write - + permissions: {} steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 + - name: Save PR number + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + run: echo "$PR_NUMBER" > pr-number.txt - - name: Run Claude Code Review - id: claude-review - uses: anthropics/claude-code-action@v1 + - name: Upload PR number + uses: actions/upload-artifact@v4 with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - # review dependency bumps opened by Dependabot - allowed_bots: 'dependabot[bot]' - plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' - plugins: 'code-review@claude-code-plugins' - prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' - # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - # or https://code.claude.com/docs/en/cli-reference for available options - + name: pr-number + path: pr-number.txt + retention-days: 1