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
315 lines
14 KiB
YAML
315 lines
14 KiB
YAML
name: PR Review Commands
|
|
|
|
on:
|
|
issue_comment:
|
|
types: [created]
|
|
|
|
jobs:
|
|
parse:
|
|
if: github.event.issue.pull_request != null
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
command: ${{ steps.parse.outputs.command }}
|
|
extra_prompt: ${{ steps.parse.outputs.extra_prompt }}
|
|
steps:
|
|
- name: Parse command from comment
|
|
id: parse
|
|
env:
|
|
BODY: ${{ github.event.comment.body }}
|
|
run: |
|
|
FIRST_LINE=$(printf '%s' "$BODY" | head -n 1 | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')
|
|
FIRST_WORD=${FIRST_LINE%% *}
|
|
case "$FIRST_WORD" in
|
|
/review|/codex|/pi|/claude)
|
|
COMMAND="${FIRST_WORD#/}"
|
|
REMAINDER_FIRST_LINE=${FIRST_LINE#"$FIRST_WORD"}
|
|
REMAINDER_FIRST_LINE=${REMAINDER_FIRST_LINE# }
|
|
REST=$(printf '%s' "$BODY" | tail -n +2)
|
|
# The value is the comment body, which anyone can write. A fixed delimiter lets a
|
|
# comment close the block early and have the rest of itself read as further step
|
|
# outputs, so the delimiter has to be unguessable.
|
|
delimiter="EXTRA_EOF_$(openssl rand -hex 16)"
|
|
{
|
|
echo "command=$COMMAND"
|
|
echo "extra_prompt<<$delimiter"
|
|
if [ -n "$REMAINDER_FIRST_LINE" ]; then
|
|
printf '%s\n' "$REMAINDER_FIRST_LINE"
|
|
fi
|
|
if [ -n "$REST" ]; then
|
|
printf '%s\n' "$REST"
|
|
fi
|
|
echo "$delimiter"
|
|
} >> "$GITHUB_OUTPUT"
|
|
;;
|
|
*)
|
|
echo "command=" >> "$GITHUB_OUTPUT"
|
|
;;
|
|
esac
|
|
|
|
# author_association misses private org members; check-access resolves them via the
|
|
# internal app token. Both are OR'd so public members still pass instantly.
|
|
check-access:
|
|
needs: [parse]
|
|
if: needs.parse.outputs.command != ''
|
|
uses: ./.github/workflows/check-write-access.yml
|
|
with:
|
|
username: ${{ github.event.comment.user.login }}
|
|
secrets: inherit
|
|
|
|
acknowledge:
|
|
needs: [parse, check-access]
|
|
if: |
|
|
needs.parse.outputs.command != '' &&
|
|
(
|
|
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) ||
|
|
needs.check-access.outputs.authorized == 'true'
|
|
)
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
issues: write
|
|
pull-requests: write
|
|
steps:
|
|
- name: React to comment with eyes
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
REPO: ${{ github.repository }}
|
|
COMMENT_ID: ${{ github.event.comment.id }}
|
|
run: |
|
|
gh api -X POST \
|
|
"/repos/$REPO/issues/comments/$COMMENT_ID/reactions" \
|
|
-f content=eyes >/dev/null
|
|
|
|
# Decide, per agent, whether to launch a fresh run, re-run in place, or skip. A push
|
|
# already auto-triggers codex/pi (and claude on open) against the PR head. Relaunching
|
|
# via this issue_comment path both cancels those in-flight auto runs (shared concurrency
|
|
# group) AND lands the new run's status on main — issue_comment runs never attach a
|
|
# check to the PR head — leaving the PR showing only a cancelled review. So for every
|
|
# command, launch an agent only when nothing covers the head commit; if the head's run
|
|
# was cancelled/failed, re-run it in place (a re-run keeps the original pull_request
|
|
# event, so its checks re-attach to the PR head); skip when a running or successful run
|
|
# already covers it. `/review` applies this to all three agents; `/codex`, `/pi`,
|
|
# `/claude` apply the same decision to just their own agent.
|
|
plan:
|
|
needs: [parse, check-access]
|
|
if: |
|
|
needs.parse.outputs.command != '' &&
|
|
(
|
|
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) ||
|
|
needs.check-access.outputs.authorized == 'true'
|
|
)
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
actions: write
|
|
pull-requests: read
|
|
statuses: write
|
|
outputs:
|
|
head_sha: ${{ steps.plan.outputs.head_sha }}
|
|
launch_codex: ${{ steps.plan.outputs.launch_codex }}
|
|
launch_pi: ${{ steps.plan.outputs.launch_pi }}
|
|
launch_claude: ${{ steps.plan.outputs.launch_claude }}
|
|
steps:
|
|
- name: Decide per-agent launch vs re-run for the head commit
|
|
id: plan
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
REPO: ${{ github.repository }}
|
|
PR_NUMBER: ${{ github.event.issue.number }}
|
|
COMMAND: ${{ needs.parse.outputs.command }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
HEAD_SHA=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json headRefOid --jq '.headRefOid')
|
|
echo "PR #$PR_NUMBER head: $HEAD_SHA"
|
|
echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT"
|
|
|
|
RUN_URL="$GITHUB_SERVER_URL/$REPO/actions/runs/$GITHUB_RUN_ID"
|
|
|
|
# A fresh launch runs from this issue_comment workflow (associated with main),
|
|
# so it never appears in the PR-head run query below and its own check lands on
|
|
# main, not the head. To keep fresh launches idempotent per head, mark the head
|
|
# SHA with a `review-launch/<agent>` commit status at launch; the `finalize` job
|
|
# resolves it to success/failure. A prior launch's status covering the head lets
|
|
# a second comment skip instead of relaunching (which would cancel the first via
|
|
# the reviewer's shared concurrency group). All status calls are best-effort — a
|
|
# GitHub API hiccup must degrade to a relaunch, never abort the decision.
|
|
mark_launch() {
|
|
agent="$1"
|
|
gh api -X POST "repos/$REPO/statuses/$HEAD_SHA" \
|
|
-f state=pending -f "context=review-launch/$agent" -f "target_url=$RUN_URL" \
|
|
-f "description=Review launched via /$COMMAND" >/dev/null 2>&1 || true
|
|
}
|
|
|
|
# Returns "covered" if a prior fresh launch (this or an earlier comment run)
|
|
# already covers the head: a success status, or a pending status whose launching
|
|
# run is still alive. A pending whose run has completed is stale (that run
|
|
# crashed before finalize) and does not count.
|
|
launch_coverage() {
|
|
agent="$1"
|
|
st_json=$(gh api "repos/$REPO/commits/$HEAD_SHA/statuses" \
|
|
--jq "[.[] | select(.context == \"review-launch/$agent\")] | first // empty" 2>/dev/null || true)
|
|
[ -n "$st_json" ] || return 0
|
|
state=$(jq -r '.state // empty' <<<"$st_json" 2>/dev/null || true)
|
|
[ "$state" = success ] && { echo covered; return 0; }
|
|
[ "$state" = pending ] || return 0
|
|
target=$(jq -r '.target_url // empty' <<<"$st_json" 2>/dev/null || true)
|
|
run_id=$(printf '%s' "$target" | grep -oE '[0-9]+$' || true)
|
|
if [ -n "$run_id" ]; then
|
|
run_state=$(gh run view "$run_id" --repo "$REPO" --json status --jq '.status' 2>/dev/null || true)
|
|
[ "$run_state" = completed ] && return 0 # stale pending -> not covered
|
|
fi
|
|
echo covered
|
|
}
|
|
|
|
decide() {
|
|
wf="$1"; key="$2"; agent="$3"
|
|
if [ "$(launch_coverage "$agent")" = covered ]; then
|
|
echo "$key: a prior launch already covers $HEAD_SHA (review-launch/$agent) -> skip"
|
|
echo "$key=false" >> "$GITHUB_OUTPUT"
|
|
return
|
|
fi
|
|
# `--commit` matches runs whose head SHA is the PR head. Auto reviews run on
|
|
# `pull_request` against that SHA; `/review` (issue_comment) runs execute on
|
|
# main, so they never match and are not counted as covering the head commit.
|
|
runs=$(gh run list --repo "$REPO" --workflow "$wf" --commit "$HEAD_SHA" --limit 40 \
|
|
--json databaseId,status,conclusion)
|
|
# Healthy = still running, or completed successfully: a review already
|
|
# covers this commit, so skip.
|
|
healthy=$(jq -r '[.[] | select(.status != "completed" or .conclusion == "success")] | length' <<<"$runs")
|
|
if [ "$healthy" -gt 0 ]; then
|
|
echo "$key: a running or successful review already covers $HEAD_SHA -> skip"
|
|
echo "$key=false" >> "$GITHUB_OUTPUT"
|
|
return
|
|
fi
|
|
# Re-run only genuinely interrupted runs (cancelled/failed/timed out) in
|
|
# place, so their checks re-attach to the PR head instead of posting on
|
|
# main. A `skipped` run produced no review and would just skip again (it is
|
|
# the draft/fork gate), so it does not count — fall through to a fresh launch.
|
|
retry_id=$(jq -r '[.[] | select(.status == "completed" and (.conclusion == "cancelled" or .conclusion == "failure" or .conclusion == "timed_out"))] | sort_by(.databaseId) | last | .databaseId // empty' <<<"$runs")
|
|
if [ -n "$retry_id" ]; then
|
|
if gh run rerun "$retry_id" --repo "$REPO" >/dev/null 2>&1; then
|
|
echo "$key: re-ran interrupted run $retry_id (re-attaches to PR head)"
|
|
echo "$key=false" >> "$GITHUB_OUTPUT"
|
|
return
|
|
fi
|
|
echo "$key: re-run of $retry_id failed -> fresh launch"
|
|
mark_launch "$agent"
|
|
echo "$key=true" >> "$GITHUB_OUTPUT"
|
|
return
|
|
fi
|
|
echo "$key: no usable review for $HEAD_SHA -> launch"
|
|
mark_launch "$agent"
|
|
echo "$key=true" >> "$GITHUB_OUTPUT"
|
|
}
|
|
|
|
# `/review` targets all three agents; `/codex`, `/pi`, `/claude` target only
|
|
# their own. A non-targeted agent is left untouched (no launch, no re-run).
|
|
decide_if_targeted() {
|
|
wf="$1"; key="$2"; agent="$3"
|
|
if [ "$COMMAND" = review ] || [ "$COMMAND" = "$agent" ]; then
|
|
decide "$wf" "$key" "$agent"
|
|
else
|
|
echo "$key: /$COMMAND does not target $agent -> skip"
|
|
echo "$key=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
}
|
|
|
|
decide_if_targeted codex-pr-review.yml launch_codex codex
|
|
decide_if_targeted pi-pr-review.yml launch_pi pi
|
|
decide_if_targeted pr-ready-review.yml launch_claude claude
|
|
|
|
claude:
|
|
needs: [parse, check-access, plan]
|
|
if: |
|
|
(
|
|
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) ||
|
|
needs.check-access.outputs.authorized == 'true'
|
|
) &&
|
|
needs.plan.outputs.launch_claude == 'true'
|
|
permissions:
|
|
contents: read
|
|
pull-requests: read
|
|
id-token: write
|
|
uses: ./.github/workflows/pr-ready-review.yml
|
|
with:
|
|
pr_number: ${{ github.event.issue.number }}
|
|
extra_prompt: ${{ needs.parse.outputs.extra_prompt }}
|
|
triggered_by: ${{ github.event.comment.user.login }}
|
|
secrets:
|
|
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
WINDMILL_EE_PRIVATE_ACCESS: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
|
|
|
|
codex:
|
|
needs: [parse, check-access, plan]
|
|
if: |
|
|
(
|
|
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) ||
|
|
needs.check-access.outputs.authorized == 'true'
|
|
) &&
|
|
needs.plan.outputs.launch_codex == 'true'
|
|
permissions:
|
|
contents: read
|
|
issues: write
|
|
pull-requests: write
|
|
uses: ./.github/workflows/codex-pr-review.yml
|
|
with:
|
|
pr_number: ${{ github.event.issue.number }}
|
|
extra_prompt: ${{ needs.parse.outputs.extra_prompt }}
|
|
triggered_by: ${{ github.event.comment.user.login }}
|
|
secrets:
|
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
CODEX_AUTH_JSON: ${{ secrets.CODEX_AUTH_JSON }}
|
|
WINDMILL_EE_PRIVATE_ACCESS: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
|
|
|
|
pi:
|
|
needs: [parse, check-access, plan]
|
|
if: |
|
|
(
|
|
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) ||
|
|
needs.check-access.outputs.authorized == 'true'
|
|
) &&
|
|
needs.plan.outputs.launch_pi == 'true'
|
|
permissions:
|
|
contents: read
|
|
issues: write
|
|
pull-requests: write
|
|
uses: ./.github/workflows/pi-pr-review.yml
|
|
with:
|
|
pr_number: ${{ github.event.issue.number }}
|
|
extra_prompt: ${{ needs.parse.outputs.extra_prompt }}
|
|
triggered_by: ${{ github.event.comment.user.login }}
|
|
secrets:
|
|
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
|
|
WINDMILL_EE_PRIVATE_ACCESS: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
|
|
|
|
# Resolve the `review-launch/<agent>` head statuses that `plan` set to pending, so a
|
|
# fresh launch's outcome is visible on the PR head (not just on main) and never lingers
|
|
# as a stale pending check. Targets the exact SHA `plan` launched against, so a push
|
|
# that moved the head mid-review does not stamp a status on the new head.
|
|
finalize:
|
|
needs: [plan, claude, codex, pi]
|
|
if: always() && needs.plan.result == 'success' && needs.plan.outputs.head_sha != ''
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
statuses: write
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
REPO: ${{ github.repository }}
|
|
HEAD_SHA: ${{ needs.plan.outputs.head_sha }}
|
|
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
steps:
|
|
- name: Finalize launch statuses on the PR head
|
|
run: |
|
|
set -uo pipefail
|
|
finalize() {
|
|
agent="$1"; launched="$2"; result="$3"
|
|
[ "$launched" = true ] || return 0
|
|
state=$([ "$result" = success ] && echo success || echo failure)
|
|
gh api -X POST "repos/$REPO/statuses/$HEAD_SHA" \
|
|
-f "state=$state" -f "context=review-launch/$agent" -f "target_url=$RUN_URL" \
|
|
-f "description=Review $result" >/dev/null 2>&1 || true
|
|
}
|
|
finalize codex "${{ needs.plan.outputs.launch_codex }}" "${{ needs.codex.result }}"
|
|
finalize pi "${{ needs.plan.outputs.launch_pi }}" "${{ needs.pi.result }}"
|
|
finalize claude "${{ needs.plan.outputs.launch_claude }}" "${{ needs.claude.result }}"
|