forked from PSPDFKit-labs/nutrient-code-review
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathaction.yml
More file actions
607 lines (526 loc) · 25.1 KB
/
action.yml
File metadata and controls
607 lines (526 loc) · 25.1 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
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
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: 'DEPRECATED: Use trigger-on-commit instead. 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'
deprecationMessage: 'run-every-commit is deprecated. Use trigger-on-commit instead for more granular control over when reviews run.'
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: ''
skip-draft-prs:
description: 'Skip code review on draft pull requests'
required: false
default: 'true'
app-slug:
description: 'GitHub App slug for bot mentions (e.g., "my-code-review-app"). Defaults to "github-actions".'
required: false
default: 'github-actions'
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-chars:
description: |
Maximum diff characters to embed in prompt (default: 400000 = 400k chars).
Larger diffs use agentic file reading instead. Set to 0 to always use agentic mode.
IMPORTANT: For large limits (>400k), use a model with larger context like:
- claude-sonnet-4-5-20250929 (1M context) for diffs up to 800k chars
- Set via 'claude-model' input parameter
Note: ~400k chars fits comfortably in 200k token models (Opus/Sonnet standard).
required: false
default: '400000'
max-diff-lines:
description: |
[DEPRECATED] Use 'max-diff-chars' instead. This converts lines to chars (line * 80).
Kept for backward compatibility only. If both set, max-diff-chars takes precedence.
required: false
default: ''
trigger-on-open:
description: 'Run review when PR is first opened'
required: false
default: 'true'
trigger-on-commit:
description: 'Run review on every new commit'
required: false
default: 'false'
trigger-on-review-request:
description: 'Run review when someone requests a review from the bot'
required: false
default: 'true'
trigger-on-mention:
description: 'Run review when bot is mentioned in a PR comment'
required: false
default: 'true'
trigger-on-review-command:
description: 'Run review when "review" is used in a PR comment'
required: false
default: 'true'
trigger-on-ready-for-review:
description: 'Run review when a draft PR is marked ready for review'
required: false
default: 'true'
enable-claude-filtering:
description: 'Use Claude API to validate and filter findings (reduces false positives but increases API costs)'
required: false
default: 'false'
enable-heuristic-filtering:
description: 'Use pattern-based heuristic rules to filter common false positives (e.g., stylistic issues, low-signal security warnings)'
required: false
default: 'true'
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: Get PR info for issue_comment events
id: pr-info
if: github.event_name == 'issue_comment'
shell: bash
env:
GITHUB_TOKEN: ${{ env.GITHUB_TOKEN || github.token }}
run: |
set -euo pipefail # Exit on error, undefined vars, pipe failures
# Check if comment is on a PR
if [ -n "${{ github.event.issue.pull_request.url }}" ]; then
if ! PR_DATA=$(gh api "${{ github.event.issue.pull_request.url }}" 2>&1); then
echo "Error: Failed to fetch PR data from GitHub API"
echo "is_pr=false" >> $GITHUB_OUTPUT
exit 1
fi
if ! PR_HEAD_SHA=$(echo "$PR_DATA" | jq -r '.head.sha' 2>&1); then
echo "Error: Failed to parse PR SHA from API response"
echo "is_pr=false" >> $GITHUB_OUTPUT
exit 1
fi
if ! PR_BASE_SHA=$(echo "$PR_DATA" | jq -r '.base.sha' 2>&1); then
echo "Error: Failed to parse PR base SHA from API response"
echo "is_pr=false" >> $GITHUB_OUTPUT
exit 1
fi
# Extract PR labels (array of label names)
if ! PR_LABELS=$(echo "$PR_DATA" | jq -c '[.labels[].name]' 2>&1); then
echo "Error: Failed to parse PR labels from API response"
echo "is_pr=false" >> $GITHUB_OUTPUT
exit 1
fi
# Extract draft status
if ! IS_DRAFT=$(echo "$PR_DATA" | jq -r '.draft' 2>&1); then
echo "Error: Failed to parse PR draft status from API response"
echo "is_pr=false" >> $GITHUB_OUTPUT
exit 1
fi
echo "pr_number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT
echo "pr_sha=$PR_HEAD_SHA" >> $GITHUB_OUTPUT
echo "pr_base_sha=$PR_BASE_SHA" >> $GITHUB_OUTPUT
echo "pr_labels=$PR_LABELS" >> $GITHUB_OUTPUT
echo "is_draft=$IS_DRAFT" >> $GITHUB_OUTPUT
echo "is_pr=true" >> $GITHUB_OUTPUT
echo "Issue comment is on PR #${{ github.event.issue.number }} with base SHA $PR_BASE_SHA, head SHA $PR_HEAD_SHA"
else
echo "is_pr=false" >> $GITHUB_OUTPUT
echo "Issue comment is not on a PR, skipping"
fi
- name: Checkout PR head for issue_comment events
if: github.event_name == 'issue_comment' && steps.pr-info.outputs.is_pr == 'true'
shell: bash
run: |
set -euo pipefail # Exit on error, undefined vars, pipe failures
echo "Checking out PR #${{ steps.pr-info.outputs.pr_number }} at SHA ${{ steps.pr-info.outputs.pr_sha }}"
if ! git fetch origin pull/${{ steps.pr-info.outputs.pr_number }}/head:pr-${{ steps.pr-info.outputs.pr_number }}; then
echo "Error: Failed to fetch PR branch"
exit 1
fi
if ! git checkout pr-${{ steps.pr-info.outputs.pr_number }}; then
echo "Error: Failed to checkout PR branch"
exit 1
fi
- name: Check ClaudeCode run history
id: claudecode-history
if: github.event_name == 'pull_request' || (github.event_name == 'issue_comment' && steps.pr-info.outputs.is_pr == 'true')
uses: actions/cache@v4
with:
path: .claudecode-marker
key: claudecode-${{ github.repository_id }}-pr-${{ github.event.pull_request.number || steps.pr-info.outputs.pr_number }}-${{ github.event.pull_request.head.sha || steps.pr-info.outputs.pr_sha }}
restore-keys: |
claudecode-${{ github.repository_id }}-pr-${{ github.event.pull_request.number || steps.pr-info.outputs.pr_number }}-
- name: Detect trigger type
id: trigger-detection
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
EVENT_ACTION: ${{ github.event.action }}
COMMENT_BODY: ${{ github.event.comment.body }}
APP_SLUG: ${{ inputs.app-slug }}
run: |
TRIGGER_TYPE="unknown"
if [ "$EVENT_NAME" == "pull_request" ]; then
case "$EVENT_ACTION" in
opened|reopened)
TRIGGER_TYPE="open"
;;
ready_for_review)
TRIGGER_TYPE="ready_for_review"
;;
synchronize)
TRIGGER_TYPE="commit"
;;
review_requested)
TRIGGER_TYPE="review_request"
;;
labeled)
TRIGGER_TYPE="label"
;;
esac
elif [ "$EVENT_NAME" == "issue_comment" ]; then
# Check if comment uses review command trigger
if echo "$COMMENT_BODY" | grep -qiE '^[[:space:]]*review([[:space:]]|$)'; then
TRIGGER_TYPE="slash_command"
echo "Detected review command in comment"
fi
# Check if comment mentions this specific bot
# Escape special regex characters in bot login to prevent regex injection
if [ "$TRIGGER_TYPE" == "unknown" ] && [ -n "$APP_SLUG" ]; then
ESCAPED_BOT=$(printf '%s\n' "$APP_SLUG" | sed 's/[]\/$*.^[]/\\&/g')
if echo "$COMMENT_BODY" | grep -qE "@${ESCAPED_BOT}\\b"; then
TRIGGER_TYPE="mention"
echo "Detected mention of @$APP_SLUG in comment"
fi
fi
fi
echo "trigger_type=$TRIGGER_TYPE" >> $GITHUB_OUTPUT
echo "Detected trigger type: $TRIGGER_TYPE"
- name: Determine ClaudeCode enablement
id: claudecode-check
shell: bash
env:
GITHUB_EVENT_NAME: ${{ github.event_name }}
PR_NUMBER: ${{ github.event.pull_request.number || steps.pr-info.outputs.pr_number }}
GITHUB_SHA: ${{ github.event.pull_request.head.sha || steps.pr-info.outputs.pr_sha || github.sha }}
RUN_EVERY_COMMIT: ${{ inputs.run-every-commit }}
TRIGGER_ON_OPEN: ${{ inputs.trigger-on-open }}
TRIGGER_ON_COMMIT: ${{ inputs.trigger-on-commit }}
TRIGGER_ON_REVIEW_REQUEST: ${{ inputs.trigger-on-review-request }}
TRIGGER_ON_MENTION: ${{ inputs.trigger-on-mention }}
TRIGGER_ON_REVIEW_COMMAND: ${{ inputs.trigger-on-review-command }}
TRIGGER_ON_READY_FOR_REVIEW: ${{ inputs.trigger-on-ready-for-review }}
SKIP_DRAFT_PRS: ${{ inputs.skip-draft-prs }}
IS_DRAFT: ${{ github.event.pull_request.draft || steps.pr-info.outputs.is_draft }}
REQUIRE_LABEL: ${{ inputs.require-label }}
PR_LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) || steps.pr-info.outputs.pr_labels }}
TRIGGER_TYPE: ${{ steps.trigger-detection.outputs.trigger_type }}
IS_PR: ${{ steps.pr-info.outputs.is_pr }}
run: |
# Execute the enablement determination script
# This script encapsulates the complex logic for deciding when to run code reviews
# See scripts/determine-claudecode-enablement.sh for implementation details
"${{ github.action_path }}/scripts/determine-claudecode-enablement.sh"
- name: Add eyes reaction for /review command
if: github.event_name == 'issue_comment' && steps.trigger-detection.outputs.trigger_type == 'slash_command' && steps.claudecode-check.outputs.enable_claudecode == 'true'
shell: bash
env:
GITHUB_TOKEN: ${{ env.GITHUB_TOKEN || github.token }}
run: |
gh api \
"repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions" \
-X POST \
-H "Accept: application/vnd.github+json" \
-f content='eyes' >/dev/null 2>&1 || \
echo "::warning::Unable to add eyes reaction to slash command comment"
- name: Reserve ClaudeCode slot to prevent race conditions
if: steps.claudecode-check.outputs.enable_claudecode == 'true'
shell: bash
env:
REPOSITORY_ID: ${{ github.repository_id }}
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number || steps.pr-info.outputs.pr_number }}
SHA: ${{ github.event.pull_request.head.sha || steps.pr-info.outputs.pr_sha || github.sha }}
RUN_ID: ${{ github.run_id }}
RUN_NUMBER: ${{ github.run_number }}
TRIGGER_TYPE: ${{ steps.trigger-detection.outputs.trigger_type }}
EVENT_NAME: ${{ github.event_name }}
EVENT_ACTION: ${{ github.event.action }}
run: |
set -euo pipefail # Exit on error, undefined vars, pipe failures
# Create a reservation marker immediately to prevent other concurrent runs
# Note: Using concurrency control (see README) prevents race conditions by ensuring
# only one workflow runs at a time per PR. Without it, simultaneous runs may both
# save to cache (last write wins), but SHA-based deduplication prevents duplicate reviews.
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",
"trigger_type": "$TRIGGER_TYPE",
"event_name": "$EVENT_NAME",
"event_action": "$EVENT_ACTION"
}
EOF
echo "Created ClaudeCode reservation marker for PR #$PR_NUMBER (trigger: $TRIGGER_TYPE, SHA: $SHA)"
- name: Save ClaudeCode reservation to cache
if: steps.claudecode-check.outputs.enable_claudecode == 'true'
uses: actions/cache/save@v4
with:
path: .claudecode-marker
key: claudecode-${{ github.repository_id }}-pr-${{ github.event.pull_request.number || steps.pr-info.outputs.pr_number }}-${{ github.event.pull_request.head.sha || steps.pr-info.outputs.pr_sha || github.sha }}
- name: Set up Python
if: steps.claudecode-check.outputs.enable_claudecode == 'true'
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Set up Node.js
if: steps.claudecode-check.outputs.enable_claudecode == 'true'
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Setup git for diffing
if: steps.claudecode-check.outputs.enable_claudecode == 'true'
shell: bash
env:
BASE_SHA: ${{ github.event.pull_request.base.sha || steps.pr-info.outputs.pr_base_sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha || steps.pr-info.outputs.pr_sha }}
run: |
echo "::group::Prepare repository so 'git diff' shows PR changes"
BASE_COMMIT="$BASE_SHA"
HEAD_COMMIT="$HEAD_SHA"
echo "Base SHA (PR snapshot): $BASE_COMMIT"
echo "Head SHA (current PR): $HEAD_COMMIT"
# Fetch both commits
git fetch origin "$BASE_COMMIT" --depth=1
git fetch origin "$HEAD_COMMIT" --depth=1
# Reset to base commit (B)
git checkout "$BASE_COMMIT"
# Restore PR files to working directory (E) - NOT staged
git restore --source="$HEAD_COMMIT" --worktree .
# Now: HEAD=B, index=B, worktree=E
# So: git diff shows B → E (PR changes)
echo "Repository prepared. Plain 'git diff' now shows PR changes."
echo "::endgroup::"
- name: Install dependencies
if: steps.claudecode-check.outputs.enable_claudecode == 'true'
shell: bash
env:
ACTION_PATH: ${{ github.action_path }}
run: |
echo "::group::Install Deps"
pip install -r "$ACTION_PATH/claudecode/requirements.txt"
npm install -g @anthropic-ai/claude-code
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 || steps.pr-info.outputs.pr_number }}
ANTHROPIC_API_KEY: ${{ inputs.claude-api-key }}
ENABLE_CLAUDE_FILTERING: ${{ inputs.enable-claude-filtering }}
ENABLE_HEURISTIC_FILTERING: ${{ inputs.enable-heuristic-filtering }}
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_CHARS: ${{ inputs.max-diff-chars }}
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 or issue_comment event
if [ "${{ github.event_name }}" != "pull_request" ] && [ "${{ github.event_name }}" != "issue_comment" ]; then
echo "ClaudeCode only runs on pull requests and issue comments, 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 and pr-summary.json for PR comment script
jq '.findings // []' claudecode/claudecode-results.json > findings.json || echo '[]' > findings.json
jq '.pr_summary // {}' claudecode/claudecode-results.json > pr-summary.json || echo '{}' > pr-summary.json
jq '.analysis_summary // {}' claudecode/claudecode-results.json > analysis-summary.json || echo '{}' > analysis-summary.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 pr-summary.json ]; then
cp pr-summary.json ${{ github.workspace }}/pr-summary.json || true
fi
if [ -f analysis-summary.json ]; then
cp analysis-summary.json ${{ github.workspace }}/analysis-summary.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' || github.event_name == 'issue_comment') && 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 }}
ACTION_PATH: ${{ github.action_path }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha || steps.pr-info.outputs.pr_sha }}
run: |
node "$ACTION_PATH/scripts/comment-pr-findings.js"
- name: Add completion reaction for /review command
if: success() && github.event_name == 'issue_comment' && steps.trigger-detection.outputs.trigger_type == 'slash_command' && steps.claudecode-check.outputs.enable_claudecode == 'true'
shell: bash
env:
GITHUB_TOKEN: ${{ env.GITHUB_TOKEN || github.token }}
run: |
gh api \
"repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions" \
-X POST \
-H "Accept: application/vnd.github+json" \
-f content='rocket' >/dev/null 2>&1 || \
echo "::warning::Unable to add completion reaction to slash command comment"
branding:
icon: 'shield'
color: 'red'