Skip to content
114 changes: 64 additions & 50 deletions .github/workflows/claude-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,22 @@ name: PR Reviews with Claude Code
permissions:
contents: read
pull-requests: write
statuses: write

on:
pull_request_target:
types: [opened, ready_for_review]

concurrency:
group: claude-review-${{ github.event.pull_request.html_url }}
cancel-in-progress: true
issue_comment:
types: [created]

jobs:
review:
if: |
github.event.pull_request.draft != true
&& !contains(github.event.pull_request.title, 'Revert')
authorize:
if: >-
(github.event_name == 'pull_request_target' && github.event.pull_request.draft != true && !contains(github.event.pull_request.title, 'Revert')) ||
(github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(github.event.comment.body, '@claude review'))
runs-on: blacksmith-2vcpu-ubuntu-2404
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
outputs:
IS_AUTHORIZED: ${{ steps.gate.outputs.IS_AUTHORIZED }}
steps:
- name: Checkout
uses: useblacksmith/checkout@0647fdbab2614a5eb86d2971070e325060d91056 # v1.7.0
Expand All @@ -28,98 +27,113 @@ jobs:
id: gate
uses: ./.github/actions/javascript/isAuthorizedContributor
with:
PR_NUMBER: ${{ github.event.pull_request.number }}
ACTOR: ${{ github.event.pull_request.user.login }}
ACTOR_ASSOCIATION: ${{ github.event.pull_request.author_association }}
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
ACTOR: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.user.login || github.event.comment.user.login }}
ACTOR_ASSOCIATION: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.author_association || github.event.comment.author_association }}
GITHUB_TOKEN: ${{ github.token }}
OS_BOTIFY_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }}

- name: Set review authorized
id: set-authorized
run: |
if [ "${{ steps.gate.outputs.IS_AUTHORIZED }}" = "true" ]; then
echo "IS_AUTHORIZED=true" >> "$GITHUB_OUTPUT"
else
echo "IS_AUTHORIZED=false" >> "$GITHUB_OUTPUT"
fi

review:
needs: authorize
if: needs.authorize.outputs.IS_AUTHORIZED == 'true'
# Set on this job, after authorization, so an unauthorized "@claude review" comment cannot cancel a running review
concurrency:
group: claude-review-${{ github.event.pull_request.html_url || github.event.issue.html_url }}
cancel-in-progress: true
Comment thread
neil-marcellini marked this conversation as resolved.
runs-on: blacksmith-2vcpu-ubuntu-2404
env:
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
steps:
- name: Checkout repository
if: steps.set-authorized.outputs.IS_AUTHORIZED == 'true'
uses: useblacksmith/checkout@0647fdbab2614a5eb86d2971070e325060d91056 # v1.7.0
with:
fetch-depth: 1

- name: Setup Node
if: steps.set-authorized.outputs.IS_AUTHORIZED == 'true'
uses: ./.github/actions/composite/setupNode

- name: Filter paths
if: steps.set-authorized.outputs.IS_AUTHORIZED == 'true'
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: filter
with:
filters: |
code:
- 'src/**'
docs:
- 'docs/**/*.md'
- 'docs/**/*.csv'

- name: Setup Claude review toolkit
if: steps.set-authorized.outputs.IS_AUTHORIZED == 'true'
id: toolkit
uses: Expensify/GitHub-Actions/.github/actions/claude-review-toolkit@54bfb8923d4f94c3cb209bfbad362fca9f0816f5
with:
rules_directory: .claude/skills/app-coding-standards/rules

- name: Check for an existing review of this commit
id: skip
env:
GH_TOKEN: ${{ github.token }}
run: shouldSkipReview.sh "$PR_NUMBER" "ai-review-completed/claude"

- name: Filter paths
if: steps.skip.outputs.skip != 'true'
id: filter
env:
GH_TOKEN: ${{ github.token }}
run: |
# gh pr view --json files is a GraphQL query capped at 100 files with no pagination,
# so a large PR would silently lose the paths that decide whether a review runs.
gh api --paginate "/repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --jq '.[].filename' > "$RUNNER_TEMP/changed-files.txt"
if grep -q '^src/' "$RUNNER_TEMP/changed-files.txt"; then
echo "code=true" >> "$GITHUB_OUTPUT"
fi
if grep -qE '^docs/.*\.(md|csv)$' "$RUNNER_TEMP/changed-files.txt"; then
echo "docs=true" >> "$GITHUB_OUTPUT"
fi
- name: Mark review as in progress
if: steps.set-authorized.outputs.IS_AUTHORIZED == 'true'
if: steps.skip.outputs.skip != 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
addPrReaction.sh "$PR_NUMBER" "eyes"

- name: Setup Node
if: steps.skip.outputs.skip != 'true' && (steps.filter.outputs.code == 'true' || steps.filter.outputs.docs == 'true')
uses: ./.github/actions/composite/setupNode

- name: Run Claude Code (code)
id: code-review
if: steps.set-authorized.outputs.IS_AUTHORIZED == 'true' && steps.filter.outputs.code == 'true'
if: steps.skip.outputs.skip != 'true' && steps.filter.outputs.code == 'true'
uses: anthropics/claude-code-action@4d7e1f0cd85743fdc93b1c8040ab54395da024e2 # v1.0.149
with:
display_report: 'true'
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
allowed_non_write_users: '*'
prompt: '/review-code-pr REPO: ${{ github.repository }} PR_NUMBER: ${{ github.event.pull_request.number }}'
prompt: '/review-code-pr REPO: ${{ github.repository }} PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}'
claude_args: |
--model claude-opus-4-8
--effort xhigh
--allowedTools "Task,Glob,Grep,Read,Bash(gh pr diff:*),Bash(gh pr view:*),Bash(check-compiler.sh:*)" --json-schema '${{ steps.toolkit.outputs.schema_json }}'

- name: Post code review results
if: |
steps.set-authorized.outputs.IS_AUTHORIZED == 'true'
&& steps.code-review.outcome == 'success'
&& steps.filter.outputs.code == 'true'
if: steps.code-review.outcome == 'success'
env:
GH_TOKEN: ${{ github.token }}
STRUCTURED_OUTPUT: ${{ steps.code-review.outputs.structured_output }}
run: postCodeReviewResults.sh "$PR_NUMBER"

- name: Run Claude Code (docs)
if: steps.set-authorized.outputs.IS_AUTHORIZED == 'true' && steps.filter.outputs.docs == 'true'
id: docs-review
if: steps.skip.outputs.skip != 'true' && steps.filter.outputs.docs == 'true'
uses: anthropics/claude-code-action@4d7e1f0cd85743fdc93b1c8040ab54395da024e2 # v1.0.149
with:
display_report: 'true'
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
allowed_non_write_users: '*'
prompt: '/review-helpdot-pr REPO: ${{ github.repository }} PR_NUMBER: ${{ github.event.pull_request.number }}'
prompt: '/review-helpdot-pr REPO: ${{ github.repository }} PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}'
claude_args: |
--model claude-opus-4-8
--effort high
--allowedTools "Task,Glob,Grep,Read,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),mcp__github_inline_comment__create_inline_comment"

- name: Record review completion
if: steps.code-review.outcome == 'success' || steps.docs-review.outcome == 'success'
env:
GH_TOKEN: ${{ github.token }}
HEAD_SHA: ${{ steps.skip.outputs.head_sha }}
STATUS_CONTEXT: ${{ steps.skip.outputs.context }}
run: recordReviewComplete.sh "$HEAD_SHA" "$STATUS_CONTEXT" "Reviewed at this commit - comment @claude review to re-run"

- name: Remove in-progress indicator
if: always() && steps.set-authorized.outputs.IS_AUTHORIZED == 'true'
if: always() && steps.skip.outputs.skip != 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
Expand Down
Loading