Skip to content
Open
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
84 changes: 54 additions & 30 deletions .github/workflows/claude-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,31 @@ name: PR Reviews with Claude Code
permissions:
contents: read
pull-requests: write
statuses: write

on:
pull_request_target:
types: [opened, ready_for_review]
issue_comment:
types: [created]

concurrency:
group: claude-review-${{ github.event.pull_request.html_url }}
group: >-
claude-review-${{
github.event_name == 'pull_request_target' && github.event.pull_request.html_url ||
github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude review') && github.event.issue.html_url ||
Comment on lines +17 to +18

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the same concurrency key for both PR event types

For a pull-request issue_comment event, github.event.issue.html_url is the issue-shaped /issues/<number> URL, while github.event.pull_request.html_url is /pull/<number>. Consequently, an @claude review comment arriving during an opened or ready_for_review run enters a different concurrency group; both runs can pass shouldSkipReview.sh before either records its status, run Claude simultaneously, and post duplicate feedback. Build the key from the repository and resolved PR number instead.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent unauthorized comments from canceling active reviews

Workflow-level concurrency is applied before the job reaches isAuthorizedContributor, so every PR comment containing @claude review joins this cancellation group regardless of who posted it. A user who fails the authorization gate can therefore repeatedly comment during a legitimate comment-triggered review, canceling it before completion and preventing the status from being recorded. Avoid enabling cancel-in-progress for untrusted comment-triggered runs, or otherwise isolate them until authorization has succeeded.

Useful? React with 👍 / 👎.

github.run_id
}}
cancel-in-progress: true

jobs:
review:
if: |
github.event.pull_request.draft != true
&& !contains(github.event.pull_request.title, 'Revert')
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 }}
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
steps:
- name: Checkout
uses: useblacksmith/checkout@0647fdbab2614a5eb86d2971070e325060d91056 # v1.7.0
Expand All @@ -28,9 +36,9 @@ 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 }}

Expand All @@ -53,73 +61,89 @@ jobs:
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: Mark review as in progress
- name: Check for an existing review of this commit
if: steps.set-authorized.outputs.IS_AUTHORIZED == 'true'
id: skip
env:
GH_TOKEN: ${{ github.token }}
run: shouldSkipReview.sh "$PR_NUMBER" "ai-review-completed/claude"

- name: Filter paths
if: steps.set-authorized.outputs.IS_AUTHORIZED == 'true' && 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' && steps.skip.outputs.skip != 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
addPrReaction.sh "$PR_NUMBER" "eyes"

- name: Run Claude Code (code)
id: code-review
if: steps.set-authorized.outputs.IS_AUTHORIZED == 'true' && steps.filter.outputs.code == 'true'
if: steps.set-authorized.outputs.IS_AUTHORIZED == 'true' && 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.set-authorized.outputs.IS_AUTHORIZED == 'true' && 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.set-authorized.outputs.IS_AUTHORIZED == 'true' && steps.skip.outputs.skip != 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
Expand Down
Loading