Claude Code Review #896
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Claude Code Review | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened] | |
| # NOTE: No paths-ignore here. "Claude Review" is a required status check, | |
| # so the workflow must always run to report a result. Path filtering is | |
| # handled inside the job via the "Check for reviewable changes" step. | |
| pull_request_target: | |
| types: [opened, synchronize, reopened] | |
| issue_comment: | |
| types: [created] | |
| merge_group: | |
| workflow_dispatch: | |
| concurrency: | |
| group: claude-review-${{ github.event_name }}-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| issues: write | |
| id-token: write | |
| jobs: | |
| # Merge queue no-op: the real review already ran on the PR. The queue just | |
| # needs the "Claude Review" check name to exist and pass. | |
| merge-queue-pass: | |
| name: Claude Review | |
| if: github.event_name == 'merge_group' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - run: echo "Merge queue — skipping review (already passed on PR)" | |
| # When the Claude review pushes an autofix commit via the GitHub App token, | |
| # the synchronize event fires with actor=<app-name>[bot]. We don't want | |
| # to re-run the full review (infinite loop), but "Claude Review" is a | |
| # required check — so we need this no-op job to satisfy the gate. | |
| # NOTE: The bot name below (rhai-org-pulse[bot]) must match the GitHub App | |
| # configured for this repo. Update if using a different App. | |
| # Dependabot PRs also skip here — they lack repo secrets for Vertex AI auth. | |
| # The maintainer reviews them manually. | |
| autofix-pass: | |
| name: Claude Review | |
| if: | | |
| github.event_name != 'merge_group' && | |
| (github.actor == 'rhai-org-pulse[bot]' || github.actor == 'github-actions[bot]' || github.actor == 'dependabot[bot]') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - run: echo "Autofix push by ${{ github.actor }} — review already passed, skipping re-review" | |
| claude-review: | |
| name: Claude Review | |
| timeout-minutes: 20 | |
| if: | | |
| github.event_name != 'merge_group' && | |
| ((github.event_name == 'pull_request' && | |
| github.event.pull_request.head.repo.full_name == github.repository && | |
| github.event.pull_request.draft != true && | |
| github.actor != 'github-actions[bot]' && | |
| github.actor != 'rhai-org-pulse[bot]' && | |
| github.actor != 'dependabot[bot]') || | |
| (github.event_name == 'pull_request_target' && | |
| github.event.pull_request.head.repo.full_name != github.repository && | |
| github.event.pull_request.draft != true && | |
| github.actor != 'github-actions[bot]' && | |
| github.actor != 'rhai-org-pulse[bot]' && | |
| github.actor != 'dependabot[bot]') || | |
| (github.event_name == 'issue_comment' && | |
| github.event.issue.pull_request && | |
| contains(github.event.comment.body, '@claude') && | |
| github.event.comment.author_association != 'NONE' && | |
| github.event.comment.author_association != 'FIRST_TIMER' && | |
| github.event.comment.author_association != 'FIRST_TIME_CONTRIBUTOR') || | |
| (github.event_name == 'workflow_dispatch')) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Determine review context | |
| id: context | |
| run: | | |
| if [ "${{ github.event_name }}" = "pull_request_target" ]; then | |
| echo "is_fork=true" >> "$GITHUB_OUTPUT" | |
| echo "pr_number=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "is_fork=false" >> "$GITHUB_OUTPUT" | |
| echo "pr_number=${{ github.event.pull_request.number || github.event.issue.number }}" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Check for reviewable changes | |
| id: check-paths | |
| run: | | |
| PR_NUM="${{ steps.context.outputs.pr_number }}" | |
| if [ -z "$PR_NUM" ]; then | |
| echo "needs_review=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| CHANGED_FILES=$(gh api repos/${{ github.repository }}/pulls/${PR_NUM}/files \ | |
| --paginate --jq '.[].filename') | |
| needs_review=false | |
| while IFS= read -r file; do | |
| case "$file" in | |
| deploy/openshift/overlays/*/kustomization.yaml) | |
| ;; # matches ignore pattern | |
| *) | |
| needs_review=true | |
| break | |
| ;; | |
| esac | |
| done <<< "$CHANGED_FILES" | |
| echo "needs_review=$needs_review" >> "$GITHUB_OUTPUT" | |
| if [ "$needs_review" = "false" ]; then | |
| echo "All changed files match ignore patterns — skipping review" | |
| fi | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Reject fork PRs on issue_comment trigger | |
| id: fork-guard | |
| if: steps.check-paths.outputs.needs_review == 'true' && github.event_name == 'issue_comment' | |
| run: | | |
| IS_FORK=$(gh api repos/${{ github.repository }}/pulls/${{ github.event.issue.number }} \ | |
| --jq '.head.repo.full_name != .base.repo.full_name') | |
| if [ "$IS_FORK" = "true" ]; then | |
| echo "is_fork=true" >> "$GITHUB_OUTPUT" | |
| gh pr comment "${{ github.event.issue.number }}" \ | |
| --repo "${{ github.repository }}" \ | |
| --body "Autofix review via \`@claude\` is not available on fork PRs for security reasons. A read-only review runs automatically when the PR is opened." | |
| echo "::warning::Skipping autofix review — fork PR detected on issue_comment trigger" | |
| fi | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # --- Same-repo setup (full checkout, Node.js, git identity) --- | |
| # Generate a GitHub App token so that autofix pushes trigger downstream | |
| # workflows (CI, Integration Tests, etc.). The default GITHUB_TOKEN does | |
| # not trigger other workflows — this is a GitHub anti-recursion safeguard. | |
| - name: Generate GitHub App token | |
| if: steps.check-paths.outputs.needs_review == 'true' && steps.context.outputs.is_fork != 'true' && steps.fork-guard.outputs.is_fork != 'true' | |
| id: app-token | |
| uses: actions/create-github-app-token@v3 | |
| with: | |
| app-id: ${{ secrets.APP_ID }} | |
| private-key: ${{ secrets.APP_PRIVATE_KEY }} | |
| - name: Checkout code | |
| if: steps.check-paths.outputs.needs_review == 'true' && steps.context.outputs.is_fork != 'true' && steps.fork-guard.outputs.is_fork != 'true' | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ steps.app-token.outputs.token }} | |
| - name: Set up Node.js | |
| if: steps.check-paths.outputs.needs_review == 'true' && steps.context.outputs.is_fork != 'true' && steps.fork-guard.outputs.is_fork != 'true' | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: 22 | |
| cache: 'npm' | |
| - name: Install dependencies | |
| if: steps.check-paths.outputs.needs_review == 'true' && steps.context.outputs.is_fork != 'true' && steps.fork-guard.outputs.is_fork != 'true' | |
| run: npm ci | |
| - name: Configure git identity | |
| if: steps.check-paths.outputs.needs_review == 'true' && steps.context.outputs.is_fork != 'true' && steps.fork-guard.outputs.is_fork != 'true' | |
| run: | | |
| git config user.name "claude-code-review[bot]" | |
| git config user.email "claude-code-review[bot]@users.noreply.github.com" | |
| # --- Fork setup (base-branch checkout, temp branch for action) --- | |
| - name: Checkout base branch | |
| if: steps.check-paths.outputs.needs_review == 'true' && steps.context.outputs.is_fork == 'true' && github.event_name == 'pull_request_target' | |
| uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ github.event.pull_request.base.sha }} | |
| - name: Setup fork PR branch for action | |
| if: steps.check-paths.outputs.needs_review == 'true' && steps.context.outputs.is_fork == 'true' && github.event_name == 'pull_request_target' | |
| id: fork-setup | |
| run: | | |
| echo "Fork PR #${PR_NUM} detected (branch: ${HEAD_REF})" | |
| # Check if PR modifies workflow files | |
| CHANGED_FILES=$(gh api repos/${{ github.repository }}/pulls/${PR_NUM}/files \ | |
| --paginate --jq '.[].filename') | |
| MODIFIES_WORKFLOWS=false | |
| while IFS= read -r file; do | |
| if [[ "$file" == .github/workflows/* ]]; then | |
| MODIFIES_WORKFLOWS=true | |
| break | |
| fi | |
| done <<< "$CHANGED_FILES" | |
| if [ "$MODIFIES_WORKFLOWS" = "true" ]; then | |
| echo "::warning::PR modifies workflow files - skipping temp branch push due to GitHub security restrictions" | |
| echo "::notice::Review will proceed using gh pr diff only (some Read/Grep/Glob tools may not work on PR code)" | |
| echo "modifies_workflows=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # The claude-code-action runs `git fetch origin <branchName>` internally, | |
| # which fails for fork PRs because the branch only exists on the fork. | |
| # Workaround: push the PR head commit to origin under a namespaced ref | |
| # so the action's fetch succeeds. Clean up after the review completes. | |
| SAFE_BRANCH="claude-review/fork-pr-${PR_NUM}" | |
| git fetch origin "refs/pull/${PR_NUM}/head" | |
| git push origin "FETCH_HEAD:refs/heads/${SAFE_BRANCH}" | |
| echo "fork_branch=${SAFE_BRANCH}" >> "$GITHUB_OUTPUT" | |
| echo "Pushed fork PR head to origin/${SAFE_BRANCH}" | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| PR_NUM: ${{ github.event.pull_request.number }} | |
| HEAD_REF: ${{ github.event.pull_request.head.ref }} | |
| # --- Shared: GCP auth --- | |
| - name: Authenticate to Google Cloud | |
| if: steps.check-paths.outputs.needs_review == 'true' && steps.fork-guard.outputs.is_fork != 'true' && env.HAS_GCP_KEY == 'true' | |
| env: | |
| HAS_GCP_KEY: ${{ secrets.GCP_SA_KEY != '' }} | |
| uses: google-github-actions/auth@v3 | |
| with: | |
| credentials_json: ${{ secrets.GCP_SA_KEY }} | |
| # --- Claude review (one step per mode, shared verdict check) --- | |
| - name: Claude Code Review | |
| if: steps.check-paths.outputs.needs_review == 'true' && steps.context.outputs.is_fork != 'true' && steps.fork-guard.outputs.is_fork != 'true' | |
| id: claude-review | |
| uses: anthropics/claude-code-action@v1.0.216 | |
| with: | |
| use_vertex: "true" | |
| track_progress: true | |
| display_report: "true" | |
| allowed_bots: "claude" | |
| claude_args: '--model claude-opus-4-6 --json-schema ''{"type":"object","properties":{"verdict":{"enum":["PASS","FAIL"]},"unfixed_blocking_issues":{"type":"array","items":{"type":"object","properties":{"category":{"type":"string"},"description":{"type":"string"}},"required":["category","description"]}}},"required":["verdict","unfixed_blocking_issues"]}'' --allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr checkout:*),Bash(git add:*),Bash(git commit:*),Bash(git push origin:*),Bash(git diff:*),Bash(git status:*),Bash(npm test:*),Bash(npm run lint:*),Read,Glob,Grep,Edit,Write"' | |
| prompt: | | |
| REPO: ${{ github.repository }} | |
| PR NUMBER: ${{ steps.context.outputs.pr_number }} | |
| You are reviewing a pull request. Your job is to: | |
| 1. **Determine PR context**: | |
| - If no PR number is available (e.g., workflow_dispatch without context), | |
| post a comment explaining that no PR was found and exit. | |
| - Run `gh pr checkout <PR_NUMBER>` to switch to the PR branch. | |
| 2. **Review** the PR diff. Read `AGENTS.md` (hard constraints and | |
| conventions) and `.github/instructions/review.instructions.md` | |
| (review checklist and verdict rules). Apply all criteria from both. | |
| 3. **Fix** any issues you find by editing the files directly: | |
| - Use Edit/Write tools to modify the source files | |
| - Fix all issues you're confident about: bugs, style, security, performance | |
| - After making all fixes, validate them: | |
| ``` | |
| npm test | |
| npm run lint | |
| ``` | |
| - If tests or lint fail due to your changes, fix the issues or revert | |
| your changes to that file | |
| - Only commit and push if you actually made changes and tests pass: | |
| ``` | |
| git add -u | |
| git diff --cached --quiet || git commit -m "fix: Claude code review autofix | |
| <concise summary of changes>" | |
| git push origin HEAD | |
| ``` | |
| 4. **Report** your findings: | |
| - Use inline comments for issues you found (whether or not you fixed them) | |
| - Post a top-level PR comment summarizing: | |
| - Issues found and fixed (with brief descriptions) | |
| - Issues found but NOT fixed (with explanations of why) | |
| - If no issues were found, say so briefly | |
| 5. **Set review verdict**: Follow the "Verdict rules" section in | |
| `.github/instructions/review.instructions.md` to populate the | |
| structured output. This is your very last action. | |
| **Important rules:** | |
| - Never force push. Only push to the current PR branch via `git push origin HEAD`. | |
| - Use `git add -u` (not `git add -A`) to avoid staging untracked files. | |
| - Be concise. Focus on actionable feedback. Don't nitpick style unless | |
| it impacts readability. | |
| - Only fix things you're confident about. If a fix is ambiguous or | |
| subjective, comment instead of changing code. | |
| - Do not modify files outside the scope of the PR unless necessary to | |
| fix an issue (e.g., a missing import in another file). | |
| env: | |
| ANTHROPIC_VERTEX_PROJECT_ID: "rhai-org-pulse" | |
| CLOUD_ML_REGION: "global" | |
| CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: "1" | |
| DISABLE_PROMPT_CACHING: "1" | |
| - name: Claude Code Review (Read-Only) | |
| if: steps.check-paths.outputs.needs_review == 'true' && steps.context.outputs.is_fork == 'true' && github.event_name == 'pull_request_target' | |
| id: claude-review-fork | |
| uses: anthropics/claude-code-action@v1.0.216 | |
| with: | |
| use_vertex: "true" | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| allowed_non_write_users: "*" | |
| track_progress: true | |
| display_report: "true" | |
| allowed_bots: "claude" | |
| claude_args: '--model claude-opus-4-6 --json-schema ''{"type":"object","properties":{"verdict":{"enum":["PASS","FAIL"]},"unfixed_blocking_issues":{"type":"array","items":{"type":"object","properties":{"category":{"type":"string"},"description":{"type":"string"}},"required":["category","description"]}}},"required":["verdict","unfixed_blocking_issues"]}'' --allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Read,Glob,Grep"' | |
| prompt: | | |
| REPO: ${{ github.repository }} | |
| PR NUMBER: ${{ steps.context.outputs.pr_number }} | |
| You are reviewing a **fork pull request**. This is a READ-ONLY review. | |
| You must NEVER execute any code from the PR (no npm test, npm run, node, | |
| or any other command that runs PR code). You do NOT have tools to edit | |
| files, commit, or push — only read and comment. | |
| 1. Run `gh pr diff ${{ steps.context.outputs.pr_number }}` to get the | |
| full diff of the PR. | |
| 2. **Review** the diff. Read `AGENTS.md` (hard constraints and | |
| conventions) and `.github/instructions/review.instructions.md` | |
| (review checklist and verdict rules). Apply all criteria from both. | |
| 3. **Report** your findings: | |
| - Use inline comments for specific issues in the diff | |
| - Post a top-level PR comment summarizing: | |
| - Issues found (with brief descriptions and suggestions) | |
| - If no issues were found, say so briefly | |
| - Note that this was a read-only review (fork PR) and no autofixes | |
| were applied | |
| 4. **Set review verdict**: Follow the "Verdict rules" section in | |
| `.github/instructions/review.instructions.md` to populate the | |
| structured output. This is your very last action. | |
| **Important rules:** | |
| - This is a FORK PR. Do NOT run any commands that execute PR code. | |
| - Do NOT attempt to check out the PR branch. | |
| - Use `gh pr diff` and `Read` to understand the changes. | |
| - Be concise. Focus on actionable feedback. | |
| env: | |
| ANTHROPIC_VERTEX_PROJECT_ID: "rhai-org-pulse" | |
| CLOUD_ML_REGION: "global" | |
| CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: "1" | |
| DISABLE_PROMPT_CACHING: "1" | |
| # --- Shared: verdict check and cleanup --- | |
| - name: Check review result | |
| if: steps.check-paths.outputs.needs_review == 'true' && steps.fork-guard.outputs.is_fork != 'true' | |
| env: | |
| STRUCTURED_OUTPUT: ${{ steps.claude-review.outputs.structured_output || steps.claude-review-fork.outputs.structured_output }} | |
| run: | | |
| if [ -z "$STRUCTURED_OUTPUT" ] || [ "$STRUCTURED_OUTPUT" = "null" ]; then | |
| echo "::error::No structured output from Claude review" | |
| exit 1 | |
| fi | |
| VERDICT=$(echo "$STRUCTURED_OUTPUT" | jq -r '.verdict') | |
| ISSUE_COUNT=$(echo "$STRUCTURED_OUTPUT" | jq '.unfixed_blocking_issues | length') | |
| if [ "$VERDICT" = "FAIL" ] || [ "$ISSUE_COUNT" -gt 0 ]; then | |
| echo "::error::Claude review found blocking issues that must be addressed:" | |
| echo "$STRUCTURED_OUTPUT" | jq -r '.unfixed_blocking_issues[] | " - [\(.category)] \(.description)"' | |
| exit 1 | |
| fi | |
| echo "Claude review passed" | |
| - name: Cleanup fork PR branch | |
| if: always() && steps.fork-setup.outputs.fork_branch && steps.fork-setup.outputs.modifies_workflows != 'true' | |
| run: | | |
| echo "Cleaning up temporary branch: ${{ steps.fork-setup.outputs.fork_branch }}" | |
| git push origin --delete "${{ steps.fork-setup.outputs.fork_branch }}" || true | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |