@@ -2,56 +2,101 @@ name: Claude Code Review
22
33on :
44 pull_request :
5- types : [opened, synchronize]
5+ types : [opened, synchronize, ready_for_review, reopened ]
66 # Optional: Only run on specific file changes
77 # paths:
88 # - "src/**/*.ts"
99 # - "src/**/*.tsx"
1010 # - "src/**/*.js"
1111 # - "src/**/*.jsx"
1212
13+ # A push while a review is still running replaces it, rather than stacking a
14+ # second review of a diff that is already stale.
15+ concurrency :
16+ group : claude-review-${{ github.event.pull_request.number }}
17+ cancel-in-progress : true
18+
1319jobs :
1420 claude-review :
15- # Optional: Filter by PR author
16- # if: |
17- # github.event.pull_request.user.login == 'external-contributor' ||
18- # github.event.pull_request.user.login == 'new-developer' ||
19- # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
21+ # Drafts are reviewed once they are marked ready. Bot PRs (release-please)
22+ # need no review and only queue up as action_required.
23+ if : github.event.pull_request.draft == false && github.event.pull_request.user.type != 'Bot'
2024
2125 runs-on : ubuntu-latest
2226 permissions :
2327 contents : read
24- pull-requests : read
25- issues : read
28+ pull-requests : write
29+ issues : write
2630 id-token : write
31+ checks : read
32+ statuses : read
33+ actions : read # Required for Claude to read CI results on PRs
2734
2835 steps :
2936 - name : Checkout repository
30- uses : actions/checkout@v4
37+ uses : actions/checkout@v7
3138 with :
32- fetch-depth : 1
39+ # Full history, so the review can diff, log and blame against the base
40+ # branch instead of piecing the PR together from `gh pr diff`.
41+ fetch-depth : 0
42+
43+ # - name: Trust workspace directory
44+ # run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
3345
3446 - name : Run Claude Code Review
3547 id : claude-review
3648 uses : anthropics/claude-code-action@v1
3749 with :
3850 claude_code_oauth_token : ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
39- prompt : |
40- REPO: ${{ github.repository }}
41- PR NUMBER: ${{ github.event.pull_request.number }}
42-
43- Please review this pull request and provide feedback on:
44- - Code quality and best practices
45- - Potential bugs or issues
46- - Performance considerations
47- - Security concerns
48- - Test coverage
49-
50- Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.
51-
52- Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
53-
51+ # Print Claude's messages to the job log. Without it a run that stops
52+ # early or skims gives no reason.
53+ show_full_output : true
54+ # anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
55+ # The built-in /code-review, not the code-review@claude-code-plugins
56+ # command. The plugin's procedure (eligibility check, four parallel
57+ # reviewers, validators) was routinely skipped by its Sonnet main loop:
58+ # it would skip the subagents the review steps describe, read a few
59+ # line ranges of a diff too big for one tool result, and post
60+ # "No issues found" after a few seconds. Since Claude Code 2.1.x the
61+ # built-in takes a PR number, an effort level and --comment, and
62+ # verifies its findings before posting them.
63+ prompt : ' /code-review high ${{ github.event.pull_request.number }} --comment'
5464 # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
5565 # or https://code.claude.com/docs/en/cli-reference for available options
56- claude_args : ' --allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'
66+ additional_permissions : |
67+ actions: read
68+ claude_args : |
69+ --allowedTools "Bash(gh:*),Bash(git:*),mcp__github_inline_comment__create_inline_comment"
5770
71+ # The action reports success whenever the SDK returns is_error: false, even
72+ # if Claude never did anything (unresolved slash command, missing plugin,
73+ # empty prompt). That failure mode hid behind months of green runs, so
74+ # assert that work actually happened.
75+ #
76+ # Check cost as well as turns: a real review delegates to subagents and can
77+ # legitimately report num_turns 0 while spending several dollars. Only a run
78+ # that is zero on BOTH counts truly did nothing.
79+ #
80+ # A run that did work but never delegated is the quieter failure: one
81+ # agent skimming the diff and calling it clean. That only warns, because a
82+ # tiny diff may not need subagents.
83+ - name : Assert the review actually ran
84+ if : always() && steps.claude-review.conclusion == 'success'
85+ run : |
86+ file="${{ steps.claude-review.outputs.execution_file }}"
87+ if [ -z "$file" ] || [ ! -f "$file" ]; then
88+ echo "::warning::No execution file — the action exited before starting Claude. Expected on PRs that modify this workflow (the action requires it to match the default branch); investigate otherwise."
89+ exit 0
90+ fi
91+ result=$(jq -r '[.. | objects | select(.type == "result")] | last' "$file")
92+ turns=$(jq -r '.num_turns // 0' <<<"$result")
93+ cost=$(jq -r '.total_cost_usd // 0' <<<"$result")
94+ delegations=$(jq -r '[.. | objects | select(.type == "tool_use" and (.name == "Agent" or .name == "Task" or .name == "Workflow"))] | length' "$file")
95+ echo "Claude turns: $turns, cost: \$$cost, subagent launches: $delegations"
96+ if [ "$turns" -eq 0 ] && [ "$(jq -rn --argjson c "$cost" '$c > 0')" = "false" ]; then
97+ echo "::error::Claude did no work — 0 turns and \$0 spent. The review was a no-op (bad prompt or unresolved slash command)."
98+ exit 1
99+ fi
100+ if [ "$delegations" -eq 0 ]; then
101+ echo "::warning::The review launched no subagents. On anything but a tiny diff that means one agent skimmed it, so treat a clean result with suspicion."
102+ fi
0 commit comments