diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index 98324697a22a..20a92f1ccfad 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -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 @@ -28,52 +27,60 @@ 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 + 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: | @@ -81,45 +88,51 @@ jobs: - 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: |