Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 161 additions & 48 deletions .github/workflows/claude-documentation-reviewer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,37 +10,20 @@
claude-response:
runs-on: ubuntu-latest
permissions:
contents: write
contents: read
pull-requests: write
issues: write
id-token: write
actions: read
steps:
- name: Detect fork
id: pr-info
run: |
if [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then
echo "is_fork=true" >> "$GITHUB_OUTPUT"
else
echo "is_fork=false" >> "$GITHUB_OUTPUT"
fi

- name: Checkout repository (non-fork)
if: steps.pr-info.outputs.is_fork == 'false'
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
fetch-depth: 0

- name: Checkout repository (fork)
if: steps.pr-info.outputs.is_fork == 'true'
- name: Checkout repository
uses: actions/checkout@v4
with:
# Check out by SHA to prevent TOCTOU attacks from forks.
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0

- name: Get changed markdown files

Check failure

Code scanning / CodeQL

Checkout of untrusted code in trusted context

Potential execution of untrusted code on a privileged workflow ([pull_request_target](1))
id: changed-files
run: |
BASE_SHA="${{ github.event.pull_request.base.sha }}"
Expand All @@ -58,18 +41,28 @@
echo "count=$(echo "$CHANGED_MD_FILES" | wc -l | tr -d ' ')" >> "$GITHUB_OUTPUT"
fi

- name: Delete existing review comment
- name: Delete existing review comments
if: steps.changed-files.outputs.count > 0
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_NUMBER=${{ github.event.pull_request.number }}

# Delete existing prose summary comments
COMMENT_IDS=$(gh api repos/${{ github.repository }}/issues/${PR_NUMBER}/comments \
--jq '[.[] | select(.user.login == "github-actions[bot]") | select(.body | startswith("## Documentation Review")) | .id] | .[]' 2>/dev/null || true)
for ID in $COMMENT_IDS; do
gh api repos/${{ github.repository }}/issues/comments/${ID} -X DELETE || true
done

# Dismiss existing inline suggestion reviews
REVIEW_IDS=$(gh api repos/${{ github.repository }}/pulls/${PR_NUMBER}/reviews \
--jq '[.[] | select(.user.login == "github-actions[bot]") | .id] | .[]' 2>/dev/null || true)
for ID in $REVIEW_IDS; do
gh api repos/${{ github.repository }}/pulls/${PR_NUMBER}/reviews/${ID}/dismissals \
-X PUT -f message="Superseded by new review" 2>/dev/null || true
done

- name: Checkout system prompt repository
uses: actions/checkout@v4
with:
Expand All @@ -91,9 +84,9 @@
echo "EOF"
} >> "$GITHUB_OUTPUT"

- name: Review and fix (non-fork)
if: steps.changed-files.outputs.count > 0 && steps.pr-info.outputs.is_fork == 'false'
- name: Review and post issues
if: steps.changed-files.outputs.count > 0
uses: anthropics/claude-code-action@v1

Check warning

Code scanning / CodeQL

Unpinned tag for a non-immutable Action in workflow

Unpinned 3rd party Action 'Documentation Reviewer' step [Uses Step](1) uses 'anthropics/claude-code-action' with ref 'v1', not a pinned commit hash
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
Expand All @@ -105,34 +98,154 @@

Do not review or comment on any other files. Focus exclusively on the documentation changes in the markdown files listed above.

1. Fix all issues directly in the files using the Write and Edit tools.
2. Commit and push the fixes using git.
3. Post a PR comment starting with "## Documentation Review" that lists each issue that was fixed, following the format in your instructions.
1. Fix all issues directly in the files using the Write and Edit tools. Do not commit or push.
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`."

claude_args: |
--model claude-sonnet-4-5-20250929
--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:*)"
--allowedTools "Read,Write,Edit,Bash(gh pr view:*),Bash(gh pr diff:*),Bash(gh pr comment:*)"
--append-system-prompt "${{ steps.read-prompt.outputs.prompt }}"

- name: Review only (fork)
if: steps.changed-files.outputs.count > 0 && steps.pr-info.outputs.is_fork == 'true'
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
show_full_output: true
prompt: |
Review ONLY the following markdown files that were changed in this PR: ${{ steps.changed-files.outputs.files }}

Use `gh pr diff ${{ github.event.pull_request.number }}` to see the exact changes made.

Do not review or comment on any other files. Focus exclusively on the documentation changes in the markdown files listed above.

Post a PR comment starting with "## Documentation Review" that lists all issues found, following the format in your instructions.

This PR is from a fork. Do not attempt to edit files or push changes. The author must address the issues manually.

claude_args: |
--model claude-sonnet-4-5-20250929
--allowedTools "Bash(gh pr view:*),Bash(gh pr diff:*),Bash(gh pr comment:*)"
--append-system-prompt "${{ steps.read-prompt.outputs.prompt }}"
- name: Post inline suggestions
if: steps.changed-files.outputs.count > 0
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
REPO: ${{ github.repository }}
run: |
python3 << 'PYTHON_EOF'
import subprocess
import json
import re
import os
import sys

def parse_diff_to_suggestions(diff_text):
suggestions = []
current_file = None
old_line_num = 0
new_line_num = 0
in_change = False
old_chunk = []
new_chunk = []
change_old_start = 0

for line in diff_text.split('\n'):
if line.startswith('diff --git'):
if in_change and current_file and old_chunk:
s = make_suggestion(current_file, change_old_start, old_chunk, new_chunk)
if s:
suggestions.append(s)
in_change = False
old_chunk = []
new_chunk = []
current_file = None
elif line.startswith('+++ b/'):
current_file = line[6:]
elif line.startswith('@@'):
if in_change and current_file and old_chunk:
s = make_suggestion(current_file, change_old_start, old_chunk, new_chunk)
if s:
suggestions.append(s)
in_change = False
old_chunk = []
new_chunk = []
match = re.match(r'@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@', line)
if match:
old_line_num = int(match.group(1))
new_line_num = int(match.group(2))
elif line.startswith('-') and not line.startswith('---'):
if not in_change:
in_change = True
change_old_start = old_line_num
old_chunk = []
new_chunk = []
old_chunk.append(line[1:])
old_line_num += 1
elif line.startswith('+') and not line.startswith('+++'):
if not in_change:
in_change = True
change_old_start = old_line_num
old_chunk = []
new_chunk = []
new_chunk.append(line[1:])
new_line_num += 1
else:
# Context line or blank
if in_change and current_file and old_chunk:
s = make_suggestion(current_file, change_old_start, old_chunk, new_chunk)
if s:
suggestions.append(s)
in_change = False
old_chunk = []
new_chunk = []
if line.startswith(' '):
old_line_num += 1
new_line_num += 1

# Flush final change
if in_change and current_file and old_chunk:
s = make_suggestion(current_file, change_old_start, old_chunk, new_chunk)
if s:
suggestions.append(s)

return suggestions

def make_suggestion(path, old_start, old_chunk, new_chunk):
if not old_chunk:
return None # Pure insertions cannot be placed as inline suggestions
end_line = old_start + len(old_chunk) - 1
suggestion_body = '```suggestion\n' + '\n'.join(new_chunk) + '\n```'
comment = {
'path': path,
'line': end_line,
'side': 'RIGHT',
'body': suggestion_body,
}
if len(old_chunk) > 1:
comment['start_line'] = old_start
comment['start_side'] = 'RIGHT'
return comment

# Get diff of Claude's local edits vs HEAD
result = subprocess.run(['git', 'diff', 'HEAD'], capture_output=True, text=True)
diff_text = result.stdout

if not diff_text.strip():
print("No changes detected. Skipping inline suggestions.")
sys.exit(0)

suggestions = parse_diff_to_suggestions(diff_text)

if not suggestions:
print("No inline suggestions to post (changes may be pure insertions or deletions).")
sys.exit(0)

print(f"Posting {len(suggestions)} inline suggestion(s)...")

pr_number = os.environ['PR_NUMBER']
head_sha = os.environ['HEAD_SHA']
repo = os.environ['REPO']

review_payload = {
'commit_id': head_sha,
'body': '',
'event': 'COMMENT',
'comments': suggestions,
}

result = subprocess.run(
['gh', 'api', f'repos/{repo}/pulls/{pr_number}/reviews',
'-X', 'POST', '--input', '-'],
input=json.dumps(review_payload),
capture_output=True,
text=True,
)

if result.returncode != 0:
print(f"Error posting inline suggestions: {result.stderr}", file=sys.stderr)
sys.exit(1)

print("Successfully posted inline suggestions.")
PYTHON_EOF
Loading