mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-07 00:01:49 +00:00
* fix(ci): use a random delimiter for the review prompt env var * fix(ci): use a random delimiter for the review command extra_prompt output
209 lines
9.1 KiB
YAML
209 lines
9.1 KiB
YAML
name: Claude Auto Review
|
|
|
|
on:
|
|
pull_request:
|
|
types: [ready_for_review, opened]
|
|
workflow_call:
|
|
inputs:
|
|
pr_number:
|
|
description: 'PR number to review'
|
|
required: true
|
|
type: number
|
|
extra_prompt:
|
|
description: 'Additional reviewer instructions appended to the standard review prompt'
|
|
required: false
|
|
type: string
|
|
default: ''
|
|
triggered_by:
|
|
description: 'GitHub username that triggered this review (for audit only)'
|
|
required: false
|
|
type: string
|
|
default: ''
|
|
secrets:
|
|
CLAUDE_CODE_OAUTH_TOKEN:
|
|
required: true
|
|
WINDMILL_EE_PRIVATE_ACCESS:
|
|
required: false
|
|
|
|
concurrency:
|
|
group: claude-review-${{ inputs.pr_number || github.event.pull_request.number }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
auto-review:
|
|
runs-on: ubuntu-latest
|
|
# A non-fork PR (head.repo.fork == false) can only be opened by someone with push
|
|
# access to this repo, so fork==false already enforces write access. Do NOT re-add
|
|
# an author_association gate: the pull_request webhook payload reports private org
|
|
# members as CONTRIBUTOR/NONE (only public members show as MEMBER), which silently
|
|
# skips auto-review for every private member.
|
|
if: |
|
|
github.event_name == 'workflow_call' ||
|
|
(
|
|
(github.event.pull_request.draft == false || github.event.pull_request.ready_for_review == true) &&
|
|
github.event.pull_request.head.repo.fork == false
|
|
)
|
|
permissions:
|
|
contents: read
|
|
pull-requests: read
|
|
id-token: write
|
|
steps:
|
|
# An agent-driven PR flips to ready only after a clean /review round on a
|
|
# draft, marked by an author comment naming the head SHA (pr skill, "Review
|
|
# rounds"). Re-reviewing that same head on ready_for_review is redundant.
|
|
# The marker alone is author attestation, so also require reviewer evidence:
|
|
# a Codex review (posted by github-actions[bot], not forgeable by the author)
|
|
# that predates the marker and carries a non-blocking verdict. Comment-
|
|
# triggered (workflow_call) and opened runs never skip. Keep the three
|
|
# copies of this check in sync (pr-ready-review / codex-pr-review /
|
|
# pi-pr-review); a shared local action would need the repo checked out
|
|
# before the check, which the codex/pi fork paths deliberately avoid.
|
|
- name: Check clean-round marker
|
|
id: marker
|
|
if: github.event_name == 'pull_request' && github.event.action == 'ready_for_review'
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
REPO: ${{ github.repository }}
|
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
|
run: |
|
|
# Fetch failures fail open (skip=false): an API hiccup must run the
|
|
# review, never skip it or fail the job.
|
|
COMMENTS=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments?per_page=100" --paginate | jq -s '[.[][]]') || COMMENTS='[]'
|
|
MARKER_TIME=$(jq -r --arg author "$PR_AUTHOR" --arg marker "✅ Review round clean @ $HEAD_SHA" \
|
|
'[.[] | select(.user.login == $author) | select(.body | contains($marker)) | .created_at] | min // empty' <<<"$COMMENTS")
|
|
CODEX_VERDICT=''
|
|
if [ -n "$MARKER_TIME" ]; then
|
|
# Only Codex evidence that predates the marker counts: the ready-
|
|
# triggered Codex run itself posts after the flip and must not vouch
|
|
# for a sibling reviewer's skip.
|
|
CODEX_VERDICT=$(jq -r --arg mt "$MARKER_TIME" \
|
|
'[.[] | select(.user.login == "github-actions[bot]") | select(.body | contains("## Codex Review")) | select(.created_at < $mt)] | last | .body // ""' <<<"$COMMENTS" \
|
|
| grep -m1 -oE '(Good to merge|Mergeable, but should ideally address nits|Should address issues before merging)' || true)
|
|
fi
|
|
if [ -n "$MARKER_TIME" ] && [ -n "$CODEX_VERDICT" ] && [ "$CODEX_VERDICT" != "Should address issues before merging" ]; then
|
|
echo "Clean review round marker found for $HEAD_SHA with pre-marker non-blocking Codex verdict; skipping redundant review."
|
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Checkout repository
|
|
if: steps.marker.outputs.skip != 'true'
|
|
uses: actions/checkout@v5
|
|
with:
|
|
fetch-depth: 1
|
|
|
|
- name: Check EE access
|
|
if: steps.marker.outputs.skip != 'true'
|
|
id: ee
|
|
env:
|
|
EE_TOKEN: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
|
|
run: |
|
|
if [ -n "$EE_TOKEN" ]; then
|
|
echo "available=true" >> "$GITHUB_OUTPUT"
|
|
echo "ee_repo_ref=$(cat ./backend/ee-repo-ref.txt)" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "available=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Checkout EE repository
|
|
if: steps.ee.outputs.available == 'true'
|
|
uses: actions/checkout@v5
|
|
with:
|
|
repository: windmill-labs/windmill-ee-private
|
|
path: ./windmill-ee-private
|
|
ref: ${{ steps.ee.outputs.ee_repo_ref }}
|
|
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
|
|
fetch-depth: 1
|
|
|
|
- name: Substitute EE code
|
|
if: steps.ee.outputs.available == 'true'
|
|
run: ./backend/substitute_ee_code.sh --copy --dir ./windmill-ee-private
|
|
|
|
- name: Resolve PR number
|
|
if: steps.marker.outputs.skip != 'true'
|
|
id: resolve
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
REPO: ${{ github.repository }}
|
|
INPUT_PR_NUMBER: ${{ inputs.pr_number }}
|
|
EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
EVENT_PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
|
run: |
|
|
if [ -n "$INPUT_PR_NUMBER" ]; then
|
|
PR_NUMBER="$INPUT_PR_NUMBER"
|
|
PR_AUTHOR=$(gh api "repos/$REPO/pulls/$PR_NUMBER" --jq '.user.login')
|
|
else
|
|
PR_NUMBER="$EVENT_PR_NUMBER"
|
|
PR_AUTHOR="$EVENT_PR_AUTHOR"
|
|
fi
|
|
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
|
|
echo "pr_author=$PR_AUTHOR" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Fetch prior PR discussion
|
|
if: steps.marker.outputs.skip != 'true'
|
|
id: prior
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
REPO: ${{ github.repository }}
|
|
PR_NUMBER: ${{ steps.resolve.outputs.pr_number }}
|
|
run: |
|
|
gh api "repos/$REPO/issues/$PR_NUMBER/comments?per_page=100" \
|
|
--jq '[.[] | {user: .user.login, created_at: .created_at, body: (.body | .[:4000])}] | sort_by(.created_at) | .[-20:]' \
|
|
> prior-comments.json || echo "[]" > prior-comments.json
|
|
jq -r '
|
|
if length == 0 then ""
|
|
else
|
|
"## Prior PR discussion (most recent up to 20 comments)\n\nIf you have already reviewed this PR (look for your own earlier comment), focus on what changed since then per the diff and respect any decisions the human made in replies. Do not re-flag findings the human already pushed back on.\n\n" +
|
|
(map("### @\(.user) (\(.created_at))\n\n\(.body)") | join("\n\n---\n\n"))
|
|
end
|
|
' prior-comments.json > prior-comments.md
|
|
|
|
- name: Read review prompt
|
|
if: steps.marker.outputs.skip != 'true'
|
|
id: review-prompt
|
|
env:
|
|
EXTRA_PROMPT: ${{ inputs.extra_prompt }}
|
|
run: |
|
|
# prior-comments.md is PR comment text verbatim, and commenting needs no write access.
|
|
# With a fixed delimiter, a comment containing a bare `EOF` line closes the block early:
|
|
# the step dies, and whatever follows in that comment is read as further environment
|
|
# assignments for the rest of this job, which holds the review tokens. Hence a random
|
|
# delimiter, per GitHub's guidance for untrusted multiline values.
|
|
delimiter="REVIEW_PROMPT_EOF_$(openssl rand -hex 16)"
|
|
{
|
|
echo "REVIEW_PROMPT<<$delimiter"
|
|
cat REVIEW.md
|
|
echo ''
|
|
cat .claude/review-prompt.md
|
|
if [ -n "$EXTRA_PROMPT" ]; then
|
|
echo ''
|
|
echo '## Additional reviewer instructions'
|
|
echo ''
|
|
printf '%s\n' "$EXTRA_PROMPT"
|
|
fi
|
|
if [ -s prior-comments.md ]; then
|
|
echo ''
|
|
cat prior-comments.md
|
|
fi
|
|
echo "$delimiter"
|
|
} >> "$GITHUB_ENV"
|
|
|
|
- name: Automatic PR Review
|
|
if: steps.marker.outputs.skip != 'true'
|
|
uses: anthropics/claude-code-action@v1
|
|
with:
|
|
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
track_progress: true
|
|
prompt: |
|
|
REPO: ${{ github.repository }}
|
|
PR NUMBER: ${{ steps.resolve.outputs.pr_number }}
|
|
PR AUTHOR: ${{ steps.resolve.outputs.pr_author }}
|
|
|
|
${{ env.REVIEW_PROMPT }}
|
|
claude_args: |
|
|
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"
|
|
--model claude-opus-5
|