From: Daan De Meyer Date: Tue, 31 Mar 2026 19:18:12 +0000 (+0200) Subject: ci: Rework Claude review workflow to use CLI directly X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e0789b6001a5f48dfe978491fce7f40d0bf6ccec;p=thirdparty%2Fsystemd.git 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. --- diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index f05ea14d2d5..eea4a5ed337 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -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 ~1.. 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 }} - - 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 ` or `git diff ~1..` 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 ` or - `git diff ~1..` 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 - { + } + SETTINGS + + 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//` 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 # () - 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 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. - - 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. - - ## Phase 3: 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. 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. - - **If no existing tracking comment was found (first run):** - Use this format: - - ``` - ## Claude review of PR # () - - - - ### 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" } - } - } + + + ### Must fix + - [ ] **short title** — `path:line` — brief explanation + + ### Suggestions + - [ ] **short title** — `path:line` — brief explanation + + ### Nits + - [ ] **short title** — `path:line` — brief explanation ``` - Do NOT attempt to post comments or use any MCP tools to modify the PR. + 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. + + ## Review result + + 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 \ + --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 + + jq '.structured_output | select(. != null)' claude.json > review-result.json - name: Upload review result if: always()