From fb513a7e1c5aa5f1ac7a274a0ebf9a6ed7fc02d1 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Tue, 17 Mar 2026 22:14:15 +0100 Subject: [PATCH] ci: Fetch context for claude-review job in setup job Rather than have claude fetch the context itself, let's fetch the context for it in the setup job. This has the following advantages: - We can reduce the permissions granted to the claude job - claude has less opportunity to mess up trying to fetch the context itself. Specifically, it keeps spawsning a background task to fetch the PR branch which messes up the structured output at the end, causing the review job to fail. By pre-fetching the context it won't have to spawn the background task. Additionally, we limit the git commands it can execute to local ones to ensure it doesn't try to fetch the PR branch. Finally, we fetch the branch ourselves as pr-review so claude can look at it to review the PR. --- .github/workflows/claude-review.yml | 168 +++++++++++++--------------- 1 file changed, 76 insertions(+), 92 deletions(-) diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index 736b459b07..afcae1c9e9 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -3,7 +3,7 @@ # via AWS Bedrock using OIDC — no long-lived API keys required. # # Architecture: The workflow is split into three jobs for least-privilege: -# 1. "setup" — posts/updates a "reviewing…" tracking comment (write permissions) +# 1. "setup" — fetches PR context, posts/updates 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) @@ -54,9 +54,9 @@ jobs: pull-requests: write outputs: - pr_number: ${{ steps.pr.outputs.number }} - head_sha: ${{ steps.pr.outputs.head_sha }} - comment_id: ${{ steps.tracking.outputs.comment_id }} + pr_number: ${{ steps.context.outputs.pr_number }} + comment_id: ${{ steps.context.outputs.comment_id }} + pr_context: ${{ steps.context.outputs.pr_context }} steps: - name: Auto-add claude-review label for trusted contributors @@ -65,17 +65,8 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: gh pr edit --repo "${{ github.repository }}" "$PR_NUMBER" --add-label claude-review - - 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 + - name: Fetch PR context and create tracking comment + id: context uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd with: script: | @@ -85,14 +76,19 @@ jobs: 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 }, - ); + /* Fetch all PR data in parallel. */ + const [pr, reviews, issueComments, reviewComments] = await Promise.all([ + github.rest.pulls.get({ owner, repo, pull_number: prNumber }), + github.paginate(github.rest.pulls.listReviews, { owner, repo, pull_number: prNumber, per_page: 100 }), + github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: prNumber, per_page: 100 }), + github.paginate(github.rest.pulls.listReviewComments, { owner, repo, pull_number: prNumber, per_page: 100 }), + ]); + /* Find or create tracking comment. */ const existing = issueComments.find((c) => c.body && c.body.includes(MARKER)); - let commentId; + let trackingCommentBody = null; + if (existing) { console.log(`Updating existing tracking comment ${existing.id}.`); /* Prepend a re-reviewing banner but keep the previous review visible. */ @@ -104,6 +100,7 @@ jobs: body: `> **Claude is re-reviewing this PR…** ([workflow run](${runUrl}))\n\n${prevBody}`, }); commentId = existing.id; + trackingCommentBody = prevBody; } else { console.log("Creating new tracking comment."); const {data: created} = await github.rest.issues.createComment({ @@ -115,7 +112,17 @@ jobs: commentId = created.id; } + /* Build context JSON for Claude. */ + const prContext = { + pr: pr.data, + reviews, + tracking_comment: trackingCommentBody, + review_comments: reviewComments, + }; + + core.setOutput("pr_number", prNumber); core.setOutput("comment_id", commentId); + core.setOutput("pr_context", JSON.stringify(prContext)); review: runs-on: ubuntu-latest @@ -123,8 +130,6 @@ jobs: permissions: contents: read - pull-requests: read # Fetch PR comments and reviews - issues: read # Fetch issue comments id-token: write # Authenticate with AWS via OIDC outputs: @@ -132,6 +137,19 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + # Need full history so git diff ~1.. works for all PR commits. + fetch-depth: 0 + + - name: Fetch PR branch + env: + PR_NUMBER: ${{ needs.setup.outputs.pr_number }} + run: git fetch origin "pull/${PR_NUMBER}/head:pr-review" + + - name: Write PR context + env: + PR_CONTEXT: ${{ needs.setup.outputs.pr_context }} + run: printenv PR_CONTEXT > pr-context.json - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 @@ -193,9 +211,8 @@ jobs: } with: 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. + # Required by claude-code-action even though Claude itself doesn't + # call the GitHub API — the action uses it for permission checks. github_token: ${{ secrets.GITHUB_TOKEN }} track_progress: false show_full_output: "true" @@ -205,88 +222,60 @@ jobs: --allowedTools " Read,LS,Grep,Glob,Task,TaskStop, Bash(cat *),Bash(test *),Bash(printf *),Bash(jq *),Bash(head *),Bash(tail *), - Bash(git *),Bash(grep *),Bash(find *),Bash(ls *),Bash(wc *), - Bash(gh api *), + Bash(git log *),Bash(git diff *),Bash(git show *),Bash(git rev-parse *), + Bash(git merge-base *),Bash(git blame *),Bash(git branch *),Bash(git status *), + Bash(grep *),Bash(find *),Bash(ls *),Bash(wc *), Bash(diff *),Bash(sed *),Bash(awk *),Bash(sort *),Bash(uniq *), " --json-schema '${{ env.REVIEW_SCHEMA }}' 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. + with the PR branch available as `pr-review`. Do not apply or merge the patch. - ## Phase 1: Gather context + ## Phase 1: Read context - Use `gh api` to fetch all PR data. The base path for all endpoints is - `repos/${{ github.repository }}/pulls/${{ needs.setup.outputs.pr_number }}`. + All PR data has been pre-fetched. Read `pr-context.json` from the repo root. + It contains a JSON object with: + - `pr` — full GitHub PR object (title, body, user, head SHA, etc.) + - `reviews` — array of PR reviews from the GitHub API + - `tracking_comment` — body of the existing tracking comment (null on first run); + if present, use it as the basis for your `summary` in Phase 3 + - `review_comments` — array of inline review comments from the GitHub API; + you will need the `id` fields in Phase 3 to populate the `resolve` array - **IMPORTANT: All data fetching in this phase MUST complete before moving to - Phase 2. Do NOT use `run_in_background` for any commands in this phase. Wait - for all results before proceeding.** - - Fetch the following in parallel: - - `gh api repos/${{ github.repository }}/pulls/${{ needs.setup.outputs.pr_number }}` - — PR title, body, and metadata - - `gh api repos/${{ github.repository }}/pulls/${{ needs.setup.outputs.pr_number }}/reviews --paginate` - — PR reviews - - `gh api repos/${{ github.repository }}/pulls/${{ needs.setup.outputs.pr_number }}/commits --paginate --jq '.[].sha'` - — list of commit SHAs - - `gh api repos/${{ github.repository }}/issues/${{ needs.setup.outputs.pr_number }}/comments --paginate` - — issue comments (look for the tracking comment containing ``; - if one exists, use it as the basis for your `summary` in Phase 3) - - `gh api repos/${{ github.repository }}/pulls/${{ needs.setup.outputs.pr_number }}/comments --paginate` - — inline review comments including each comment's numeric `id`, `path`, - `line`, `body`, `user.login`, and `in_reply_to_id`; you will need the - `id` fields in Phase 3 to populate the `resolve` array + The PR branch has been fetched locally as `pr-review`. Use + `git log --reverse --format=%H HEAD..pr-review` to list the PR commits, and + `git show ` or `git diff ~1..` to access commit diffs. ## Phase 2: Per-commit review with subagents - Review each commit in the PR individually, in order (oldest first). For each - commit, fetch its diff using `gh api repos/{owner}/{repo}/commits/{sha} - -H 'Accept: application/vnd.github.diff'` via Bash, then launch a subagent - to review that commit's changes. + Launch a subagent for each commit in the PR, all in parallel. Each subagent + receives only the commit SHA to review. It reads `pr-context.json` for PR + context, uses `git show ` or `git diff ~1..` to fetch the + diff, and reads the codebase to verify its findings. - Each commit review subagent receives: - - The PR title and description (for overall context) - - The diffs of all preceding commits in the PR (for context on what was already changed) - - The commit message and SHA of the commit being reviewed - - The commit diff (from `gh api`) - - Each commit review subagent reviews: - - Code quality, style, and best practices - - Potential bugs, issues, incorrect logic - - Security implications - - CLAUDE.md compliance - - Each commit review subagent must return a JSON array of issues: + Each subagent reviews code quality, style, potential bugs, and security + implications. It must return a JSON array: `[{"file": "path", "line": (optional), "severity": "must-fix|suggestion|nit", "body": "...", "commit": ""}]` - The `commit` field MUST be set to the SHA of the commit being reviewed. - Each commit review subagent MUST only return comments about changes in the commit it is - reviewing — do NOT comment on code from preceding commits, even if it was - provided for context. If a preceding commit has an issue, it will be caught - by the subagent reviewing that commit. + The `commit` field MUST be the SHA of the commit being reviewed. Only + comment on changes in that commit — not preceding commits. - `line` should be a line number from the NEW side of the diff **that appears inside - a diff hunk** (i.e. a line that is shown in the patch output). GitHub's review - comment API rejects lines outside the diff context, so never reference lines - that are not visible in the patch. If you cannot determine a valid diff line, - omit `line` — the comment will still appear in the tracking comment summary. + `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 commit review subagent MUST verify its findings before returning them: - - 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. + Each subagent MUST verify findings before returning them: + - 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. - If unsure, don't include the issue. - Launch commit review subagents for all commits in parallel (e.g. if - reviewing a 7-commit PR, launch all 7 commit review subagents at once). - ## Phase 3: Collect, deduplicate, and summarize After ALL commit review subagents complete: @@ -346,12 +335,9 @@ jobs: ## 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 to the summary listing each failed tool/action and - the error message, so maintainers can fix the workflow configuration. + If any errors prevented you from doing your job fully (tools that were + not available, git commands that failed, etc.), append a `### Errors` + section to the summary listing each failed action and the error message. ## CRITICAL: Return structured JSON output @@ -386,14 +372,12 @@ jobs: 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 = "";