ci: Rework Claude review workflow to use CLI directly
Replace claude-code-action with a direct claude CLI invocation. This gives us explicit control over settings, permissions, and output handling. Other changes: - Prepare per-commit git worktrees with pre-generated commit.patch and commit-message.txt files, replacing the pr-review branch approach. - Use structured JSON output (--output-format stream-json --json-schema) instead of having Claude write review-result.json directly. - Use jq instead of python3 for JSON prettification. - Add timeout-minutes: 60 to the review job. - List tool permissions explicitly instead of using a wildcard. - Fix sandbox filesystem paths to use regular paths instead of the "//" prefix.
This commit is contained in:
committed by
Daan De Meyer
parent
614509699a
commit
e0789b6001
+178
-206
@@ -139,6 +139,7 @@ jobs:
|
||||
review:
|
||||
runs-on: ubuntu-latest
|
||||
needs: setup
|
||||
timeout-minutes: 60
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -147,21 +148,29 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
# Need full history so git diff <sha>~1..<sha> works for all PR commits.
|
||||
# Need full history for git worktree add to work on 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: Download PR context
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
|
||||
with:
|
||||
name: pr-context.json
|
||||
|
||||
- name: Prettify PR context
|
||||
run: python3 -m json.tool pr-context.json > pr-context-pretty.json && mv pr-context-pretty.json pr-context.json
|
||||
run: |
|
||||
jq . pr-context.json > pr-context-pretty.json
|
||||
mv pr-context-pretty.json pr-context.json
|
||||
|
||||
- name: Prepare PR worktrees
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.setup.outputs.pr_number }}
|
||||
run: |
|
||||
git fetch origin "pull/${PR_NUMBER}/head"
|
||||
for sha in $(git log --reverse --format=%H HEAD..FETCH_HEAD); do
|
||||
git worktree add "worktrees/$sha" "$sha"
|
||||
git -C "worktrees/$sha" diff HEAD~..HEAD > "worktrees/$sha/commit.patch"
|
||||
git -C "worktrees/$sha" log -1 --format='%B' HEAD > "worktrees/$sha/commit-message.txt"
|
||||
done
|
||||
|
||||
- name: Install sandbox dependencies
|
||||
run: |
|
||||
@@ -175,226 +184,189 @@ jobs:
|
||||
role-session-name: GitHubActions-Claude-${{ github.run_id }}
|
||||
aws-region: us-east-1
|
||||
|
||||
- name: Install Claude Code
|
||||
run: curl -fsSL https://claude.ai/install.sh | bash
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@26ec041249acb0a944c0a47b6c0c13f05dbc5b44
|
||||
with:
|
||||
use_bedrock: "true"
|
||||
# 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 }}
|
||||
# Safe because the workflow's `if` condition already restricts
|
||||
# execution to trusted actors (MEMBER/OWNER/COLLABORATOR) or PRs
|
||||
# that a trusted actor explicitly labeled, and this job only has
|
||||
# read-only permissions.
|
||||
allowed_non_write_users: "*"
|
||||
track_progress: false
|
||||
show_full_output: "true"
|
||||
# Sandbox Bash commands to prevent network access and restrict
|
||||
# filesystem writes to the working directory.
|
||||
settings: |
|
||||
{
|
||||
"permissions": {
|
||||
"allow": ["*"]
|
||||
},
|
||||
"sandbox": {
|
||||
"enabled": true,
|
||||
"autoAllowBashIfSandboxed": true,
|
||||
"allowUnsandboxedCommands": false,
|
||||
"filesystem": {
|
||||
"allowWrite": ["//tmp", "//var/tmp"]
|
||||
}
|
||||
env:
|
||||
CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1"
|
||||
CLAUDE_CODE_USE_BEDROCK: "1"
|
||||
run: |
|
||||
mkdir -p ~/.claude
|
||||
|
||||
cat > ~/.claude/settings.json << 'SETTINGS'
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash",
|
||||
"Read",
|
||||
"Edit(//${{ github.workspace }}/**)",
|
||||
"Write(//${{ github.workspace }}/**)",
|
||||
"Grep",
|
||||
"Glob",
|
||||
"Agent",
|
||||
"Task",
|
||||
"TaskOutput",
|
||||
"ToolSearch"
|
||||
]
|
||||
},
|
||||
"sandbox": {
|
||||
"enabled": true,
|
||||
"autoAllowBashIfSandboxed": true,
|
||||
"allowUnsandboxedCommands": false,
|
||||
"filesystem": {
|
||||
"allowWrite": ["/tmp", "/var/tmp", "${{ github.workspace }}"]
|
||||
}
|
||||
}
|
||||
claude_args: |
|
||||
--model us.anthropic.claude-opus-4-6-v1
|
||||
--effort max
|
||||
--max-turns 200
|
||||
--disallowedTools "WebFetch,WebSearch"
|
||||
--setting-sources user
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ needs.setup.outputs.pr_number }}
|
||||
}
|
||||
SETTINGS
|
||||
|
||||
You are a code reviewer for the ${{ github.repository }} project. Review this pull request and
|
||||
produce a JSON result containing your review and write it to
|
||||
`review-result.json` in the repo root. Do NOT attempt to post
|
||||
comments yourself. You are in the upstream repo with the PR branch
|
||||
available as `pr-review`. Do not apply or merge the patch.
|
||||
You have no network access — all required context has been
|
||||
pre-fetched locally.
|
||||
|
||||
## Phase 1: Read context
|
||||
|
||||
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
|
||||
- `issue_comments` — array of issue comments on the PR 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 ALL inline review comments on the PR from the
|
||||
GitHub API. Use these as context, but observe the following rules:
|
||||
- Only re-check your own comments (user.login == "github-actions[bot]" and
|
||||
body starts with "Claude: "). Do NOT validate, re-raise, respond to, or
|
||||
duplicate comments from other authors.
|
||||
- Items checked off in the tracking comment (`- [x]`) are resolved. Do NOT
|
||||
re-check or re-raise review comments that correspond to resolved items.
|
||||
- You will need the `id` fields of your own unresolved comments 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 <sha>` or `git diff <sha>~1..<sha>` to access commit diffs.
|
||||
|
||||
## Phase 2: Review commits
|
||||
|
||||
Review every commit in the PR. Use subagents to parallelize the
|
||||
work — decide how many subagents to spawn and how to divide commits
|
||||
between them and the main conversation based on the number and size
|
||||
of commits. Very large commits can be assigned to multiple subagents
|
||||
for extra thoroughness. Always review some commits yourself so you
|
||||
have useful work to do while subagents run in the background. For
|
||||
single-commit PRs with small diffs, just review directly without
|
||||
subagents. For large single-commit PRs, have multiple subagents
|
||||
review it independently to maximize coverage.
|
||||
|
||||
IMPORTANT: Always spawn subagents with `isolation: "worktree"` so
|
||||
each gets its own git worktree. This prevents concurrent git
|
||||
operations from interfering with each other. Because worktrees
|
||||
do not include untracked files, first `git add pr-context.json`
|
||||
so it is available in worktrees.
|
||||
|
||||
Each reviewer (you or a subagent) uses `git show <sha>` or
|
||||
`git diff <sha>~1..<sha>` to fetch diffs, reads `pr-context.json`
|
||||
for PR context, and reads the codebase to verify findings.
|
||||
|
||||
Each reviewer reviews code quality, style, potential bugs, and security
|
||||
implications. It must return a JSON array of issues matching this schema:
|
||||
|
||||
```json
|
||||
{
|
||||
cat > review-schema.json << 'SCHEMA'
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["summary", "comments"],
|
||||
"properties": {
|
||||
"summary": { "type": "string" },
|
||||
"comments": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["path", "line", "severity", "body", "commit"],
|
||||
"properties": {
|
||||
"path": { "type": "string", "description": "File path relative to repo root" },
|
||||
"line": { "type": "integer", "description": "Diff line number (last line for multi-line)" },
|
||||
"side": { "enum": ["LEFT", "RIGHT"], "description": "Diff side: LEFT for deletions, RIGHT for additions/context (default: RIGHT)" },
|
||||
"start_line": { "type": "integer", "description": "First line of a multi-line comment range" },
|
||||
"start_side": { "enum": ["LEFT", "RIGHT"], "description": "Diff side for start_line" },
|
||||
"path": { "type": "string" },
|
||||
"line": { "type": "integer" },
|
||||
"side": { "enum": ["LEFT", "RIGHT"] },
|
||||
"start_line": { "type": "integer" },
|
||||
"start_side": { "enum": ["LEFT", "RIGHT"] },
|
||||
"severity": { "enum": ["must-fix", "suggestion", "nit"] },
|
||||
"body": { "type": "string", "description": "Review comment in markdown" },
|
||||
"commit": { "type": "string", "description": "SHA of the commit being reviewed" }
|
||||
"body": { "type": "string" },
|
||||
"commit": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"resolve": { "type": "array", "items": { "type": "integer" } }
|
||||
}
|
||||
}
|
||||
SCHEMA
|
||||
|
||||
cat > /tmp/review-prompt.txt << 'PROMPT'
|
||||
You are a code reviewer for the ${{ github.repository }} project.
|
||||
Review this pull request. All required context has been
|
||||
pre-fetched into local files.
|
||||
|
||||
## Phase 1: Review commits
|
||||
|
||||
Read `pr-context.json` from the repository root. `pr-context.json` contains
|
||||
PR metadata from the GitHub API. Rules for its `review_comments` field:
|
||||
- Only re-check your own comments (user.login == "github-actions[bot]" and
|
||||
body starts with "Claude: ").
|
||||
- Items checked off in the `tracking_comment` (`- [x]`) are resolved.
|
||||
- You will need the `id` fields of your own unresolved comments in Phase 2
|
||||
to populate the `resolve` array.
|
||||
- If `tracking_comment` is non-null, use it as the basis for your summary
|
||||
in Phase 2.
|
||||
|
||||
Then, list the directories in `worktrees/` — there is one per commit. Each
|
||||
worktree at `worktrees/<sha>/` contains the full source tree checked out at
|
||||
that commit, plus `commit.patch` (the diff) and `commit-message.txt`
|
||||
(the commit message). Spawn one
|
||||
review subagent per worktree, all in a single message so they run concurrently.
|
||||
Do NOT pre-compute diffs or read any other files before spawning — the subagents
|
||||
will do that themselves.
|
||||
|
||||
Each reviewer reviews design, code quality, style, potential bugs, and
|
||||
security implications.
|
||||
|
||||
Each subagent prompt must include:
|
||||
- Instructions to read `pr-context.json` in the repository root for context.
|
||||
- Instructions to read `review-schema.json` in the repository root and
|
||||
return a JSON array matching the `comments` items schema from that file.
|
||||
- The worktree path.
|
||||
- Instructions to read `commit-message.txt` and `commit.patch` in the
|
||||
worktree for the commit message and diff.
|
||||
- Instructions to verify every `line` and `start_line` value
|
||||
against the hunk ranges in `commit.patch` before returning.
|
||||
|
||||
## Phase 2: Collect, deduplicate, and summarize
|
||||
|
||||
After all reviews (yours and any subagents') are done:
|
||||
1. Collect all issues. Merge duplicates (same file, lines within 3 of each other, same problem).
|
||||
2. Drop low-confidence findings.
|
||||
3. 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.
|
||||
Populate the `resolve` array with the REST API `id` (integer) of existing
|
||||
review comments whose threads should be resolved. A thread should be resolved if:
|
||||
- The issue it raised has been addressed in the current PR (i.e. your review
|
||||
no longer flags it), or
|
||||
- The PR author (or another reviewer) left a reply disagreeing with or
|
||||
dismissing the comment.
|
||||
Only include the `id` of the **first** comment in each thread (the one that
|
||||
started the conversation). Do not resolve threads for issues that are still
|
||||
present and unaddressed.
|
||||
4. 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 #<number> (<HEAD SHA>)
|
||||
|
||||
<!-- claude-pr-review -->
|
||||
|
||||
### Must fix
|
||||
- [ ] **short title** — `path:line` — brief explanation
|
||||
|
||||
### Suggestions
|
||||
- [ ] **short title** — `path:line` — brief explanation
|
||||
|
||||
### Nits
|
||||
- [ ] **short title** — `path:line` — brief explanation
|
||||
```
|
||||
|
||||
The `commit` field MUST be the SHA of the commit being reviewed. Only
|
||||
comment on changes in that commit — not preceding commits.
|
||||
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.
|
||||
|
||||
`line` should be a line number from the diff **that appears inside a
|
||||
diff hunk**. GitHub rejects lines outside the diff context. `side`
|
||||
indicates which side of the diff (`LEFT` for deletions, `RIGHT` for
|
||||
additions or context lines); defaults to `RIGHT` if omitted. For
|
||||
multi-line comments, set `start_line` and `start_side` to the first
|
||||
line of the range and `line`/`side` to the last.
|
||||
**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.
|
||||
|
||||
Each reviewer 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.
|
||||
## Error tracking
|
||||
|
||||
## Phase 3: Collect, deduplicate, and summarize
|
||||
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.
|
||||
|
||||
After all reviews (yours and any subagents') are done:
|
||||
1. Collect all issues. Merge duplicates (same file, lines within 3 of each other, same problem).
|
||||
2. Drop low-confidence findings.
|
||||
3. 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.
|
||||
Populate the `resolve` array with the REST API `id` (integer) of existing
|
||||
review comments whose threads should be resolved. A thread should be resolved if:
|
||||
- The issue it raised has been addressed in the current PR (i.e. your review
|
||||
no longer flags it), OR
|
||||
- The PR author (or another reviewer) left a reply disagreeing with or
|
||||
dismissing the comment.
|
||||
Only include the `id` of the **first** comment in each thread (the one that
|
||||
started the conversation). Do NOT resolve threads for issues that are still
|
||||
present and unaddressed.
|
||||
4. 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.
|
||||
5. Write a `summary` field in markdown for a top-level tracking comment.
|
||||
## Review result
|
||||
|
||||
**If no existing tracking comment was found (first run):**
|
||||
Use this format:
|
||||
Produce your review result as structured output. The fields are:
|
||||
- `summary`: Markdown summary for the tracking comment.
|
||||
- `comments`: Array of review comments (same schema as the reviewer output above).
|
||||
- `resolve`: REST API IDs of review comment threads to resolve.
|
||||
PROMPT
|
||||
|
||||
```
|
||||
## Claude review of PR #<number> (<HEAD SHA>)
|
||||
claude \
|
||||
--model us.anthropic.claude-opus-4-6-v1 \
|
||||
--effort max \
|
||||
--max-turns 200 \
|
||||
--setting-sources user \
|
||||
--output-format stream-json \
|
||||
--json-schema "$(cat review-schema.json)" \
|
||||
--verbose \
|
||||
-p "$(cat /tmp/review-prompt.txt)" \
|
||||
| tee claude.json
|
||||
|
||||
<!-- claude-pr-review -->
|
||||
|
||||
### Must fix
|
||||
- [ ] **short title** — `path:line` — brief explanation
|
||||
|
||||
### Suggestions
|
||||
- [ ] **short title** — `path:line` — brief explanation
|
||||
|
||||
### Nits
|
||||
- [ ] **short title** — `path:line` — brief explanation
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
## Output formatting
|
||||
|
||||
Do NOT escape characters in `body` or `summary`. Write plain markdown — no
|
||||
backslash escaping of `!` or other characters. In particular, HTML comments
|
||||
like `<!-- ... -->` must be written verbatim, never as `<\!-- ... -->`.
|
||||
|
||||
## CRITICAL: Write review result to file
|
||||
|
||||
Your FINAL action must be to write `review-result.json` in the repo
|
||||
root. The file must contain a JSON object with the following schema:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["summary", "comments"],
|
||||
"properties": {
|
||||
"summary": { "type": "string", "description": "Markdown summary for the tracking comment" },
|
||||
"comments": { "description": "Array of review comments (same schema as the reviewer output above)" },
|
||||
"resolve": { "type": "array", "items": { "type": "integer" }, "description": "REST API IDs of review comment threads to resolve" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Do NOT attempt to post comments or use any MCP tools to modify the PR.
|
||||
jq '.structured_output | select(. != null)' claude.json > review-result.json
|
||||
|
||||
- name: Upload review result
|
||||
if: always()
|
||||
|
||||
Reference in New Issue
Block a user