forked from anthropics/claude-code-security-review
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathaction.yml
More file actions
379 lines (330 loc) · 15.5 KB
/
action.yml
File metadata and controls
379 lines (330 loc) · 15.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
name: 'Claude Code Reviewer'
description: 'AI-powered code review GitHub Action using Claude. Unified multi-agent review for code quality and security.'
author: 'Nutrient'
inputs:
comment-pr:
description: 'Whether to comment on PRs with findings'
required: false
default: 'true'
upload-results:
description: 'Whether to upload results as artifacts'
required: false
default: 'true'
exclude-directories:
description: 'Comma-separated list of directories to exclude from scanning'
required: false
default: ''
claudecode-timeout:
description: 'Timeout for ClaudeCode analysis in minutes'
required: false
default: '20'
claude-api-key:
description: 'Anthropic Claude API key for code review analysis'
required: true
default: ''
claude-model:
description: 'Claude model to use for code review analysis (e.g., claude-sonnet-4-20250514)'
required: false
default: ''
run-every-commit:
description: 'Run ClaudeCode on every commit (skips cache check). Warning: This may lead to more false positives on PRs with many commits as the AI analyzes the same code multiple times.'
required: false
default: 'false'
false-positive-filtering-instructions:
description: 'Path to custom false positive filtering instructions text file'
required: false
default: ''
custom-review-instructions:
description: 'Path to custom code review instructions text file to append to the audit prompt'
required: false
default: ''
custom-security-scan-instructions:
description: 'Path to custom security scan instructions text file to append to the security section (optional)'
required: false
default: ''
dismiss-stale-reviews:
description: 'Dismiss previous bot reviews when posting a new review (useful for follow-up commits). If false, skips posting when existing review comments are found.'
required: false
default: 'true'
skip-draft-prs:
description: 'Skip code review on draft pull requests'
required: false
default: 'true'
require-label:
description: 'Only run review if this label is present on the PR. Leave empty to review all PRs. To trigger on label addition, add "labeled" to your workflow pull_request types.'
required: false
default: ''
max-diff-lines:
description: 'Maximum diff lines to embed in prompt. Larger diffs use agentic file reading instead. Set to 0 to always use agentic mode. Default: 5000'
required: false
default: '5000'
outputs:
findings-count:
description: 'Number of code review findings'
value: ${{ steps.claudecode-scan.outputs.findings_count }}
results-file:
description: 'Path to the results JSON file'
value: ${{ steps.claudecode-scan.outputs.results_file }}
runs:
using: 'composite'
steps:
- name: Install GitHub CLI
shell: bash
run: |
echo "::group::Install gh CLI"
# Install GitHub CLI for PR operations
sudo apt-get update && sudo apt-get install -y gh
echo "::endgroup::"
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Check ClaudeCode run history
id: claudecode-history
if: github.event_name == 'pull_request'
uses: actions/cache@v4
with:
path: .claudecode-marker
key: claudecode-${{ github.repository_id }}-pr-${{ github.event.pull_request.number }}-${{ github.sha }}
restore-keys: |
claudecode-${{ github.repository_id }}-pr-${{ github.event.pull_request.number }}-
- name: Determine ClaudeCode enablement
id: claudecode-check
shell: bash
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
RUN_EVERY_COMMIT: ${{ inputs.run-every-commit }}
SKIP_DRAFT_PRS: ${{ inputs.skip-draft-prs }}
IS_DRAFT: ${{ github.event.pull_request.draft }}
REQUIRE_LABEL: ${{ inputs.require-label }}
PR_LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }}
run: |
# Check if ClaudeCode should be enabled
ENABLE_CLAUDECODE="true"
SILENCE_CLAUDECODE_COMMENTS="false"
# For PRs, check sampling and cache
if [ "${{ github.event_name }}" == "pull_request" ]; then
PR_NUMBER="$PR_NUMBER"
CACHE_HIT="${{ steps.claudecode-history.outputs.cache-hit }}"
# Check if required label is present
if [ -n "$REQUIRE_LABEL" ]; then
if echo "$PR_LABELS" | jq -e --arg label "$REQUIRE_LABEL" 'index($label) != null' > /dev/null 2>&1; then
echo "Required label '$REQUIRE_LABEL' found on PR #$PR_NUMBER"
else
echo "Skipping code review: required label '$REQUIRE_LABEL' not found on PR #$PR_NUMBER"
ENABLE_CLAUDECODE="false"
fi
fi
# Skip draft PRs if configured
if [ "$ENABLE_CLAUDECODE" == "true" ] && [ "$SKIP_DRAFT_PRS" == "true" ] && [ "$IS_DRAFT" == "true" ]; then
echo "Skipping code review for draft PR #$PR_NUMBER"
ENABLE_CLAUDECODE="false"
# Now check cache - if ClaudeCode has already run, disable unless run-every-commit is true
# Check if marker file exists (cache may have been restored from a different SHA)
elif [ "$ENABLE_CLAUDECODE" == "true" ] && [ "$RUN_EVERY_COMMIT" != "true" ] && [ -f ".claudecode-marker/marker.json" ]; then
echo "ClaudeCode has already run on PR #$PR_NUMBER (found marker file), forcing disable to avoid false positives"
ENABLE_CLAUDECODE="false"
elif [ "$ENABLE_CLAUDECODE" == "true" ] && [ "$RUN_EVERY_COMMIT" == "true" ] && [ -f ".claudecode-marker/marker.json" ]; then
echo "ClaudeCode has already run on PR #$PR_NUMBER but run-every-commit is enabled, running again"
elif [ "$ENABLE_CLAUDECODE" == "true" ]; then
echo "ClaudeCode will run for PR #$PR_NUMBER (first run)"
fi
fi
echo "enable_claudecode=$ENABLE_CLAUDECODE" >> $GITHUB_OUTPUT
echo "silence_claudecode_comments=$SILENCE_CLAUDECODE_COMMENTS" >> $GITHUB_OUTPUT
if [ "$ENABLE_CLAUDECODE" == "true" ]; then
echo "ClaudeCode is enabled for this run"
else
echo "ClaudeCode is disabled for this run"
fi
- name: Reserve ClaudeCode slot to prevent race conditions
if: steps.claudecode-check.outputs.enable_claudecode == 'true' && github.event_name == 'pull_request'
shell: bash
env:
REPOSITORY_ID: ${{ github.repository_id }}
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
SHA: ${{ github.sha }}
RUN_ID: ${{ github.run_id }}
RUN_NUMBER: ${{ github.run_number }}
run: |
# Create a reservation marker immediately to prevent other concurrent runs
mkdir -p .claudecode-marker
cat > .claudecode-marker/marker.json << EOF
{
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"repository_id": "$REPOSITORY_ID",
"repository": "$REPOSITORY",
"pr_number": $PR_NUMBER,
"sha": "$SHA",
"status": "reserved",
"run_id": "$RUN_ID",
"run_number": "$RUN_NUMBER"
}
EOF
echo "Created ClaudeCode reservation marker for PR #$PR_NUMBER"
- name: Save ClaudeCode reservation to cache
if: steps.claudecode-check.outputs.enable_claudecode == 'true' && github.event_name == 'pull_request'
uses: actions/cache/save@v4
with:
path: .claudecode-marker
key: claudecode-${{ github.repository_id }}-pr-${{ github.event.pull_request.number }}-${{ github.sha }}
- name: Set up Node.js
if: steps.claudecode-check.outputs.enable_claudecode == 'true'
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install dependencies
shell: bash
env:
ACTION_PATH: ${{ github.action_path }}
run: |
echo "::group::Install Deps"
if [ "${{ steps.claudecode-check.outputs.enable_claudecode }}" == "true" ]; then
pip install -r "$ACTION_PATH/claudecode/requirements.txt"
npm install -g @anthropic-ai/claude-code
fi
sudo apt-get update && sudo apt-get install -y jq
echo "::endgroup::"
- name: Run ClaudeCode scan
id: claudecode-scan
if: steps.claudecode-check.outputs.enable_claudecode == 'true'
shell: bash
env:
GITHUB_TOKEN: ${{ env.GITHUB_TOKEN || github.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
ANTHROPIC_API_KEY: ${{ inputs.claude-api-key }}
ENABLE_CLAUDE_FILTERING: 'true'
EXCLUDE_DIRECTORIES: ${{ inputs.exclude-directories }}
FALSE_POSITIVE_FILTERING_INSTRUCTIONS: ${{ inputs.false-positive-filtering-instructions }}
CUSTOM_REVIEW_INSTRUCTIONS: ${{ inputs.custom-review-instructions }}
CUSTOM_SECURITY_SCAN_INSTRUCTIONS: ${{ inputs.custom-security-scan-instructions }}
CLAUDE_MODEL: ${{ inputs.claude-model }}
CLAUDECODE_TIMEOUT: ${{ inputs.claudecode-timeout }}
MAX_DIFF_LINES: ${{ inputs.max-diff-lines }}
ACTION_PATH: ${{ github.action_path }}
run: |
echo "Running ClaudeCode AI code review analysis..."
echo "----------------------------------------"
# Initialize outputs
echo "findings_count=0" >> $GITHUB_OUTPUT
echo "results_file=claudecode/claudecode-results.json" >> $GITHUB_OUTPUT
# Skip ClaudeCode if not a PR
if [ "${{ github.event_name }}" != "pull_request" ]; then
echo "ClaudeCode only runs on pull requests, skipping"
exit 0
fi
# Validate API key is provided
if [ -z "$ANTHROPIC_API_KEY" ]; then
echo "::error::ANTHROPIC_API_KEY is not set. Please provide the claude-api-key input to the action."
echo "Example usage:"
echo " - uses: PSPDFKit-labs/nutrient-code-review@main"
echo " with:"
echo " claude-api-key: \$\{{ secrets.ANTHROPIC_API_KEY }}"
exit 1
fi
# Set timeout
export CLAUDE_TIMEOUT="$CLAUDECODE_TIMEOUT"
# Run ClaudeCode audit with verbose debugging
export REPO_PATH=$(pwd)
cd "$ACTION_PATH"
# Enable verbose debugging
echo "::group::ClaudeCode Environment"
echo "Current directory: $(pwd)"
echo "Python version: $(python --version)"
echo "Claude CLI version: $(claude --version 2>&1 || echo 'Claude CLI not found')"
echo "ANTHROPIC_API_KEY set: $(if [ -n "$ANTHROPIC_API_KEY" ]; then echo 'Yes'; else echo 'No'; fi)"
echo "GITHUB_REPOSITORY: $GITHUB_REPOSITORY"
echo "PR_NUMBER: $PR_NUMBER"
echo "Python path: $PYTHONPATH"
echo "Files in claudecode directory:"
ls -la claudecode/
echo "::endgroup::"
echo "::group::ClaudeCode Execution"
# Add current directory to Python path so it can find the claudecode module
export PYTHONPATH="${PYTHONPATH:+$PYTHONPATH:}$(pwd)"
echo "Updated PYTHONPATH: $PYTHONPATH"
# Run from the action root directory so Python can find the claudecode module
python -u claudecode/github_action_audit.py > claudecode/claudecode-results.json 2>claudecode/claudecode-error.log || CLAUDECODE_EXIT_CODE=$?
if [ -n "$CLAUDECODE_EXIT_CODE" ]; then
echo "::warning::ClaudeCode exited with code $CLAUDECODE_EXIT_CODE"
else
echo "ClaudeCode scan completed successfully"
fi
# Parse ClaudeCode results and count findings regardless of exit code
if [ -f claudecode/claudecode-results.json ]; then
FILE_SIZE=$(wc -c < claudecode/claudecode-results.json)
echo "ClaudeCode results file size: $FILE_SIZE bytes"
# Check if file is empty or too small
if [ "$FILE_SIZE" -lt 2 ]; then
echo "::warning::ClaudeCode results file is empty or invalid (size: $FILE_SIZE bytes)"
echo "::warning::ClaudeCode may have failed silently. Check claudecode-error.log"
if [ -f claudecode/claudecode-error.log ]; then
echo "Error log contents:"
cat claudecode/claudecode-error.log
fi
echo "findings_count=0" >> $GITHUB_OUTPUT
else
echo "ClaudeCode results preview:"
head -n 300 claudecode/claudecode-results.json || echo "Unable to preview results"
# Check if the result is an error
if jq -e '.error' claudecode/claudecode-results.json > /dev/null 2>&1; then
ERROR_MSG=$(jq -r '.error' claudecode/claudecode-results.json)
echo "::warning::ClaudeCode error: $ERROR_MSG"
echo "findings_count=0" >> $GITHUB_OUTPUT
else
# Use -r to get raw output and handle potential null/missing findings array
CLAUDECODE_FINDINGS_COUNT=$(jq -r '.findings | if . == null then 0 else length end' claudecode/claudecode-results.json 2>/dev/null || echo "0")
echo "::debug::Extracted ClaudeCode findings count: $CLAUDECODE_FINDINGS_COUNT"
echo "findings_count=$CLAUDECODE_FINDINGS_COUNT" >> $GITHUB_OUTPUT
echo "ClaudeCode found $CLAUDECODE_FINDINGS_COUNT review issues"
# Also create findings.json for PR comment script
jq '.findings // []' claudecode/claudecode-results.json > findings.json || echo '[]' > findings.json
fi
fi
else
echo "::warning::ClaudeCode results file not found"
if [ -f claudecode/claudecode-error.log ]; then
echo "Error log contents:"
cat claudecode/claudecode-error.log
fi
echo "findings_count=0" >> $GITHUB_OUTPUT
fi
# Always copy files to workspace root regardless of the outcome
# This ensures artifact upload and PR commenting can find them
if [ -f findings.json ]; then
cp findings.json ${{ github.workspace }}/findings.json || true
fi
if [ -f claudecode/claudecode-results.json ]; then
cp claudecode/claudecode-results.json ${{ github.workspace }}/claudecode-results.json || true
fi
if [ -f claudecode/claudecode-error.log ]; then
cp claudecode/claudecode-error.log ${{ github.workspace }}/claudecode-error.log || true
fi
echo "::endgroup::"
- name: Upload scan results
if: always() && inputs.upload-results == 'true'
uses: actions/upload-artifact@v4
with:
name: code-review-results
path: |
findings.json
claudecode-results.json
claudecode-error.log
retention-days: 7
if-no-files-found: ignore
- name: Comment PR with findings
if: github.event_name == 'pull_request' && inputs.comment-pr == 'true' && steps.claudecode-check.outputs.enable_claudecode == 'true'
shell: bash
env:
GITHUB_TOKEN: ${{ env.GITHUB_TOKEN || github.token }}
CLAUDECODE_FINDINGS: ${{ steps.claudecode-scan.outputs.findings_count }}
SILENCE_CLAUDECODE_COMMENTS: ${{ steps.claudecode-check.outputs.silence_claudecode_comments }}
DISMISS_STALE_REVIEWS: ${{ inputs.dismiss-stale-reviews }}
ACTION_PATH: ${{ github.action_path }}
run: |
node "$ACTION_PATH/scripts/comment-pr-findings.js"
branding:
icon: 'shield'
color: 'red'