Skip to content

Commit 451ca39

Browse files
committed
Refactor Claude code review for deterministic pre/post-processing
Move deterministic operations out of Claude's scope: Pre-fetch (before Claude runs): - Review threads with thread IDs, resolution status, comments - Previous top-level comments from Claude - Review context (commits since last push, history rewritten) - PR metadata (title, body, branches, author) - Full PR diff Post-process (after Claude runs): - Resolve threads from /tmp/threads-to-resolve.txt - Submit replies from /tmp/thread-replies.json Claude now reads pre-fetched files instead of executing GraphQL queries, and writes thread actions to temp files for deterministic execution by subsequent workflow steps.
1 parent a52e23a commit 451ca39

File tree

1 file changed

+206
-96
lines changed

1 file changed

+206
-96
lines changed

.github/workflows/claude-code-review.yml

Lines changed: 206 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,115 @@ jobs:
3333
with:
3434
fetch-depth: 1
3535

36+
- name: Fetch review threads
37+
env:
38+
GH_TOKEN: ${{ github.token }}
39+
run: |
40+
# Pre-fetch review threads so Claude doesn't have to execute GraphQL
41+
gh api graphql -f query='
42+
query($owner: String!, $name: String!, $pr: Int!) {
43+
repository(owner: $owner, name: $name) {
44+
pullRequest(number: $pr) {
45+
reviewThreads(first: 100) {
46+
nodes {
47+
id
48+
isResolved
49+
path
50+
line
51+
comments(first: 10) {
52+
nodes {
53+
id
54+
databaseId
55+
body
56+
author { login }
57+
}
58+
}
59+
}
60+
}
61+
}
62+
}
63+
}' \
64+
-f owner="${{ github.repository_owner }}" \
65+
-f name="${{ github.event.repository.name }}" \
66+
-F pr=${{ github.event.pull_request.number }} \
67+
| jq '[.data.repository.pullRequest.reviewThreads.nodes[] |
68+
select(.comments.nodes[0].author.login == "claude" or .comments.nodes[0].author.login == "github-actions[bot]") |
69+
{
70+
threadId: .id,
71+
isResolved: .isResolved,
72+
path: .path,
73+
line: .line,
74+
firstCommentId: .comments.nodes[0].databaseId,
75+
firstCommentBody: .comments.nodes[0].body,
76+
allComments: [.comments.nodes[] | {author: .author.login, body: .body}]
77+
}]' > /tmp/review-threads.json
78+
79+
echo "Found $(jq length /tmp/review-threads.json) Claude review threads"
80+
cat /tmp/review-threads.json
81+
82+
- name: Fetch previous review context
83+
env:
84+
GH_TOKEN: ${{ github.token }}
85+
run: |
86+
# Pre-fetch previous Claude comments
87+
gh pr view ${{ github.event.pull_request.number }} --json comments \
88+
--jq '[.comments[] | select(.author.login == "claude" or .author.login == "github-actions[bot]") | {author: .author.login, body: .body, createdAt: .createdAt}]' \
89+
> /tmp/previous-comments.json
90+
91+
# Get commits since last push (for incremental reviews)
92+
BEFORE="${{ github.event.before }}"
93+
AFTER="${{ github.event.after }}"
94+
95+
# Check if this is the first push (before will be all zeros)
96+
if [ "$BEFORE" = "0000000000000000000000000000000000000000" ]; then
97+
echo '{"isFirstPush": true, "commits": [], "historyRewritten": false}' > /tmp/review-context.json
98+
else
99+
# Check if history was rewritten (before commit no longer exists)
100+
if git cat-file -t "$BEFORE" 2>/dev/null; then
101+
HISTORY_REWRITTEN="false"
102+
COMMITS=$(git log --oneline "$BEFORE".."$AFTER" 2>/dev/null | head -20 | jq -R -s 'split("\n") | map(select(length > 0))')
103+
else
104+
HISTORY_REWRITTEN="true"
105+
COMMITS="[]"
106+
fi
107+
108+
jq -n \
109+
--argjson historyRewritten "$HISTORY_REWRITTEN" \
110+
--argjson commits "$COMMITS" \
111+
'{isFirstPush: false, commits: $commits, historyRewritten: $historyRewritten}' \
112+
> /tmp/review-context.json
113+
fi
114+
115+
echo "Previous comments: $(jq length /tmp/previous-comments.json)"
116+
echo "Review context:"
117+
cat /tmp/review-context.json
118+
119+
- name: Fetch PR metadata
120+
env:
121+
GH_TOKEN: ${{ github.token }}
122+
run: |
123+
# Pre-fetch PR metadata so Claude doesn't need to query it
124+
gh pr view ${{ github.event.pull_request.number }} \
125+
--json title,body,baseRefName,headRefName,author \
126+
--jq '{
127+
title: .title,
128+
body: .body,
129+
baseBranch: .baseRefName,
130+
headBranch: .headRefName,
131+
author: .author.login
132+
}' > /tmp/pr-metadata.json
133+
134+
echo "PR metadata:"
135+
cat /tmp/pr-metadata.json
136+
137+
- name: Fetch PR diff
138+
env:
139+
GH_TOKEN: ${{ github.token }}
140+
run: |
141+
# Pre-fetch the full PR diff
142+
gh pr diff ${{ github.event.pull_request.number }} > /tmp/pr-diff.patch
143+
echo "PR diff: $(wc -l < /tmp/pr-diff.patch) lines"
144+
36145
- name: Run Claude Code Review
37146
id: claude-review
38147
uses: anthropics/claude-code-action@v1
@@ -63,22 +172,35 @@ jobs:
63172
64173
### 2. Check for Previous Review (Internal Context Only)
65174
66-
Check if you've reviewed this PR before:
175+
Your previous feedback has been pre-fetched in two places:
176+
177+
**Top-level review comments** (your main "## Claude Code Review" posts):
178+
```bash
179+
cat /tmp/previous-comments.json
180+
```
181+
182+
**Inline code comments** (your comments on specific lines - also in step 6):
67183
```bash
68-
gh pr view ${{ github.event.pull_request.number }} --json comments --jq '.comments[] | select(.author.login == "claude[bot]" or .author.login == "github-actions[bot]")'
184+
cat /tmp/review-threads.json
69185
```
70186
71-
If previous review exists:
72-
- Read it to understand what you said before
73-
- Note what commits have been added since: `git log --oneline ${{ github.event.before }}..${{ github.event.after }}`
74-
- Check if history was rewritten: `git cat-file -t ${{ github.event.before }} 2>/dev/null`
75-
- Use this context internally when writing your review
187+
**What changed since your last review:**
188+
```bash
189+
cat /tmp/review-context.json
190+
```
76191
77-
**Don't announce "Update N" or link to previous reviews** - just write naturally.
192+
Read all three to understand what you've already said and what's changed.
193+
**Don't announce "Update N"** - just write naturally, like continuing a conversation.
78194
79195
### 3. Gather Context
80-
- Use `gh pr view ${{ github.event.pull_request.number }}` for PR description
81-
- Use `gh pr diff ${{ github.event.pull_request.number }}` for FULL diff (not just incremental)
196+
197+
PR metadata and diff have been pre-fetched. Read these files:
198+
```bash
199+
cat /tmp/pr-metadata.json # title, body, baseBranch, headBranch, author
200+
cat /tmp/pr-diff.patch # Full PR diff
201+
```
202+
203+
Also:
82204
- Read CLAUDE.md for repo-specific conventions and architecture patterns
83205
- Use Read tool to examine key changed files
84206
@@ -97,52 +219,15 @@ jobs:
97219
d. What tests are needed?
98220
e. Does this need documentation updates?
99221
100-
### 6. Fetch Existing Review Threads
101-
102-
Before creating your review, fetch existing Claude review threads to enable thread continuity:
103-
104-
```bash
105-
# Get PR owner and repo name
106-
OWNER=$(gh repo view --json owner --jq '.owner.login')
107-
REPO=$(gh repo view --json name --jq '.name')
108-
109-
# Fetch all review threads
110-
gh api graphql -f query='
111-
query($owner: String!, $name: String!, $pr: Int!) {
112-
repository(owner: $owner, name: $name) {
113-
pullRequest(number: $pr) {
114-
reviewThreads(first: 100) {
115-
nodes {
116-
id
117-
isResolved
118-
path
119-
line
120-
comments(first: 10) {
121-
nodes {
122-
id
123-
databaseId
124-
body
125-
author { login }
126-
}
127-
}
128-
}
129-
}
130-
}
131-
}
132-
}' -f owner="$OWNER" -f name="$REPO" -F pr=${{ github.event.pull_request.number }}
133-
```
222+
### 6. Decide What to Do With Your Previous Inline Comments
134223
135-
**Parse the response** - structure is:
136-
```
137-
.data.repository.pullRequest.reviewThreads.nodes[]
138-
```
224+
You already read `/tmp/review-threads.json` in step 2. Now decide for each thread: was it fixed, or does it need follow-up?
139225
140-
Filter for threads where `comments.nodes[0].author.login` is "claude" or "github-actions[bot]". These are your previous review comments.
226+
This decision informs your new review - mention progress naturally:
227+
- "The encoding issue is fixed, thanks!"
228+
- "Still seeing the over-mocking - I'll follow up on that thread."
141229
142-
**Important notes:**
143-
- `line` can be `null` for file-level comments (not line-specific)
144-
- Use `id` (format: PRRT_xxx) for resolveReviewThread mutations
145-
- Use `databaseId` (format: 2518555173) for comment replies
230+
Don't duplicate thread content in your new review. Reference it instead.
146231
147232
### 7. Execute Review
148233
@@ -167,75 +252,100 @@ jobs:
167252
- **Inline comments**: Use for specific line-by-line code issues (file path, line number, comment)
168253
- **Main comment**: Summary only - overall assessment, architectural concerns, testing strategy, verdict. Do NOT duplicate inline comments here.
169254
170-
**Main comment - write naturally:**
255+
**Main comment - write like a human reviewer:**
171256
- Start with "## Claude Code Review"
172-
- If previous review exists, write conversationally: "The encoding issue is fixed. Tests look better too."
173-
- Mention what still needs work: "I still see the over-mocking in file-service.test.ts:45"
174-
- New concerns: "New issue: error handling in container.ts doesn't cover..."
175-
- Overall verdict: "Looks good" or "Needs fixes before merge"
176-
177-
**Handle existing threads before submitting new review:**
257+
- Acknowledge progress on previous feedback: "The encoding issue is fixed, nice!"
258+
- Reference ongoing threads: "Still discussing the mocking approach in that thread."
259+
- Raise new concerns: "New issue: error handling in container.ts doesn't cover..."
260+
- Give a clear verdict: "Looks good to merge" or "A few things to address first"
178261
179-
For each thread from step 6, compare with your new findings:
180-
- **Issue is fixed:** Mark for resolution (save thread ID)
181-
- **Issue persists but needs update:** Mark for reply (save comment database ID)
182-
- **Issue no longer relevant:** Mark for resolution
262+
**Step 1: Submit your new review**
183263
184-
**Submit review as a single cohesive unit:**
185-
186-
1. Get the latest commit SHA:
187-
```bash
188-
COMMIT_SHA=$(gh pr view ${{ github.event.pull_request.number }} --json headRefOid --jq '.headRefOid')
189-
```
190-
191-
2. Create review.json with ONLY NEW issues (don't duplicate existing thread issues):
264+
Create review.json with your main comment and any NEW inline comments (don't re-comment on existing threads):
192265
```json
193266
{
194-
"body": "## Claude Code Review\n\nYour overall assessment here...",
267+
"body": "## Claude Code Review\n\nYour overall assessment...",
195268
"event": "COMMENT",
196-
"commit_id": "COMMIT_SHA_HERE",
269+
"commit_id": "${{ github.event.pull_request.head.sha }}",
197270
"comments": [
198-
{
199-
"path": "path/to/file.ts",
200-
"line": 42,
201-
"body": "Your inline comment here"
202-
}
271+
{"path": "path/to/file.ts", "line": 42, "body": "New issue here"}
203272
]
204273
}
205274
```
206275
207-
3. Post the unified review:
276+
Post it:
208277
```bash
209278
gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews \
210279
--method POST \
211280
--input review.json
212281
```
213282
214-
4. **Manage existing threads (after review submission):**
283+
**Step 2: Handle your existing inline threads**
284+
285+
For each thread from step 6, decide: is the issue fixed or does it need follow-up?
215286
216-
Resolve fixed issues:
287+
**If fixed** → resolve it (write thread ID to file):
217288
```bash
218-
gh api graphql -f query='
219-
mutation($threadId: ID!) {
220-
resolveReviewThread(input: {threadId: $threadId}) {
221-
thread { id isResolved }
222-
}
223-
}' -f threadId="THREAD_ID_FROM_STEP_6"
289+
echo "PRRT_xxx" >> /tmp/threads-to-resolve.txt
224290
```
225291
226-
Reply to threads that need updates:
292+
**If needs follow-up** → reply to continue the conversation (write to JSON):
227293
```bash
228-
gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/comments/COMMENT_DATABASE_ID/replies \
229-
--method POST \
230-
-f body="Update: Issue has been partially addressed but..."
294+
echo '[{"commentId": 123456789, "body": "Still seeing this issue because..."}]' > /tmp/thread-replies.json
231295
```
232296
233-
**Important**: This workflow maintains conversation continuity - resolved issues show as "Resolved", ongoing issues have threaded replies, and only new issues create new comment threads.
234-
235-
Always post a NEW comment - never update previous ones. Natural conversation flow.
297+
You can also reply AND resolve (e.g., "Fixed, thanks!" then resolve).
298+
Subsequent workflow steps will submit these automatically.
236299
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
237300
# or https://docs.claude.com/en/docs/claude-code/cli-reference for available options
238-
claude_args: '--allowedTools "Task,Skill,Read,Glob,Grep,Write,TodoWrite,mcp__cloudflare-docs__search_cloudflare_documentation,mcp__exa__get_code_context_exa,mcp__exa__web_search_exa,Bash(gh pr view:*),Bash(gh pr diff:*),Bash(gh repo view:*),Bash(gh api:*),Bash(git log:*),Bash(git cat-file:*),Bash(git rev-parse:*),Bash(jq:*)"'
301+
claude_args: '--allowedTools "Task,Skill,Read,Glob,Grep,Write,TodoWrite,mcp__cloudflare-docs__search_cloudflare_documentation,mcp__exa__get_code_context_exa,mcp__exa__web_search_exa,Bash(gh api:*),Bash(gh pr checks:*),Bash(gh issue view:*),Bash(git log:*),Bash(git show:*),Bash(git blame:*),Bash(jq:*),Bash(echo:*),Bash(cat:*),Bash(mv:*)"'
302+
303+
- name: Resolve review threads
304+
env:
305+
GH_TOKEN: ${{ github.token }}
306+
run: |
307+
# Resolve threads that Claude marked for resolution
308+
if [ -f /tmp/threads-to-resolve.txt ]; then
309+
while read -r thread_id; do
310+
if [ -n "$thread_id" ]; then
311+
echo "Resolving thread: $thread_id"
312+
gh api graphql -f query='
313+
mutation($threadId: ID!) {
314+
resolveReviewThread(input: {threadId: $threadId}) {
315+
thread { id isResolved }
316+
}
317+
}' -f threadId="$thread_id" || echo "Failed to resolve $thread_id (may already be resolved)"
318+
fi
319+
done < /tmp/threads-to-resolve.txt
320+
echo "Done resolving $(wc -l < /tmp/threads-to-resolve.txt | tr -d ' ') threads"
321+
else
322+
echo "No threads to resolve"
323+
fi
324+
325+
- name: Submit thread replies
326+
env:
327+
GH_TOKEN: ${{ github.token }}
328+
run: |
329+
# Submit replies that Claude wrote to thread-replies.json
330+
if [ -f /tmp/thread-replies.json ]; then
331+
REPLY_COUNT=$(jq length /tmp/thread-replies.json)
332+
if [ "$REPLY_COUNT" -gt 0 ]; then
333+
echo "Submitting $REPLY_COUNT thread replies"
334+
jq -c '.[]' /tmp/thread-replies.json | while read -r reply; do
335+
COMMENT_ID=$(echo "$reply" | jq -r '.commentId')
336+
BODY=$(echo "$reply" | jq -r '.body')
337+
echo "Replying to comment $COMMENT_ID"
338+
gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/comments/"$COMMENT_ID"/replies \
339+
--method POST \
340+
-f body="$BODY" || echo "Failed to reply to $COMMENT_ID"
341+
done
342+
echo "Done submitting replies"
343+
else
344+
echo "No replies to submit"
345+
fi
346+
else
347+
echo "No thread replies file found"
348+
fi
239349
240350
- name: Minimize outdated reviews
241351
env:

0 commit comments

Comments
 (0)