@@ -10,30 +10,13 @@ jobs:
1010 claude-response :
1111 runs-on : ubuntu-latest
1212 permissions :
13- contents : write
13+ contents : read
1414 pull-requests : write
1515 issues : write
1616 id-token : write
1717 actions : read
1818 steps :
19- - name : Detect fork
20- id : pr-info
21- run : |
22- if [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then
23- echo "is_fork=true" >> "$GITHUB_OUTPUT"
24- else
25- echo "is_fork=false" >> "$GITHUB_OUTPUT"
26- fi
27-
28- - name : Checkout repository (non-fork)
29- if : steps.pr-info.outputs.is_fork == 'false'
30- uses : actions/checkout@v4
31- with :
32- ref : ${{ github.event.pull_request.head.ref }}
33- fetch-depth : 0
34-
35- - name : Checkout repository (fork)
36- if : steps.pr-info.outputs.is_fork == 'true'
19+ - name : Checkout repository
3720 uses : actions/checkout@v4
3821 with :
3922 # Check out by SHA to prevent TOCTOU attacks from forks.
@@ -58,18 +41,28 @@ jobs:
5841 echo "count=$(echo "$CHANGED_MD_FILES" | wc -l | tr -d ' ')" >> "$GITHUB_OUTPUT"
5942 fi
6043
61- - name : Delete existing review comment
44+ - name : Delete existing review comments
6245 if : steps.changed-files.outputs.count > 0
6346 env :
6447 GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
6548 run : |
6649 PR_NUMBER=${{ github.event.pull_request.number }}
50+
51+ # Delete existing prose summary comments
6752 COMMENT_IDS=$(gh api repos/${{ github.repository }}/issues/${PR_NUMBER}/comments \
6853 --jq '[.[] | select(.user.login == "github-actions[bot]") | select(.body | startswith("## Documentation Review")) | .id] | .[]' 2>/dev/null || true)
6954 for ID in $COMMENT_IDS; do
7055 gh api repos/${{ github.repository }}/issues/comments/${ID} -X DELETE || true
7156 done
7257
58+ # Dismiss existing inline suggestion reviews
59+ REVIEW_IDS=$(gh api repos/${{ github.repository }}/pulls/${PR_NUMBER}/reviews \
60+ --jq '[.[] | select(.user.login == "github-actions[bot]") | .id] | .[]' 2>/dev/null || true)
61+ for ID in $REVIEW_IDS; do
62+ gh api repos/${{ github.repository }}/pulls/${PR_NUMBER}/reviews/${ID}/dismissals \
63+ -X PUT -f message="Superseded by new review" 2>/dev/null || true
64+ done
65+
7366 - name : Checkout system prompt repository
7467 uses : actions/checkout@v4
7568 with :
9184 echo "EOF"
9285 } >> "$GITHUB_OUTPUT"
9386
94- - name : Review and fix (non-fork)
95- if : steps.changed-files.outputs.count > 0 && steps.pr-info.outputs.is_fork == 'false'
87+ - name : Review and post issues
88+ if : steps.changed-files.outputs.count > 0
9689 uses : anthropics/claude-code-action@v1
9790 with :
9891 anthropic_api_key : ${{ secrets.ANTHROPIC_API_KEY }}
@@ -105,34 +98,154 @@ jobs:
10598
10699 Do not review or comment on any other files. Focus exclusively on the documentation changes in the markdown files listed above.
107100
108- 1. Fix all issues directly in the files using the Write and Edit tools.
109- 2. Commit and push the fixes using git.
110- 3. Post a PR comment starting with "## Documentation Review" that lists each issue that was fixed, following the format in your instructions.
101+ 1. Fix all issues directly in the files using the Write and Edit tools. Do not commit or push.
102+ 2. Post a PR comment starting with "## Documentation Review" that lists each issue found and fixed, following the format in your instructions. End the comment with a blank line followed by: "To apply all fixes at once, reply with `@claude apply all fixes`."
111103
112104 claude_args : |
113105 --model claude-sonnet-4-5-20250929
114- --allowedTools "Read,Write,Edit,Bash(gh pr view:*),Bash(gh pr diff:*),Bash(gh pr comment:*),Bash(git config:*),Bash(git add:*),Bash(git commit:*),Bash(git push:*),Bash(git status:*),Bash(git diff:*) "
106+ --allowedTools "Read,Write,Edit,Bash(gh pr view:*),Bash(gh pr diff:*),Bash(gh pr comment:*)"
115107 --append-system-prompt "${{ steps.read-prompt.outputs.prompt }}"
116108
117- - name : Review only (fork)
118- if : steps.changed-files.outputs.count > 0 && steps.pr-info.outputs.is_fork == 'true'
119- uses : anthropics/claude-code-action@v1
120- with :
121- anthropic_api_key : ${{ secrets.ANTHROPIC_API_KEY }}
122- github_token : ${{ secrets.GITHUB_TOKEN }}
123- show_full_output : true
124- prompt : |
125- Review ONLY the following markdown files that were changed in this PR: ${{ steps.changed-files.outputs.files }}
126-
127- Use `gh pr diff ${{ github.event.pull_request.number }}` to see the exact changes made.
128-
129- Do not review or comment on any other files. Focus exclusively on the documentation changes in the markdown files listed above.
130-
131- Post a PR comment starting with "## Documentation Review" that lists all issues found, following the format in your instructions.
132-
133- This PR is from a fork. Do not attempt to edit files or push changes. The author must address the issues manually.
134-
135- claude_args : |
136- --model claude-sonnet-4-5-20250929
137- --allowedTools "Bash(gh pr view:*),Bash(gh pr diff:*),Bash(gh pr comment:*)"
138- --append-system-prompt "${{ steps.read-prompt.outputs.prompt }}"
109+ - name : Post inline suggestions
110+ if : steps.changed-files.outputs.count > 0
111+ env :
112+ GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
113+ PR_NUMBER : ${{ github.event.pull_request.number }}
114+ HEAD_SHA : ${{ github.event.pull_request.head.sha }}
115+ REPO : ${{ github.repository }}
116+ run : |
117+ python3 << 'PYTHON_EOF'
118+ import subprocess
119+ import json
120+ import re
121+ import os
122+ import sys
123+
124+ def parse_diff_to_suggestions(diff_text):
125+ suggestions = []
126+ current_file = None
127+ old_line_num = 0
128+ new_line_num = 0
129+ in_change = False
130+ old_chunk = []
131+ new_chunk = []
132+ change_old_start = 0
133+
134+ for line in diff_text.split('\n'):
135+ if line.startswith('diff --git'):
136+ if in_change and current_file and old_chunk:
137+ s = make_suggestion(current_file, change_old_start, old_chunk, new_chunk)
138+ if s:
139+ suggestions.append(s)
140+ in_change = False
141+ old_chunk = []
142+ new_chunk = []
143+ current_file = None
144+ elif line.startswith('+++ b/'):
145+ current_file = line[6:]
146+ elif line.startswith('@@'):
147+ if in_change and current_file and old_chunk:
148+ s = make_suggestion(current_file, change_old_start, old_chunk, new_chunk)
149+ if s:
150+ suggestions.append(s)
151+ in_change = False
152+ old_chunk = []
153+ new_chunk = []
154+ match = re.match(r'@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@', line)
155+ if match:
156+ old_line_num = int(match.group(1))
157+ new_line_num = int(match.group(2))
158+ elif line.startswith('-') and not line.startswith('---'):
159+ if not in_change:
160+ in_change = True
161+ change_old_start = old_line_num
162+ old_chunk = []
163+ new_chunk = []
164+ old_chunk.append(line[1:])
165+ old_line_num += 1
166+ elif line.startswith('+') and not line.startswith('+++'):
167+ if not in_change:
168+ in_change = True
169+ change_old_start = old_line_num
170+ old_chunk = []
171+ new_chunk = []
172+ new_chunk.append(line[1:])
173+ new_line_num += 1
174+ else:
175+ # Context line or blank
176+ if in_change and current_file and old_chunk:
177+ s = make_suggestion(current_file, change_old_start, old_chunk, new_chunk)
178+ if s:
179+ suggestions.append(s)
180+ in_change = False
181+ old_chunk = []
182+ new_chunk = []
183+ if line.startswith(' '):
184+ old_line_num += 1
185+ new_line_num += 1
186+
187+ # Flush final change
188+ if in_change and current_file and old_chunk:
189+ s = make_suggestion(current_file, change_old_start, old_chunk, new_chunk)
190+ if s:
191+ suggestions.append(s)
192+
193+ return suggestions
194+
195+ def make_suggestion(path, old_start, old_chunk, new_chunk):
196+ if not old_chunk:
197+ return None # Pure insertions cannot be placed as inline suggestions
198+ end_line = old_start + len(old_chunk) - 1
199+ suggestion_body = '```suggestion\n' + '\n'.join(new_chunk) + '\n```'
200+ comment = {
201+ 'path': path,
202+ 'line': end_line,
203+ 'side': 'RIGHT',
204+ 'body': suggestion_body,
205+ }
206+ if len(old_chunk) > 1:
207+ comment['start_line'] = old_start
208+ comment['start_side'] = 'RIGHT'
209+ return comment
210+
211+ # Get diff of Claude's local edits vs HEAD
212+ result = subprocess.run(['git', 'diff', 'HEAD'], capture_output=True, text=True)
213+ diff_text = result.stdout
214+
215+ if not diff_text.strip():
216+ print("No changes detected. Skipping inline suggestions.")
217+ sys.exit(0)
218+
219+ suggestions = parse_diff_to_suggestions(diff_text)
220+
221+ if not suggestions:
222+ print("No inline suggestions to post (changes may be pure insertions or deletions).")
223+ sys.exit(0)
224+
225+ print(f"Posting {len(suggestions)} inline suggestion(s)...")
226+
227+ pr_number = os.environ['PR_NUMBER']
228+ head_sha = os.environ['HEAD_SHA']
229+ repo = os.environ['REPO']
230+
231+ review_payload = {
232+ 'commit_id': head_sha,
233+ 'body': '',
234+ 'event': 'COMMENT',
235+ 'comments': suggestions,
236+ }
237+
238+ result = subprocess.run(
239+ ['gh', 'api', f'repos/{repo}/pulls/{pr_number}/reviews',
240+ '-X', 'POST', '--input', '-'],
241+ input=json.dumps(review_payload),
242+ capture_output=True,
243+ text=True,
244+ )
245+
246+ if result.returncode != 0:
247+ print(f"Error posting inline suggestions: {result.stderr}", file=sys.stderr)
248+ sys.exit(1)
249+
250+ print("Successfully posted inline suggestions.")
251+ PYTHON_EOF
0 commit comments