diff --git a/.github/workflows/claude-pr-review.yml b/.github/workflows/claude-pr-review.yml index 7e944709f9..0a0003da26 100644 --- a/.github/workflows/claude-pr-review.yml +++ b/.github/workflows/claude-pr-review.yml @@ -14,21 +14,60 @@ jobs: github.event.pull_request != null || github.event.issue.pull_request != null ) && - contains(github.event.comment.body, '@claude') + contains(github.event.comment.body, '@claude') && + ( + github.event.comment.author_association == 'OWNER' || + github.event.comment.author_association == 'MEMBER' || + github.event.comment.author_association == 'COLLABORATOR' + ) runs-on: ubuntu-latest permissions: contents: read pull-requests: write - id-token: write - + id-token: write # Required by claude-code-action for GitHub App token exchange. steps: + # issue_comment payloads identify PR comments, but do not include the PR + # head repo. Query the PR before allowing Claude to run on fork PRs. + - name: Check PR origin + id: pr-origin + env: + GH_TOKEN: ${{ github.token }} + EVENT_NAME: ${{ github.event_name }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + ISSUE_PULL_REQUEST_URL: ${{ github.event.issue.pull_request.url }} + PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + REPOSITORY: ${{ github.repository }} + run: | + set -euo pipefail + if [ "$EVENT_NAME" = "issue_comment" ] && [ -z "$ISSUE_PULL_REQUEST_URL" ]; then + echo "is_internal=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + pr_number="${PULL_REQUEST_NUMBER:-$ISSUE_NUMBER}" + if ! head_repo="$(gh api "repos/${REPOSITORY}/pulls/${pr_number}" --jq '.head.repo.full_name')"; then + echo "Unable to determine PR head repository for #${pr_number}; skipping Claude." + echo "is_internal=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if [ "$head_repo" = "$REPOSITORY" ]; then + echo "is_internal=true" >> "$GITHUB_OUTPUT" + else + echo "is_internal=false" >> "$GITHUB_OUTPUT" + echo "Skipping Claude for external PR from ${head_repo:-unknown}." + fi + - name: Checkout repository - uses: actions/checkout@v7 + if: steps.pr-origin.outputs.is_internal == 'true' + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 1 + persist-credentials: false - name: Comprehensive PR Review + if: steps.pr-origin.outputs.is_internal == 'true' uses: anthropics/claude-code-action@769e3bdff9bb7ecfb51bad4c9cba23d6f5fc5ea5 # v1.0.163 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} @@ -131,8 +170,7 @@ jobs: Only provide noteworthy feedback. Keep feedback concise. Provide feedback using inline comments for specific issues. - Use top-level comments for general observations. Do not add positive reinforcement comments. claude_args: | - --allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)" + --allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr diff:*),Bash(gh pr view:*)" diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 2b22da2c5f..4a4e228036 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -10,8 +10,7 @@ permissions: contents: write pull-requests: write issues: write - id-token: write - + id-token: write # Required by claude-code-action for GitHub App token exchange. jobs: claude: runs-on: ubuntu-latest @@ -20,12 +19,46 @@ jobs: contains(github.event.comment.body, '@claude') && github.event.comment.user.login == 'matticbot' steps: + # issue_comment payloads identify PR comments, but do not include the PR + # head repo. Query the PR before allowing Claude to run on fork PRs. + - name: Check PR origin + id: pr-origin + env: + GH_TOKEN: ${{ github.token }} + EVENT_NAME: ${{ github.event_name }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + ISSUE_PULL_REQUEST_URL: ${{ github.event.issue.pull_request.url }} + PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + REPOSITORY: ${{ github.repository }} + run: | + set -euo pipefail + if [ "$EVENT_NAME" = "issue_comment" ] && [ -z "$ISSUE_PULL_REQUEST_URL" ]; then + echo "is_internal=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + pr_number="${PULL_REQUEST_NUMBER:-$ISSUE_NUMBER}" + if ! head_repo="$(gh api "repos/${REPOSITORY}/pulls/${pr_number}" --jq '.head.repo.full_name')"; then + echo "Unable to determine PR head repository for #${pr_number}; skipping Claude." + echo "is_internal=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if [ "$head_repo" = "$REPOSITORY" ]; then + echo "is_internal=true" >> "$GITHUB_OUTPUT" + else + echo "is_internal=false" >> "$GITHUB_OUTPUT" + echo "Skipping Claude for external PR from ${head_repo:-unknown}." + fi + - name: Checkout repository - uses: actions/checkout@v7 + if: steps.pr-origin.outputs.is_internal == 'true' + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 - name: Run Claude + if: steps.pr-origin.outputs.is_internal == 'true' uses: anthropics/claude-code-action@769e3bdff9bb7ecfb51bad4c9cba23d6f5fc5ea5 # v1.0.163 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}