Skip to content

Commit c9ca210

Browse files
Add Claude Code PR review workflow
Enable Claude Code online review in GitHub Actions and limit fork-triggered pull_request_target runs to review_requested so maintainers explicitly gate those review runs.
1 parent ce6852c commit c9ca210

1 file changed

Lines changed: 127 additions & 0 deletions

File tree

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
name: Claude Code Review
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened, ready_for_review]
6+
pull_request_target:
7+
types: [review_requested]
8+
9+
concurrency:
10+
# Include event_name to prevent pull_request and pull_request_target from canceling each other
11+
# This ensures non-fork PRs aren't accidentally canceled by the skipped pull_request_target run
12+
group: claude-pr-review-${{ github.event_name }}-${{ github.event.pull_request.number }}
13+
cancel-in-progress: true
14+
15+
permissions:
16+
contents: read
17+
pull-requests: write
18+
issues: write
19+
20+
jobs:
21+
claude_review:
22+
if: |
23+
(github.event_name == 'pull_request' &&
24+
github.event.pull_request.head.repo.fork == false) ||
25+
(github.event_name == 'pull_request_target' &&
26+
github.event.pull_request.head.repo.fork == true)
27+
runs-on: ubuntu-latest
28+
timeout-minutes: 20
29+
30+
steps:
31+
- name: Checkout repository
32+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
33+
with:
34+
fetch-depth: 1
35+
# For pull_request_target (fork PRs), checkout base branch to prevent prompt injection
36+
# via malicious AGENTS.md. Claude uses gh pr diff to review the actual PR changes.
37+
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.ref }}
38+
persist-credentials: false
39+
40+
- name: Detect workflow changes
41+
if: github.event_name == 'pull_request_target'
42+
id: workflow_changes
43+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
44+
with:
45+
script: |
46+
let pullNumber = context.payload.pull_request?.number;
47+
48+
if (!pullNumber && context.payload.issue?.pull_request) {
49+
pullNumber = context.payload.issue.number;
50+
}
51+
52+
// Detection logic designed for compatibility with additional triggers
53+
// that provide issue payloads.
54+
55+
if (!pullNumber) {
56+
// Fail closed: if we cannot determine a PR number, skip Claude on pull_request_target
57+
// to avoid bypassing workflow change security checks.
58+
core.warning('Claude review skipped: unable to determine pull request number from event payload.');
59+
core.setOutput('workflows_changed', 'true');
60+
return;
61+
}
62+
63+
const files = await github.paginate(
64+
github.rest.pulls.listFiles,
65+
{
66+
owner: context.repo.owner,
67+
repo: context.repo.repo,
68+
pull_number: pullNumber,
69+
}
70+
);
71+
72+
// GitHub API has a 3000 file limit. If we hit it, fail-closed to prevent bypassing
73+
// security checks by hiding sensitive changes in huge PRs beyond the limit.
74+
if (files.length >= 3000) {
75+
core.warning('PR has 3000+ files (API limit reached). Skipping Claude review as a security precaution.');
76+
core.setOutput('workflows_changed', 'true');
77+
return;
78+
}
79+
80+
const SENSITIVE_DIR_PREFIXES = ['.github/workflows/', '.github/actions/'];
81+
// Matches AGENTS.md and CLAUDE.md at root or in any subdirectory for fail-safe security.
82+
// This prevents prompt injection via instruction files anywhere in the PR.
83+
const SENSITIVE_FILES = ['AGENTS.md', 'CLAUDE.md'];
84+
const changed = files.some((file) => {
85+
// Check both filename and previous_filename (for renames) to prevent bypassing
86+
// security checks by renaming sensitive files out of protected paths.
87+
const paths = [file.filename, file.previous_filename].filter(Boolean);
88+
return paths.some(path =>
89+
SENSITIVE_DIR_PREFIXES.some((prefix) => path.startsWith(prefix)) ||
90+
SENSITIVE_FILES.some((name) => path === name || path.endsWith('/' + name))
91+
);
92+
});
93+
94+
core.setOutput('workflows_changed', changed ? 'true' : 'false');
95+
96+
- name: Run Claude Code Review
97+
# GitHub Actions step outputs are strings; keep the condition below comparing against 'true'.
98+
# Note: For pull_request events (non-fork PRs from trusted contributors), we run the review
99+
# even when sensitive files are modified. Only fork PRs (pull_request_target) check workflows_changed.
100+
if: |
101+
github.event_name != 'pull_request_target' ||
102+
steps.workflow_changes.outputs.workflows_changed != 'true'
103+
id: claude-review
104+
uses: anthropics/claude-code-action@1b8ee3b94104046d71fde52ec3557651ad8c0d71 # v1.0.29
105+
env:
106+
ANTHROPIC_BASE_URL: ${{ secrets.ANTHROPIC_BASE_URL }}
107+
with:
108+
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
109+
github_token: ${{ secrets.GITHUB_TOKEN }}
110+
prompt: |
111+
REPO: ${{ github.repository }}
112+
PR NUMBER: ${{ github.event.pull_request.number }}
113+
114+
Please review this pull request and provide feedback on:
115+
- Code quality and best practices
116+
- Potential bugs or issues
117+
- Performance considerations
118+
- Security concerns
119+
- Test coverage
120+
121+
Use the repository's AGENTS.md for guidance on style and conventions. Be constructive and helpful in your feedback.
122+
Use `gh pr diff` to see the PR changes, then use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
123+
124+
claude_args: >
125+
--model claude-opus-4-6
126+
--allowed-tools
127+
"Bash(gh pr comment *),Bash(gh pr diff *),Bash(gh pr view *)"

0 commit comments

Comments
 (0)