Skip to content

ci: use canonical dogfood URL #182

ci: use canonical dogfood URL

ci: use canonical dogfood URL #182

name: Claude code review
# Runs Claude Code as an automated code reviewer on every PR.
# Posts a sticky PR comment with the model's verdict + punch list.
# The comment is replaced (not duplicated) on every rerun via a hidden
# marker, so downstream automation can poll for the latest verdict.
#
# Requires repository secret: ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN.
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
branches: [main]
permissions:
contents: read
pull-requests: write
issues: write
id-token: write
concurrency:
group: claude-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
review:
name: Claude review
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- id: claude
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
prompt: |
Review this pull request as a senior Rust reviewer for the
Plumb project. Read `AGENTS.md` and `.agents/rules/` first —
Plumb has non-obvious invariants (determinism, layering,
no-bypass strictness) documented there.
Focus, in this order:
1. Determinism invariants (no wall-clock, no HashMap in
outputs, stable sort key) — blocker if violated.
2. Workspace layering — plumb-core has no internal deps;
unsafe is forbidden outside plumb-cdp.
3. Error handling — library crates use thiserror; no
unwrap/expect in libraries.
4. Test coverage — new public APIs need tests.
5. Documentation — every public item has a doc; fallible
fns have `# Errors`.
End your review with exactly one line:
Verdict: APPROVE
Verdict: REQUEST_CHANGES
Verdict: BLOCK
Above the verdict, give a punch list with file:line
references. Skip nits unless they stack; focus on
substantive issues.
claude_args: |
--max-turns 20
--model claude-sonnet-4-6
--allowed-tools "Bash(cargo *),Bash(git diff:*),Bash(git log:*),Read,Glob,Grep"
--disallowed-tools "Bash(git push:*),Bash(git commit:*)"
# The action above runs Claude in automation mode (with `prompt:`),
# which by design does not post the model's response anywhere — it
# only buffers inline review comments. We extract the final assistant
# message from the action's execution log and post it as a sticky PR
# comment so the verdict is visible to humans and to the gh-issue
# poll-pr automation. The hidden marker keeps each rerun replacing
# the prior comment instead of stacking new ones.
- name: Post verdict as sticky PR comment
if: ${{ !cancelled() && steps.claude.outputs.execution_file != '' }}
env:
EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
MARKER: '<!-- plumb:claude-review-verdict -->'
run: |
set -euo pipefail
if [ ! -s "$EXECUTION_FILE" ]; then
echo "::warning::Claude execution file is missing or empty; skipping verdict comment."
exit 0
fi
# The execution file is a JSON array of Turn objects from
# Claude Code's stream-json output. Prefer the final `result`
# turn (which captures the model's last assistant message
# verbatim); fall back to the last assistant text turn.
REVIEW=$(jq -r '
(
([.[]? | select(.type == "result") | .result // empty] | last)
//
([.[]? | select(.type == "assistant")
| (.message.content // [])[]?
| select(.type == "text") | .text] | last)
//
""
)
' "$EXECUTION_FILE")
if [ -z "$REVIEW" ]; then
echo "::warning::Could not extract a final assistant message; skipping verdict comment."
exit 0
fi
BODY_FILE="$RUNNER_TEMP/claude-review-body.md"
{
printf '%s\n\n' "$MARKER"
printf '%s\n' "$REVIEW"
} > "$BODY_FILE"
EXISTING_ID=$(gh api --paginate \
"repos/${GH_REPO}/issues/${PR_NUMBER}/comments" \
--jq "map(select(.body | contains(\"${MARKER}\"))) | .[0].id // empty")
if [ -n "$EXISTING_ID" ]; then
echo "Updating prior verdict comment ${EXISTING_ID}"
jq -n --rawfile body "$BODY_FILE" '{body: $body}' \
| gh api -X PATCH "repos/${GH_REPO}/issues/comments/${EXISTING_ID}" --input -
else
echo "Creating new verdict comment"
jq -n --rawfile body "$BODY_FILE" '{body: $body}' \
| gh api -X POST "repos/${GH_REPO}/issues/${PR_NUMBER}/comments" --input -
fi