Skip to content

Commit 6336a8c

Browse files
committed
Added Claude review workflow
1 parent 16f2070 commit 6336a8c

1 file changed

Lines changed: 133 additions & 0 deletions

File tree

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
name: Claude PR Review
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, edited]
6+
workflow_dispatch:
7+
8+
jobs:
9+
claude_review:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- name: Checkout code
14+
uses: actions/checkout@v4
15+
16+
- name: Setup Python
17+
uses: actions/setup-python@v4
18+
with:
19+
python-version: "3.11"
20+
21+
- name: Install dependencies
22+
run: |
23+
python -m pip install --upgrade pip
24+
pip install anthropic PyGithub unidiff requests
25+
26+
- name: Run Claude Review
27+
env:
28+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
29+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30+
PR_NUMBER: ${{ github.event.pull_request.number }}
31+
REPO_NAME: ${{ github.repository }}
32+
run: |
33+
python - <<'EOF'
34+
import os
35+
import requests
36+
import anthropic
37+
from github import Github
38+
from unidiff import PatchSet
39+
import textwrap
40+
41+
# Environment
42+
API_KEY = os.environ["ANTHROPIC_API_KEY"]
43+
GH_TOKEN = os.environ["GITHUB_TOKEN"]
44+
PR_NUMBER = int(os.environ["PR_NUMBER"])
45+
REPO_NAME = os.environ["REPO_NAME"]
46+
47+
client = anthropic.Client(API_KEY)
48+
gh = Github(GH_TOKEN)
49+
repo = gh.get_repo(REPO_NAME)
50+
pr = repo.get_pull(PR_NUMBER)
51+
52+
# Manual trigger support
53+
trigger_found = any(comment.body.strip().lower() == "@claude review"
54+
for comment in pr.get_issue_comments())
55+
if "auto" not in os.environ and not trigger_found:
56+
print("No trigger found, skipping review.")
57+
exit(0)
58+
59+
# Fetch PR diff content
60+
diff_url = pr.diff_url
61+
diff_response = requests.get(diff_url, headers={"Authorization": f"token {GH_TOKEN}"})
62+
diff_content = diff_response.text
63+
64+
# Parse diff
65+
patch = PatchSet(diff_content)
66+
67+
# Chunk large diffs
68+
MAX_LINES_PER_CHUNK = 200
69+
file_chunks = []
70+
current_chunk = []
71+
line_count = 0
72+
73+
for patched_file in patch:
74+
for hunk in patched_file:
75+
for line in hunk:
76+
if line.is_added or line.is_removed:
77+
current_chunk.append((patched_file.path, line.target_line_no, line.value))
78+
line_count += 1
79+
if line_count >= MAX_LINES_PER_CHUNK:
80+
file_chunks.append(current_chunk)
81+
current_chunk = []
82+
line_count = 0
83+
if current_chunk:
84+
file_chunks.append(current_chunk)
85+
86+
# Helper: format chunk for Claude
87+
def format_chunk(chunk):
88+
lines = [f"{f}:{ln}: {textwrap.shorten(content.strip(), width=100)}"
89+
for f, ln, content in chunk]
90+
return "\n".join(lines)
91+
92+
# Review each chunk via Claude Sonnet 4.6
93+
for idx, chunk in enumerate(file_chunks, start=1):
94+
prompt = f"""
95+
You are an AI code reviewer. Review these PR changes and provide comments in this format:
96+
file_path:line_number: comment
97+
98+
Diff Chunk {idx}:
99+
{format_chunk(chunk)}
100+
"""
101+
response = client.completions.create(
102+
model="claude-sonnet-4.6",
103+
prompt=prompt,
104+
max_tokens_to_sample=2000,
105+
temperature=0
106+
)
107+
review_text = response.completion.strip()
108+
109+
# Post comments inline
110+
for line in review_text.split("\n"):
111+
if ":" not in line:
112+
continue
113+
try:
114+
file_path, line_number_str, comment = line.split(":", 2)
115+
line_number = int(line_number_str.strip())
116+
# Map to diff position
117+
for patched_file in patch:
118+
if patched_file.path == file_path.strip():
119+
for hunk in patched_file:
120+
for i, patched_line in enumerate(hunk):
121+
if patched_line.target_line_no == line_number:
122+
pr.create_review_comment(
123+
body=comment.strip(),
124+
commit_id=pr.head.sha,
125+
path=file_path.strip(),
126+
position=i
127+
)
128+
break
129+
except Exception as e:
130+
print(f"Skipping line due to error: {line}. Reason: {e}")
131+
132+
print("Claude review completed!")
133+
EOF

0 commit comments

Comments
 (0)