diff --git a/.github/workflows/claude-pr-review.yaml b/.github/workflows/claude-pr-review.yaml deleted file mode 100644 index 0e6a502df..000000000 --- a/.github/workflows/claude-pr-review.yaml +++ /dev/null @@ -1,464 +0,0 @@ -# Integrates Claude Code as an AI assistant for reviewing pull requests. -# For tips how to configure this workflow, see https://github.com/anthropics/claude-code-action. -# Examples: https://github.com/anthropics/claude-code-action/blob/0cf5eeec4f908121edd03a81411b55485994f8d3/docs/solutions.md. -# -# Architecture: The workflow is split into three jobs for least-privilege: -# 1. "setup" - posts/updates a "reviewing…" tracking comment (write permissions) -# 2. "review" - runs Claude with read-only permissions, produces structured JSON -# 3. "post" - reads the JSON and posts comments to the PR (write permissions) - -name: Claude PR Review -on: - issue_comment: - types: [created] - pull_request_target: - types: [opened, synchronize] -concurrency: - group: claude-review-${{ github.event.pull_request.number || github.event.issue.number }} - cancel-in-progress: true - -jobs: - setup: - runs-on: ubuntu-latest - if: > - (github.event_name == 'pull_request_target' - && contains(fromJSON('["OWNER","MEMBER"]'), github.event.pull_request.author_association)) - || - (github.event_name == 'issue_comment' - && github.event.issue.pull_request - && contains(github.event.comment.body, '@claude') - && contains(fromJSON('["OWNER","MEMBER"]'), github.event.comment.author_association)) - env: - PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} - permissions: - contents: read - pull-requests: write - outputs: - comment_id: ${{ steps.tracking.outputs.comment_id }} - pr_number: ${{ steps.pr.outputs.number }} - head_sha: ${{ steps.pr.outputs.head_sha }} - steps: - - name: Resolve PR metadata - id: pr - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - echo "number=$PR_NUMBER" >> "$GITHUB_OUTPUT" - gh pr view --repo "${{ github.repository }}" "$PR_NUMBER" --json headRefOid --jq '.headRefOid' | \ - xargs -I{} echo "head_sha={}" >> "$GITHUB_OUTPUT" - - - name: Create or update tracking comment - id: tracking - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd - with: - script: | - const owner = context.repo.owner; - const repo = context.repo.repo; - const prNumber = parseInt(process.env.PR_NUMBER, 10); - const runUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`; - const MARKER = ""; - - const issueComments = await github.paginate( - github.rest.issues.listComments, - { owner, repo, issue_number: prNumber, per_page: 100 }, - ); - - const existing = issueComments.find((c) => c.body && c.body.includes(MARKER)); - - let commentId; - if (existing) { - console.log(`Updating existing tracking comment ${existing.id}.`); - /* Prepend a re-reviewing banner but keep the previous review visible. */ - const prevBody = existing.body.replace(/\n\n\[Workflow run\]\([^)]*\)$/, ""); - await github.rest.issues.updateComment({ - owner, - repo, - comment_id: existing.id, - body: `> **Claude is re-reviewing this PR…** ([workflow run](${runUrl}))\n\n${prevBody}`, - }); - commentId = existing.id; - } else { - console.log("Creating new tracking comment."); - const {data: created} = await github.rest.issues.createComment({ - owner, - repo, - issue_number: prNumber, - body: `Claude is reviewing this PR… ([workflow run](${runUrl}))\n\n${MARKER}`, - }); - commentId = created.id; - } - - core.setOutput("comment_id", commentId); - - review: - runs-on: ubuntu-latest - needs: setup - permissions: - id-token: write # authentication for Claude GH bot - contents: read # clone repo - pull-requests: read # fetch PR comments and reviews - issues: read # fetch issue comments - actions: read # see GH Actions outputs - outputs: - structured_output: ${{ steps.claude.outputs.structured_output }} - steps: - # With _target mode, actions/checkout is checking out upstream, not the current PR. - # This is fine, we don't want anyone to be able to inject custom CLAUDE.md. - - name: Checkout upstream repository - uses: actions/checkout@v6 - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 - with: - role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }} - role-session-name: GitHubActions-facebook-bpfilter-${{ github.run_id }} - aws-region: us-east-1 - - - name: Run Claude Code - id: claude - uses: anthropics/claude-code-action@main - env: - REVIEW_SCHEMA: >- - { - "type": "object", - "required": ["comments", "summary"], - "properties": { - "summary": { - "type": "string", - "description": "A markdown summary of the review to post as a top-level tracking comment" - }, - "comments": { - "type": "array", - "items": { - "type": "object", - "required": ["file", "severity", "body"], - "properties": { - "file": { - "type": "string", - "description": "Path to the file relative to the repo root" - }, - "line": { - "type": "integer", - "description": "Line number in the diff (new file side) to attach the comment to" - }, - "severity": { - "type": "string", - "enum": ["must-fix", "suggestion", "nit"] - }, - "body": { - "type": "string", - "description": "The review comment body in markdown" - } - } - } - } - } - } - with: - # Claude auth - use_bedrock: "true" - - # We still have to pass GITHUB_TOKEN here because claude-code-action - # requires it, but we restrict Claude's tools to read-only operations - # so it cannot post comments or modify the PR. - github_token: ${{ secrets.GITHUB_TOKEN }} - - # Prompt - prompt: | - REPO: ${{ github.repository }} - PR NUMBER: ${{ needs.setup.outputs.pr_number }} - HEAD SHA: ${{ needs.setup.outputs.head_sha }} - - You are a code reviewer for the ${{ github.repository }} project. Review this pull request and - produce a structured JSON result containing your review. Do NOT attempt - to post comments yourself — just return the JSON. You are in the upstream repo - without the patch applied. Do not apply it. - - Read `.claude/commands/review-pr.md` and `doc/developers/style.rst` for review guidelines. - - ## Phase 1: Gather context - - Use the GitHub MCP server tools to fetch PR data. For all tools, pass - owner `${{ github.repository_owner }}`, repo `${{ github.event.repository.name }}`, - and pullNumber/issue_number ${{ needs.setup.outputs.pr_number }}: - - `mcp__github__get_pull_request_diff` to get the PR diff - - `mcp__github__get_pull_request` to get the PR title, body, and metadata - - `mcp__github__get_pull_request_comments` to get top-level PR comments - - `mcp__github__get_pull_request_reviews` to get PR reviews - - `mcp__github__get_pull_request_review_comments` to get inline review comments - - Also fetch issue comments using `mcp__github__get_issue_comments` with - issue_number ${{ needs.setup.outputs.pr_number }}. - - Look for an existing tracking comment (containing ``) - in the issue comments. If one exists, you will use it as the basis for - your `summary` in Phase 3. - - ## Phase 2: Independent parallel reviewers - - Read `.claude/commands/review-pr.md` for the review checklist. - All build, test, coverage, and cleanup instructions do not apply — other CI workflows handle those. - Also skip the "Output format" and "Report" sections - they are not applicable here. - - Launch a team of 5 independent review agents. Each agent reviews the ENTIRE PR - independently across ALL remaining sections (Code review, Style, Documentation, Commit). - - CRITICAL: You MUST launch all 5 agents in a SINGLE assistant message containing - 5 parallel Agent tool calls. Do NOT use `run_in_background`. Do NOT launch them - one at a time across multiple turns. All 5 must be foreground calls in one message - so they run concurrently and you automatically wait for all of them to finish - before proceeding. - - Give each agent a unique reviewer ID (reviewer-1 through reviewer-5) and pass them - the PR title, description, full patch, and the list of changed files. - Tell them to look at the pre-patch repo for context, and to read - `.claude/commands/review-pr.md` and `doc/developers/style.rst` for review guidelines. - Tell them to skip the "Output format" and "Report" subsections in review-pr.md — - those are for the standalone slash-command, not for subagent output. - - Each agent works independently and must NOT communicate with other agents. - Each agent must return a JSON array of issues: - `[{"file": "path", "line": (optional), "severity": "must-fix|suggestion|nit", "body": "..."}]` - - Do NOT escape characters in `body`. Write plain markdown — no backslash - escaping of `!` or other characters. - - `line` should be a line number from the NEW side of the diff **that appears - inside a diff hunk**. GitHub rejects lines outside the diff context. If you - cannot determine a valid diff line, omit `line`. - - Each agent MUST verify its findings before returning them and only return - issues it is confident about: - - For style/convention claims, check at least 3 existing examples in the codebase to confirm - the pattern actually exists before flagging a violation. - - For "use X instead of Y" suggestions, confirm X actually exists and works for this case. - - If unsure about an issue, do NOT include it. Only return findings you are confident are real. - - ## Phase 3: Collect, deduplicate, and summarize - - After ALL 5 reviewers complete (do NOT proceed until every agent has returned): - 1. Collect all issues from all reviewers. Merge duplicates (same file, lines within 3 of each - other, same problem). Keep ALL unique findings — each reviewer has already filtered out - low-confidence issues, so everything that made it through is worth reporting. - Issues found by multiple reviewers independently are higher confidence. - 2. Do NOT drop findings just because only one reviewer flagged them. A single reviewer - catching a real issue is the whole point of having 5 independent reviewers. - 3. For CLAUDE.md violations that appear in 3+ existing places in the codebase, do NOT include - them in `comments`. Instead, add them to the 'CLAUDE.md improvements' section of the summary. - 4. Check the existing inline review comments fetched in Phase 1. Do NOT include a - comment if one already exists on the same file and line about the same problem. - Also check for author replies that dismiss or reject a previous comment — do NOT - re-raise an issue the PR author has already responded to disagreeing with. - 5. Do NOT prefix `body` with a severity tag — the severity is already - captured in the `severity` field and will be added automatically when - posting inline comments. - 6. Write a `summary` field in markdown for a top-level tracking comment. - - **If no existing tracking comment was found (first run):** - Use this format: - - ``` - ## Claude review of PR # () - - - - ### Must fix - - [ ] **short title** — `file:line` — brief explanation - - ### Suggestions - - [ ] **short title** — `file:line` — brief explanation - - ### Nits - - [ ] **short title** — `file:line` — brief explanation - - ### CLAUDE.md improvements - - improvement suggestion - ``` - - Omit empty sections. Each checkbox item must correspond to an entry in `comments`. - If there are no issues at all, write a short message saying the PR looks good. - - **If an existing tracking comment was found (subsequent run):** - Use the existing comment as the starting point. Preserve the order and wording - of all existing items. Then apply these updates: - - Update the HEAD SHA in the header line. - - For each existing item, re-check whether the issue is still present in the - current diff. If it has been fixed, mark it checked: `- [x]`. - - If the PR author replied dismissing an item, mark it: - `- [x] ~~short title~~ (dismissed)`. - - Preserve checkbox state that was already set by previous runs or by hand. - - Append any NEW issues found in this run that aren't already listed, - in the appropriate severity section, after the existing items. - - Do NOT reorder, reword, or remove existing items. - - ## Error tracking - - Throughout all phases, track any errors that prevented you from doing - your job fully: permission denials (403, "Resource not accessible by - integration"), tools that were not available, rate limits, or any other - failures that degraded the review quality. If there were any, append a - `### Errors` section listing each failed tool/action and the error - message, so maintainers can fix the workflow configuration. - - ## CRITICAL: Return structured JSON output - - Your FINAL action must be to return a JSON object matching the following - JSON schema — do NOT end with a text summary or narrative. The `--json-schema` - flag is set, so your last response must be the structured JSON result, not a - text message. - - ```json - ${{ env.REVIEW_SCHEMA }} - ``` - - Do NOT attempt to post comments or use any MCP tools to modify the PR. - claude_args: | - --max-turns 100 - --model us.anthropic.claude-opus-4-6-v1 - --effort max - --json-schema '${{ env.REVIEW_SCHEMA }}' - --allowedTools " - Read,LS,Grep,Glob,Task,Agent, - Bash(cat:*),Bash(test:*),Bash(printf:*),Bash(jq:*),Bash(head:*),Bash(tail:*), - Bash(git:*),Bash(gh:*),Bash(grep:*),Bash(find:*),Bash(ls:*),Bash(wc:*), - Bash(diff:*),Bash(sed:*),Bash(awk:*),Bash(sort:*),Bash(uniq:*), - mcp__github__get_pull_request, - mcp__github__get_pull_request_diff, - mcp__github__get_pull_request_files, - mcp__github__get_pull_request_reviews, - mcp__github__get_pull_request_comments, - mcp__github__get_pull_request_review_comments, - mcp__github__get_pull_request_status, - mcp__github__get_issue_comments, - " - - # This requires "tag mode", which is currently bugged: - # https://github.com/anthropics/claude-code-action/issues/939 - track_progress: false - - # Show logs and Claude reasoning - show_full_output: true - - # Enable access to other GH Actions outputs - additional_permissions: | - actions: read - - post: - runs-on: ubuntu-latest - needs: [setup, review] - if: always() && needs.setup.result == 'success' - - permissions: - pull-requests: write - - steps: - - name: Post review comments - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd - env: - STRUCTURED_OUTPUT: ${{ needs.review.outputs.structured_output }} - REVIEW_RESULT: ${{ needs.review.result }} - PR_NUMBER: ${{ needs.setup.outputs.pr_number }} - HEAD_SHA: ${{ needs.setup.outputs.head_sha }} - COMMENT_ID: ${{ needs.setup.outputs.comment_id }} - with: - script: | - const owner = context.repo.owner; - const repo = context.repo.repo; - const prNumber = parseInt(process.env.PR_NUMBER, 10); - const headSha = process.env.HEAD_SHA; - const commentId = parseInt(process.env.COMMENT_ID, 10); - const runUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`; - const MARKER = ""; - - /* If the review job failed or was cancelled, update the tracking - * comment to reflect that and bail out. */ - if (process.env.REVIEW_RESULT !== "success") { - const verb = process.env.REVIEW_RESULT === "cancelled" ? "was cancelled" : "failed"; - await github.rest.issues.updateComment({ - owner, - repo, - comment_id: commentId, - body: `Claude review ${verb} — see [workflow run](${runUrl}) for details.\n\n${MARKER}`, - }); - core.setFailed("Review job did not succeed."); - return; - } - - /* Parse Claude's structured output. */ - const raw = process.env.STRUCTURED_OUTPUT; - console.log("Structured output from Claude:"); - console.log(raw || "(empty)"); - - let comments = []; - let summary = ""; - if (raw) { - try { - const review = JSON.parse(raw); - if (Array.isArray(review.comments)) - comments = review.comments; - if (typeof review.summary === "string") - summary = review.summary; - } catch (e) { - core.warning(`Failed to parse structured output: ${e.message}`); - } - } - - console.log(`Claude produced ${comments.length} review comment(s).`); - - /* Post each inline comment individually. Deduplication against existing - * comments is handled by Claude in the prompt, so we just post whatever - * it returns. Using individual comments (rather than a review) means - * re-runs only add new comments instead of creating a whole new review. */ - const inlineComments = comments.filter((c) => c.line); - const skipped = comments.length - inlineComments.length; - if (skipped > 0) - console.log(`Skipping ${skipped} file-level comment(s) (no line number).`); - - let posted = 0; - for (const c of inlineComments) { - console.log(` Posting comment on ${c.file}:${c.line}`); - try { - await github.rest.pulls.createReviewComment({ - owner, - repo, - pull_number: prNumber, - commit_id: headSha, - path: c.file, - line: c.line, - body: `Claude: **${c.severity}**: ${c.body}`, - }); - posted++; - } catch (e) { - /* GitHub rejects comments on lines outside the diff context. Log - * and continue — the tracking comment still contains all findings. */ - console.log(` Warning: failed to post comment on ${c.file}:${c.line}: ${e.message}`); - } - } - - if (posted > 0) - console.log(`Posted ${posted}/${inlineComments.length} inline comment(s).`); - else if (inlineComments.length > 0) - console.log(`Could not post any of ${inlineComments.length} inline comment(s) — see warnings above.`); - else - console.log("No inline comments to post."); - - /* Update the tracking comment with Claude's summary. */ - if (!summary) - summary = "Claude review: no issues found :tada:\n\n" + MARKER; - else if (!summary.includes(MARKER)) - summary += "\n\n" + MARKER; - summary += `\n\n[Workflow run](${runUrl})`; - - await github.rest.issues.updateComment({ - owner, - repo, - comment_id: commentId, - body: summary, - }); - - console.log("Tracking comment updated successfully."); - - if (inlineComments.length > 0 && posted === 0) - core.setFailed(`Could not post any of ${inlineComments.length} inline comment(s) — see warnings above.`); - else if (posted < inlineComments.length) - core.warning(`${inlineComments.length - posted}/${inlineComments.length} inline comment(s) could not be posted.`);