Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 100 additions & 3 deletions .github/actions/pr-review/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ inputs:
description: "Review prompt profile to use: connector or general"
required: false
default: connector
summary_marker:
description: "Optional override for the review summary heading: a single-line Markdown heading of the form '### ...:'. Empty uses the review_prompt profile's heading."
required: false
default: ""

runs:
using: composite
Expand All @@ -27,21 +31,51 @@ runs:
shell: bash
env:
REVIEW_PROMPT: ${{ inputs.review_prompt }}
SUMMARY_MARKER: ${{ inputs.summary_marker }}
run: |
# Captured before any review work: the stamp/submit gates require the
# summary comment to have been created/updated at or after this moment,
# so a successful agent step can never launder a prior run's summary
# into this run's verdict.
echo "review_run_started_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "${GITHUB_OUTPUT}"
case "${REVIEW_PROMPT}" in
""|"connector")
echo "built_in_mixins=connector" >> "${GITHUB_OUTPUT}"
echo "summary_heading=### Connector PR Review:" >> "${GITHUB_OUTPUT}"
summary_heading="### Connector PR Review:"
;;
"general")
echo "built_in_mixins=" >> "${GITHUB_OUTPUT}"
echo "summary_heading=### General PR Review:" >> "${GITHUB_OUTPUT}"
summary_heading="### General PR Review:"
;;
*)
echo "::error::review_prompt must be 'connector' or 'general'"
exit 1
;;
esac
if [ -n "${SUMMARY_MARKER}" ]; then
# The override is written to GITHUB_OUTPUT below, so validate before
# writing: it must be one non-empty single-line Markdown heading
# (### ...:). A newline would smuggle extra output lines; anything
# else could never match a real summary comment.
case "${SUMMARY_MARKER}" in
*$'\n'*|*$'\r'*)
echo "::error::summary_marker must be a single line"
exit 1
;;
esac
if ! printf '%s' "${SUMMARY_MARKER}" | grep -qE '^### [^[:space:]].*:$'; then
echo "::error::summary_marker must be a Markdown heading of the form '### ...:'"
exit 1
fi
for reserved in "### Connector PR Review:" "### General PR Review:" "### PR Review:"; do
if [[ "${SUMMARY_MARKER}" != "${reserved}" && "${SUMMARY_MARKER}" == *"${reserved}"* ]]; then
echo "::error::summary_marker must not embed a reserved review heading"
exit 1
fi
done
summary_heading="${SUMMARY_MARKER}"
fi
echo "summary_heading=${summary_heading}" >> "${GITHUB_OUTPUT}"
- name: Fetch PR context
shell: bash
env:
Expand Down Expand Up @@ -87,15 +121,77 @@ runs:
echo "${DELIM}"
} >> "${GITHUB_ENV}"
- name: Run Claude PR Review
id: claude_review
uses: anthropics/claude-code-action@9ca9355b36297178e28d37c799d1c9c8a28e6507 # main: Claude Code 2.1.280
with:
anthropic_api_key: ${{ inputs.anthropic_api_key }}
github_token: ${{ inputs.github_token }}
include_fix_links: true
use_sticky_comment: true
allowed_bots: "*"
claude_args: --model claude-opus-5-5 --max-turns 100 --allowedTools "Read,Glob,Grep,Skill,Task,mcp__github_inline_comment__create_inline_comment,mcp__github_comment__update_claude_comment,Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr review:*),Bash(gh api:*)"
# --setting-sources user: do NOT load the reviewed repo's project/local
# settings. Those register the repo's own .claude/agents and .claude/commands
# into this run, which is wrong for a read-only CI reviewer: a project
# agent's `model:` frontmatter overrides --model (review turns would
# silently run on a model the workflow never pinned), and write-oriented
# repo agents inherit the action's --permission-mode acceptEdits in a
# review that must not mutate the tree.
# This also unregisters the repo's skills, so Skill is hard-denied below —
# review criteria already reach the prompt inline via load-review-criteria.
#
# --strict-mcp-config: use only the MCP servers claude-code-action itself
# passes via --mcp-config, ignoring any .mcp.json in the reviewed repo.
#
# Loop/scheduling tools (ScheduleWakeup, Cron*) are hard-denied: in a
# one-shot CI review they are meaningless and harmful — ScheduleWakeup
# schedules a wakeup no event loop will fire, so the agent ends its turn
# waiting and posts no summary.
#
# Bash(gh pr review:*) is gone from the allow-list: CI submits the verdict
# deterministically (submit-verdict-review.py) instead of relying on the
# agent to run a trailing command, which Claude Code upgrades have
# repeatedly regressed (the agent stops after the summary and the formal
# review is never submitted).
claude_args: --model claude-opus-5-5 --max-turns 100 --setting-sources user --strict-mcp-config --disallowedTools "Skill,ScheduleWakeup,CronCreate,CronDelete,CronList" --allowedTools "Read,Glob,Grep,Task,mcp__github_inline_comment__create_inline_comment,mcp__github_comment__update_claude_comment,Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh api:*)"
prompt: ${{ env.REVIEW_PROMPT }}
- name: Stamp review-state on summary comment
id: stamp
# Bind the sticky summary comment to the reviewed HEAD deterministically.
# submit-verdict-review.py requires a <!-- review-state --> marker matching
# HEAD, and fetch-pr-context.py requires its workflow_ref to match — but the
# agent does not emit the marker reliably, so state detection failed closed
# (every run fell back to full mode) and no verdict could be submitted.
# This runs only after a successful agent review of the checked-out head, so
# git HEAD is exactly what was reviewed.
if: steps.claude_review.conclusion == 'success'
shell: bash
env:
GH_TOKEN: ${{ inputs.github_token }}
PR_NUMBER: ${{ inputs.pr_number }}
SUMMARY_MARKER: ${{ steps.review-config.outputs.summary_heading }}
REVIEW_RUN_STARTED_AT: ${{ steps.review-config.outputs.review_run_started_at }}
run: python3 ${{ github.action_path }}/scripts/stamp-review-state.py
- name: Submit verdict review
id: submit_verdict
# CI submits the formal PR review from the **Blocking Issues: N** count in
# the agent's summary comment, rather than relying on the agent to run
# `gh pr review` itself (that trailing step regressed with Claude Code
# upgrades — the agent stopped after posting the summary, so PRs got a
# quiet comment and no blocking review). Baseline mode only: request
# changes on blocking findings, neutral comment otherwise — never approves.
# Gates: the summary must be fresh (this run), final (not provisional),
# owned by this workflow, bound to the reviewed commit, and the live PR
# head must not have moved; the review is posted via the REST API with an
# explicit commit_id. Any gate failure exits nonzero — a broken review is
# a loud red check, not silent green.
if: steps.claude_review.conclusion == 'success'
shell: bash
env:
GH_TOKEN: ${{ inputs.github_token }}
PR_NUMBER: ${{ inputs.pr_number }}
SUMMARY_MARKER: ${{ steps.review-config.outputs.summary_heading }}
REVIEW_RUN_STARTED_AT: ${{ steps.review-config.outputs.review_run_started_at }}
run: python3 ${{ github.action_path }}/scripts/submit-verdict-review.py
- name: Upload review context artifacts
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
Expand All @@ -104,6 +200,7 @@ runs:
path: |
.github/pr-context.json
.github/resolved-threads.json
.github/prior-findings.json
.github/incremental.diff
.github/review-criteria.md
.github/review-criteria.json
Expand Down
Loading