Skip to content

Commit 22e407d

Browse files
committed
Claude PR react on review
1 parent 824e74c commit 22e407d

File tree

1 file changed

+194
-0
lines changed

1 file changed

+194
-0
lines changed
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
name: "Claude PR Review"
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
pr_number:
7+
description: "Pull request number"
8+
required: true
9+
type: number
10+
review_id:
11+
description: "Pull request review ID"
12+
required: true
13+
type: number
14+
workflow_call:
15+
inputs:
16+
pr_number:
17+
description: "Pull request number"
18+
required: true
19+
type: number
20+
review_id:
21+
description: "Pull request review ID"
22+
required: true
23+
type: number
24+
25+
concurrency:
26+
group: claude-pr-review-${{ inputs.pr_number }}-${{ inputs.review_id }}
27+
cancel-in-progress: false
28+
29+
permissions:
30+
contents: read
31+
32+
jobs:
33+
respond:
34+
runs-on: ubuntu-latest
35+
timeout-minutes: 30
36+
permissions:
37+
contents: write
38+
pull-requests: write
39+
steps:
40+
- name: Harden the runner (Audit all outbound calls)
41+
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
42+
with:
43+
egress-policy: audit
44+
45+
- name: Fetch review details
46+
id: review
47+
env:
48+
GH_TOKEN: ${{ secrets.PHPSTAN_BOT_PR_TOKEN }}
49+
run: |
50+
gh api "repos/phpstan/phpstan-src/pulls/${{ inputs.pr_number }}" > /tmp/pr.json
51+
gh api "repos/phpstan/phpstan-src/pulls/${{ inputs.pr_number }}/reviews/${{ inputs.review_id }}" > /tmp/review.json
52+
gh api "repos/phpstan/phpstan-src/pulls/${{ inputs.pr_number }}/reviews/${{ inputs.review_id }}/comments" --paginate > /tmp/review-comments.json
53+
54+
echo "pr_head_ref=$(jq -r '.head.ref' /tmp/pr.json)" >> "$GITHUB_OUTPUT"
55+
56+
- name: "Checkout"
57+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
58+
with:
59+
ref: ${{ steps.review.outputs.pr_head_ref }}
60+
token: ${{ secrets.PHPSTAN_BOT_FORK_TOKEN }}
61+
fetch-depth: 0
62+
63+
- name: "Install PHP"
64+
uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2
65+
with:
66+
coverage: "none"
67+
php-version: "8.4"
68+
extensions: mbstring
69+
ini-file: development
70+
ini-values: memory_limit=-1
71+
72+
- name: "Install dependencies"
73+
uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3
74+
75+
- name: "Install test dependencies"
76+
run: composer install --working-dir tests
77+
78+
- name: Build prompt
79+
id: prompt
80+
shell: bash
81+
run: |
82+
review_body=$(jq -r '.body // ""' /tmp/review.json)
83+
review_state=$(jq -r '.state' /tmp/review.json)
84+
reviewer=$(jq -r '.user.login' /tmp/review.json)
85+
num_comments=$(jq 'length' /tmp/review-comments.json)
86+
87+
prompt="A reviewer ($reviewer) submitted a review on PR #${{ inputs.pr_number }} with state: $review_state."
88+
prompt+=$'\n'
89+
90+
if [ -n "$review_body" ] && [ "$review_body" != "null" ]; then
91+
prompt+=$'\n'
92+
prompt+="## Review body"
93+
prompt+=$'\n\n'
94+
prompt+="$review_body"
95+
prompt+=$'\n'
96+
fi
97+
98+
if [ "$num_comments" -gt 0 ]; then
99+
prompt+=$'\n'
100+
prompt+="## Review comments"
101+
prompt+=$'\n'
102+
103+
for i in $(seq 0 $((num_comments - 1))); do
104+
file_path=$(jq -r ".[$i].path" /tmp/review-comments.json)
105+
line=$(jq -r ".[$i].line // .[$i].original_line // \"?\"" /tmp/review-comments.json)
106+
body=$(jq -r ".[$i].body" /tmp/review-comments.json)
107+
diff_hunk=$(jq -r ".[$i].diff_hunk // \"\"" /tmp/review-comments.json)
108+
109+
prompt+=$'\n'
110+
prompt+="### $file_path (line $line)"
111+
prompt+=$'\n\n'
112+
113+
if [ -n "$diff_hunk" ] && [ "$diff_hunk" != "null" ]; then
114+
prompt+='```diff'
115+
prompt+=$'\n'
116+
prompt+="$diff_hunk"
117+
prompt+=$'\n'
118+
prompt+='```'
119+
prompt+=$'\n\n'
120+
fi
121+
122+
prompt+="$body"
123+
prompt+=$'\n'
124+
done
125+
fi
126+
127+
prompt+=$'\n'
128+
prompt+="## Instructions"
129+
prompt+=$'\n\n'
130+
prompt+="Please address this review. Make the requested code changes, then run tests with \`make tests\` and static analysis with \`make phpstan\` to verify."
131+
prompt+=$'\n'
132+
prompt+="Commit each logical change separately with a descriptive message."
133+
134+
{
135+
echo 'PROMPT<<PROMPT_DELIMITER'
136+
echo "$prompt"
137+
echo 'PROMPT_DELIMITER'
138+
} >> "$GITHUB_OUTPUT"
139+
140+
- name: Install Claude Code
141+
run: npm install -g @anthropic-ai/claude-code
142+
143+
- name: Run Claude
144+
env:
145+
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
146+
GH_TOKEN: ${{ secrets.PHPSTAN_BOT_FORK_TOKEN }}
147+
PROMPT: ${{ steps.prompt.outputs.PROMPT }}
148+
run: |
149+
claude -p "$PROMPT" \
150+
--output-format text \
151+
> /tmp/claude-response.txt
152+
153+
- name: Commit and push changes
154+
id: push
155+
run: |
156+
if [ -z "$(git status --porcelain)" ]; then
157+
echo "pushed=false" >> "$GITHUB_OUTPUT"
158+
exit 0
159+
fi
160+
161+
git config user.name "phpstan-bot"
162+
git config user.email "ondrej+phpstanbot@mirtes.cz"
163+
git add -A
164+
git commit -m "Address review feedback on PR #${{ inputs.pr_number }}"
165+
git push
166+
echo "pushed=true" >> "$GITHUB_OUTPUT"
167+
168+
- name: Reply to review
169+
if: always() && steps.claude.outcome != 'skipped'
170+
env:
171+
GH_TOKEN: ${{ secrets.PHPSTAN_BOT_PR_TOKEN }}
172+
PUSHED: ${{ steps.push.outputs.pushed }}
173+
run: |
174+
body=""
175+
if [ -f /tmp/claude-response.txt ]; then
176+
body=$(cat /tmp/claude-response.txt)
177+
fi
178+
179+
if [ "$PUSHED" = "true" ]; then
180+
body+=$'\n\n---\n_I pushed a commit addressing this review._'
181+
fi
182+
183+
if [ -z "$body" ]; then
184+
body="I processed this review but have nothing to report."
185+
fi
186+
187+
gh api \
188+
"repos/phpstan/phpstan-src/pulls/${{ inputs.pr_number }}/comments" \
189+
-f body="$body" \
190+
-F in_reply_to="$(jq -r '.[0].id // empty' /tmp/review-comments.json)" \
191+
2>/dev/null \
192+
|| gh pr comment "${{ inputs.pr_number }}" \
193+
--repo "phpstan/phpstan-src" \
194+
--body "$body"

0 commit comments

Comments
 (0)