Skip to content

Claude PR Review

Claude PR Review #1

name: "Claude PR Review"
on:
workflow_dispatch:
inputs:
pr_number:
description: "Pull request number"
required: true
type: number
review_id:
description: "Pull request review ID"
required: true
type: number
workflow_call:
inputs:
pr_number:
description: "Pull request number"
required: true
type: number
review_id:
description: "Pull request review ID"
required: true
type: number
concurrency:
group: claude-pr-review-${{ inputs.pr_number }}-${{ inputs.review_id }}
cancel-in-progress: false
permissions:
contents: read
jobs:
respond:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write
pull-requests: write
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- name: Fetch review details
id: review
env:
GH_TOKEN: ${{ secrets.PHPSTAN_BOT_PR_TOKEN }}
run: |
gh api "repos/${{ github.repository }}/pulls/${{ inputs.pr_number }}" > /tmp/pr.json
gh api "repos/${{ github.repository }}/pulls/${{ inputs.pr_number }}/reviews/${{ inputs.review_id }}" > /tmp/review.json
gh api "repos/${{ github.repository }}/pulls/${{ inputs.pr_number }}/reviews/${{ inputs.review_id }}/comments" --paginate > /tmp/review-comments.json
echo "pr_head_ref=$(jq -r '.head.ref' /tmp/pr.json)" >> "$GITHUB_OUTPUT"
- name: "Checkout"
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ steps.review.outputs.pr_head_ref }}
token: ${{ secrets.PHPSTAN_BOT_FORK_TOKEN }}
fetch-depth: 0
- name: "Install PHP"
uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2
with:
coverage: "none"
php-version: "8.4"
extensions: mbstring
ini-file: development
ini-values: memory_limit=-1
- name: "Install dependencies"
uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3
- name: "Install test dependencies"
run: composer install --working-dir tests
- name: Build prompt
id: prompt
shell: bash
run: |
review_body=$(jq -r '.body // ""' /tmp/review.json)
review_state=$(jq -r '.state' /tmp/review.json)
reviewer=$(jq -r '.user.login' /tmp/review.json)
num_comments=$(jq 'length' /tmp/review-comments.json)
prompt="A reviewer ($reviewer) submitted a review on PR #${{ inputs.pr_number }} with state: $review_state."
prompt+=$'\n'
if [ -n "$review_body" ] && [ "$review_body" != "null" ]; then
prompt+=$'\n'
prompt+="## Review body"
prompt+=$'\n\n'
prompt+="$review_body"
prompt+=$'\n'
fi
if [ "$num_comments" -gt 0 ]; then
prompt+=$'\n'
prompt+="## Review comments"
prompt+=$'\n'
for i in $(seq 0 $((num_comments - 1))); do
file_path=$(jq -r ".[$i].path" /tmp/review-comments.json)
line=$(jq -r ".[$i].line // .[$i].original_line // \"?\"" /tmp/review-comments.json)
body=$(jq -r ".[$i].body" /tmp/review-comments.json)
diff_hunk=$(jq -r ".[$i].diff_hunk // \"\"" /tmp/review-comments.json)
prompt+=$'\n'
prompt+="### $file_path (line $line)"
prompt+=$'\n\n'
if [ -n "$diff_hunk" ] && [ "$diff_hunk" != "null" ]; then
prompt+='```diff'
prompt+=$'\n'
prompt+="$diff_hunk"
prompt+=$'\n'
prompt+='```'
prompt+=$'\n\n'
fi
prompt+="$body"
prompt+=$'\n'
done
fi
prompt+=$'\n'
prompt+="## Instructions"
prompt+=$'\n\n'
prompt+="Please address this review. Make the requested code changes, then run tests with \`make tests\` and static analysis with \`make phpstan\` to verify."
prompt+=$'\n'
prompt+="Commit each logical change separately with a descriptive message."
{
echo 'PROMPT<<PROMPT_DELIMITER'
echo "$prompt"
echo 'PROMPT_DELIMITER'
} >> "$GITHUB_OUTPUT"
- name: Install Claude Code
run: npm install -g @anthropic-ai/claude-code
- name: Run Claude
env:
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
GH_TOKEN: ${{ secrets.PHPSTAN_BOT_FORK_TOKEN }}
PROMPT: ${{ steps.prompt.outputs.PROMPT }}
run: |
claude -p "$PROMPT" \
--output-format text \
> /tmp/claude-response.txt
- name: Commit and push changes
id: push
run: |
if [ -z "$(git status --porcelain)" ]; then
echo "pushed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
git config user.name "phpstan-bot"
git config user.email "ondrej+phpstanbot@mirtes.cz"
git add -A
git commit -m "Address review feedback on PR #${{ inputs.pr_number }}"
git push
echo "pushed=true" >> "$GITHUB_OUTPUT"
- name: Reply to review
if: always() && steps.claude.outcome != 'skipped'
env:
GH_TOKEN: ${{ secrets.PHPSTAN_BOT_PR_TOKEN }}
PUSHED: ${{ steps.push.outputs.pushed }}
run: |
body=""
if [ -f /tmp/claude-response.txt ]; then
body=$(cat /tmp/claude-response.txt)
fi
if [ "$PUSHED" = "true" ]; then
body+=$'\n\n---\n_I pushed a commit addressing this review._'
fi
if [ -z "$body" ]; then
body="I processed this review but have nothing to report."
fi
gh api \
"repos/${{ github.repository }}/pulls/${{ inputs.pr_number }}/comments" \
-f body="$body" \
-F in_reply_to="$(jq -r '.[0].id // empty' /tmp/review-comments.json)" \
2>/dev/null \
|| gh pr comment "${{ inputs.pr_number }}" \
--repo "${{ github.repository }}" \
--body "$body"