-
Notifications
You must be signed in to change notification settings - Fork 105
340 lines (303 loc) · 15.2 KB
/
Copy pathclaude-code-review.yml
File metadata and controls
340 lines (303 loc) · 15.2 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
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize, reopened]
paths-ignore:
- '*.md'
- 'docs/**'
- '*.lock'
- 'mkdocs.yml'
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
permissions:
contents: read
pull-requests: write
issues: write
id-token: write
jobs:
# Auto-review on PR open/push
review:
name: Claude PR Review
if: >-
github.event_name == 'pull_request'
&& github.repository == 'lightseekorg/smg'
&& github.actor != 'dependabot[bot]'
&& !github.event.pull_request.head.repo.fork
runs-on: k8s-runner-cpu
timeout-minutes: 30
concurrency:
group: claude-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Export API key from pod env
run: |
echo "::add-mask::${ANTHROPIC_API_KEY}"
echo "ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}" >> "$GITHUB_ENV"
- name: Install gh CLI
run: |
if ! command -v gh &>/dev/null; then
mkdir -p "$HOME/.local/bin"
GH_VERSION="2.74.0"
curl -fsSL "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_amd64.tar.gz" \
| tar xz --strip-components=2 -C "$HOME/.local/bin" "gh_${GH_VERSION}_linux_amd64/bin/gh"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
fi
- name: Record run start time
id: run-start
run: echo "timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
- uses: anthropics/claude-code-action@v1
id: claude-review
continue-on-error: true
with:
anthropic_api_key: ${{ env.ANTHROPIC_API_KEY }}
prompt: |
REPO: ${{ github.repository }}
PR NUMBER: ${{ github.event.pull_request.number }}
EVENT: ${{ github.event.action }}
Review this pull request.
IMPORTANT — Incremental vs full review:
If EVENT is "synchronize" (new push to existing PR), this is a
follow-up review. Previous review comments are your memory.
1. Run `git diff ${{ github.event.before }}..HEAD` to see only what this push changed.
If this fails (e.g., force-push rewrote history), fall back to
`git diff origin/main...HEAD` for a full review instead.
2. Use the comments from step 3 to see what was already flagged.
3. Focus on NEW or CHANGED code in this push. Skip re-reviewing
unchanged code that was already covered.
4. If a previous comment is now resolved by the new push, do not
re-post it.
If EVENT is "opened" or "reopened", do a full review using
`git diff origin/main...HEAD`.
IMPORTANT — Output early:
Always post inline comments BEFORE doing further exploration.
An incomplete review is better than no review because you ran
out of turns.
For every issue found, call mcp__github_inline_comment__create_inline_comment
with path, line, and a severity-prefixed body per REVIEW.md format.
IMPORTANT — Deduplication:
Use the comments fetched in step 3 to deduplicate.
Self-dedup (strict): If claude[bot] already commented on the same
file and line (±3 lines), SKIP — do not post regardless of wording.
Cross-bot awareness (soft): If another bot already commented on the
same file and line (±3 lines), READ their comment. Only post if you
have a genuinely different concern not covered by the existing comment.
Human replies: If a human replied to a bot comment (e.g., "fixed",
"won't fix", "disagree"), respect their response. Do not re-flag
issues that the author has acknowledged or intentionally declined.
IMPORTANT — PR description template check (run this FIRST,
before the code review steps below, on every event):
The PR body must follow .github/PULL_REQUEST_TEMPLATE.md.
Required headers (literal lines, anywhere in body, any order):
## Description
### Problem
### Solution
## Changes
## Test Plan
The trailing <details>Checklist</details> block is template
boilerplate, not content — strip it before computing section
content (see pre-processing below).
Pre-processing: before computing section content, strip from
BODY any <details>...</details> block whose <summary> line,
after trimming, is exactly "Checklist". This is the trailing
block from PULL_REQUEST_TEMPLATE.md and must not count as
content for any section — otherwise an empty Test Plan would
be falsely treated as filled, because there is no later ##
header so the checklist sits inside the Test Plan section.
"Section content" = text between a header and the NEXT ## or ###
header line (or end of body) in the pre-processed BODY. After
stripping all HTML comments (<!-- ... -->, including multi-line)
and trimming whitespace, content must be non-empty. For
"## Description", the Problem and Solution subsections count
as content — Description is filled if both subsections are
filled, even if no prose sits directly under it.
To check:
a. Fetch the body:
BODY=$(gh pr view ${{ github.event.pull_request.number }} \
--repo ${{ github.repository }} --json body --jq .body)
b. Verify each required header appears in BODY.
c. For each of Problem, Solution, Changes, Test Plan, verify the
section is filled per the definition above.
d. Build a bulleted gap list, e.g.
- Missing header: `## Test Plan`
- Empty section: `### Problem` (only contains HTML comments)
Dedup by a hidden marker on a claude[bot] top-level (issue) comment:
EXISTING=$(gh api \
repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments \
--paginate \
--jq '.[] | select(.user.login == "claude[bot]" and (.body | startswith("<!-- claude-pr-template-check -->"))) | .id' \
| head -1)
Cases:
gaps + no EXISTING → post a new comment:
gh pr comment ${{ github.event.pull_request.number }} \
--repo ${{ github.repository }} --body "$MSG"
gaps + EXISTING → update in place (never post a second one):
gh api repos/${{ github.repository }}/issues/comments/$EXISTING \
-X PATCH -f body="$MSG"
no gaps + EXISTING → delete on green:
gh api repos/${{ github.repository }}/issues/comments/$EXISTING -X DELETE
no gaps + no EXISTING → do nothing.
$MSG body (verbatim, with the gap list from step d substituted in):
<!-- claude-pr-template-check -->
👋 The PR description doesn't fully follow
[PULL_REQUEST_TEMPLATE.md](https://github.com/${{ github.repository }}/blob/main/.github/PULL_REQUEST_TEMPLATE.md):
<bulleted gap list from step d>
Please update the PR description so reviewers have the context they need.
Finish this check (post/update/delete) before moving on, so a
turn-limit cutoff during code review doesn't lose the template check.
Steps (do steps 1-3 in parallel):
1. Run the appropriate git diff (see incremental review rules above).
2. Read REVIEW.md for severity format, focus areas, and domain knowledge.
3. Fetch existing review comments (bot + human replies) for dedup and memory:
gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/comments \
--paginate --jq '.[] | {id, path, line, body, user: .user.login, in_reply_to_id}' \
| jq -s '.[-50:]'
4. Read changed files for context.
5. Post inline comments for issues found.
6. If EVENT is "opened" or "reopened" (full review) AND no 🔴
Important issues were found, submit an APPROVE review:
gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews \
-f event=APPROVE
Do NOT approve on "synchronize" (incremental) reviews — only
full reviews have enough context to approve.
claude_args: |
--model claude-opus-4-6
--max-turns 50
--allowedTools "Read,Glob,Grep,Bash,Agent,Skill,ToolSearch,TaskCreate,TaskUpdate,TaskGet,mcp__github_inline_comment__create_inline_comment"
show_full_output: true
plugins: "pr-review-toolkit@claude-plugins-official"
plugin_marketplaces: |
https://github.com/anthropics/claude-plugins-official.git
https://github.com/lightseekorg/smg-dev-guide.git
- name: Annotate on failure
if: steps.claude-review.outcome == 'failure'
run: echo "::warning::Claude review did not complete — may have hit turn limit or encountered an error."
- name: Review summary
if: always()
env:
GH_TOKEN: ${{ github.token }}
run: |
LOG="${RUNNER_TEMP}/claude-execution-output.json"
# Extract usage stats from execution output
# The file may be a JSON array of events — the result entry is last
if [ -f "$LOG" ]; then
RESULT=$(jq 'if type == "array" then .[-1] else . end' "$LOG")
COST=$(echo "$RESULT" | jq -r '.total_cost_usd // 0')
TURNS=$(echo "$RESULT" | jq -r '.num_turns // 0')
DURATION=$(echo "$RESULT" | jq -r '(.duration_ms // 0) / 1000 | floor')
INPUT=$(echo "$RESULT" | jq -r '.usage.input_tokens // 0')
OUTPUT=$(echo "$RESULT" | jq -r '.usage.output_tokens // 0')
CACHE_READ=$(echo "$RESULT" | jq -r '.usage.cache_read_input_tokens // 0')
CACHE_CREATE=$(echo "$RESULT" | jq -r '.usage.cache_creation_input_tokens // 0')
STATUS=$(echo "$RESULT" | jq -r '.terminal_reason // "unknown"')
else
COST=0; TURNS=0; DURATION=0; INPUT=0; OUTPUT=0
CACHE_READ=0; CACHE_CREATE=0; STATUS="no output"
fi
# Fetch inline comments posted by claude in THIS run only
RUN_START="${{ steps.run-start.outputs.timestamp }}"
FETCH_OK=true
if [ -z "$RUN_START" ]; then
FETCH_OK=false
COMMENTS="[]"
else
if COMMENTS=$(set -o pipefail; gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/comments \
--paginate --jq ".[] | select((.user.login == \"claude[bot]\" or .user.login == \"github-actions[bot]\") and .created_at >= \"${RUN_START}\") | {path, line, body}" \
| jq -s '.' 2>/dev/null); then
: # fetch succeeded
else
FETCH_OK=false
COMMENTS="[]"
fi
fi
IMPORTANT=$(echo "$COMMENTS" | jq '[.[] | select(.body | test("🔴"))] | length')
NIT=$(echo "$COMMENTS" | jq '[.[] | select(.body | test("🟡"))] | length')
PREEXISTING=$(echo "$COMMENTS" | jq '[.[] | select(.body | test("🟣"))] | length')
TOTAL=$(echo "$COMMENTS" | jq 'length')
# Build one-line summaries per finding
FINDINGS=$(echo "$COMMENTS" | jq -r '.[] | "| `\(.path):\(.line)` | \(.body | split("\n")[0] | gsub("\\|"; "∣")) |"')
# Write step summary
{
echo "## Claude PR Review Summary"
echo ""
if [ "$STATUS" = "completed" ]; then
echo "✅ **Status:** Completed in ${TURNS} turns (${DURATION}s)"
else
echo "⚠️ **Status:** ${STATUS} after ${TURNS} turns (${DURATION}s)"
fi
echo ""
echo "### Findings"
echo ""
if [ "$FETCH_OK" = "false" ]; then
echo "⚠️ Summary unavailable — comment fetch failed or run-start timestamp missing."
elif [ "$TOTAL" -eq 0 ]; then
echo "No issues found."
else
echo "| Severity | Count |"
echo "|----------|-------|"
echo "| 🔴 Important | ${IMPORTANT} |"
echo "| 🟡 Nit | ${NIT} |"
echo "| 🟣 Pre-existing | ${PREEXISTING} |"
echo "| **Total** | **${TOTAL}** |"
echo ""
echo "| Location | Finding |"
echo "|----------|---------|"
echo "$FINDINGS"
fi
echo ""
echo "### Usage"
echo ""
echo "| Metric | Value |"
echo "|--------|-------|"
echo "| Cost | \$${COST} |"
echo "| Input tokens | ${INPUT} |"
echo "| Output tokens | ${OUTPUT} |"
echo "| Cache read | ${CACHE_READ} |"
echo "| Cache creation | ${CACHE_CREATE} |"
} >> "$GITHUB_STEP_SUMMARY"
# Respond to @claude mentions in PR/issue comments
respond:
name: Claude Respond
if: >-
(github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment')
&& contains(github.event.comment.body, '@claude')
&& github.repository == 'lightseekorg/smg'
runs-on: k8s-runner-cpu
timeout-minutes: 30
concurrency:
group: claude-respond-${{ github.event.issue.number || github.event.pull_request.number }}
cancel-in-progress: false
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Export API key from pod env
run: |
echo "::add-mask::${ANTHROPIC_API_KEY}"
echo "ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}" >> "$GITHUB_ENV"
- name: Install gh CLI
run: |
if ! command -v gh &>/dev/null; then
mkdir -p "$HOME/.local/bin"
GH_VERSION="2.74.0"
curl -fsSL "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_amd64.tar.gz" \
| tar xz --strip-components=2 -C "$HOME/.local/bin" "gh_${GH_VERSION}_linux_amd64/bin/gh"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
fi
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ env.ANTHROPIC_API_KEY }}
claude_args: |
--model claude-opus-4-6
--max-turns 30
--allowedTools "Read,Glob,Grep,Bash,Agent,Skill,ToolSearch,TaskCreate,TaskUpdate,TaskGet,mcp__github_inline_comment__create_inline_comment"
plugins: "pr-review-toolkit@claude-plugins-official"
plugin_marketplaces: |
https://github.com/anthropics/claude-plugins-official.git
https://github.com/lightseekorg/smg-dev-guide.git