@@ -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