Skip to content
Draft
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
27 changes: 19 additions & 8 deletions .github/cursor-review/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ never a copy of the review logic. Nothing here to keep in sync downstream.
## How it works

```
PR gets the `cursor-review` label
PR gets `cursor-review` or `cursor-review-xl`
┌─────────┐ skip if: `skip-cursor-review` present,
│ Gate │ PR over the diff-size cap, or this exact
│ Gate │ PR over the diff-size cap (unless XL), or this exact
└────┬────┘ commit was already reviewed (idempotent)
│ should_run
Expand Down Expand Up @@ -103,10 +103,6 @@ on:
permissions:
contents: read
pull-requests: write
concurrency:
# Re-labeling cancels an in-flight run for the same PR + label.
group: cursor-review-pr-${{ github.event.pull_request.number }}-${{ github.event.label.name }}
cancel-in-progress: true
jobs:
cursor-review:
uses: Comfy-Org/github-workflows/.github/workflows/cursor-review.yml@<sha> # v1
Expand All @@ -130,7 +126,19 @@ jobs:
| Secret | `SLACK_BOT_TOKEN` | no | Enables start/complete DMs to the triggerer. |
| Variable | `CURSOR_REVIEW_DM_EMAIL_MAP` | no | Maps GitHub logins → emails for Slack DM lookup. |

**3. Trigger a review** by adding the `cursor-review` label to a PR. That's it.
**3. Trigger a review** by adding `cursor-review` to a PR. For an intentionally
large PR, add `cursor-review-xl` instead; it runs the same panel without the
diff-size cap. Create both labels once in each consumer repository.

### Large diffs: `cursor-review-xl`

`cursor-review-xl` is a standalone alternate trigger. It keeps the normal
diff exclusions and line-count reporting, but bypasses `diff_size_cap`. If both
review labels are present, XL mode wins.

Concurrency is owned by the reusable workflow: adding XL cancels an in-flight
standard panel for the same PR, while unrelated label changes cannot cancel a
review. Callers do not need their own `concurrency` block.

### Optional: review-on-assignment

Expand Down Expand Up @@ -193,6 +201,7 @@ descriptions live in the [workflow header](../workflows/cursor-review.yml).
| `judge_model` | `claude-opus-4-8-thinking-max` | Model that consolidates panel findings. |
| `diff_size_cap` | `5000` | Max changed lines (after excludes); larger PRs are skipped. |
| `review_label` | `cursor-review` | Label whose addition triggers the review. |
| `xl_review_label` | `cursor-review-xl` | Alternate trigger that bypasses `diff_size_cap`; wins when both labels are present. |
| `diff_excludes` | lockfiles, `node_modules`, `dist`, `vendor`, minified/generated files | Pathspecs excluded from both the size count and the reviewed diff. |
| `workflows_ref` | `main` | Ref this directory's prompts/scripts are loaded from. Pin to your `uses:` SHA. |
| `bot_app_id` | `''` | Optional GitHub App ID; when set (with `BOT_APP_PRIVATE_KEY`), the review posts under that App's identity instead of `github-actions[bot]`. |
Expand All @@ -202,8 +211,10 @@ descriptions live in the [workflow header](../workflows/cursor-review.yml).

- **Skip a PR**: add the `skip-cursor-review` label. It wins even if the trigger
label is present. Removing it (while the trigger label is on) starts a run.
- **Review a large PR**: add `cursor-review-xl`; it bypasses only the line cap,
while normal exclusions, idempotency, and failure handling still apply.
- **Re-review after changes**: push commits. The new HEAD SHA bypasses the
idempotency check and a re-applied label runs a fresh panel.
idempotency check and re-applying the active review label runs a fresh panel.
- **Re-review unchanged content**: dismiss the existing review, then re-apply
the label.

Expand Down
2 changes: 1 addition & 1 deletion .github/cursor-review/post-review.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ def post_error_review(repo, pr_number, commit_sha, header, error_message):
safe = neutralize_mentions(error_message)
body_text = (
f"{header}\n\n⚠️ **Review failed**\n\n```\n{safe}\n```\n\n"
"Re-trigger by removing and re-adding the `cursor-review` label."
"Re-trigger by removing and re-adding the active Cursor Review label."
)
payload = json.dumps(
{"body": body_text, "event": "COMMENT", "commit_id": commit_sha}
Expand Down
96 changes: 79 additions & 17 deletions .github/workflows/cursor-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ name: Cursor Review (reusable)
# permissions:
# contents: read
# pull-requests: write
# concurrency:
# group: cursor-review-pr-${{ github.event.pull_request.number }}-${{ github.event.label.name }}
# cancel-in-progress: true
# jobs:
# cursor-review:
# uses: Comfy-Org/github-workflows/.github/workflows/cursor-review.yml@main
Expand Down Expand Up @@ -66,6 +63,13 @@ on:
type: string
required: false
default: cursor-review
xl_review_label:
description: >-
Alternate trigger label that runs the same review while bypassing the
diff-size cap. If both trigger labels are present, XL mode wins.
type: string
required: false
default: cursor-review-xl
diff_excludes:
description: >-
Whitespace-separated git pathspecs excluded from BOTH the size-budget
Expand Down Expand Up @@ -128,13 +132,39 @@ on:
description: PEM private key matching the bot_app_id input. Required only when bot_app_id is set.
required: false

# DIFF_SIZE_CAP / REVIEW_LABEL / JUDGE_MODEL / DIFF_EXCLUDES are mapped from
# `inputs` here so the run steps below read them verbatim from the original
# Serialize actual review triggers per caller repo + PR. Standard and XL events
# share one group so adding XL cancels an in-flight standard panel. A standard
# label event while XL is already present is deliberately kept out of that group
# (XL already owns the PR), and unrelated labels keep distinct suffixes so they
# cannot cancel reviews. The reusable prefix must differ from callers' legacy
# concurrency groups or a called workflow could cancel its own caller.
concurrency:
group: >-
cursor-review-reusable-${{ github.repository }}-${{ github.event.pull_request.number }}-${{
(
github.event.label.name == inputs.xl_review_label
|| (
github.event.label.name == inputs.review_label
&& !contains(
github.event.pull_request.labels.*.name,
inputs.xl_review_label
)
)
|| (inputs.run_without_label && !github.event.label.name)
) && 'review'
|| github.event.label.name
|| github.event.action
}}
cancel-in-progress: true

# DIFF_SIZE_CAP / REVIEW_LABEL / XL_REVIEW_LABEL / JUDGE_MODEL / DIFF_EXCLUDES
# are mapped from `inputs` here so the run steps read them verbatim from the
# (private-repo) workflow without per-step plumbing. `inputs` is available to
# workflow-level env in a called workflow; `secrets` is not (those stay in jobs).
env:
DIFF_SIZE_CAP: ${{ inputs.diff_size_cap }}
REVIEW_LABEL: ${{ inputs.review_label }}
XL_REVIEW_LABEL: ${{ inputs.xl_review_label }}
JUDGE_MODEL: ${{ inputs.judge_model }}
DIFF_EXCLUDES: ${{ inputs.diff_excludes }}
# Where the assets checkout lands (this repo, .github/cursor-review/*).
Expand All @@ -150,6 +180,7 @@ jobs:
outputs:
should_run: ${{ steps.check.outputs.should_run }}
already_reviewed: ${{ steps.dup.outputs.already_reviewed }}
xl_mode: ${{ steps.check.outputs.xl_mode }}
steps:
- name: Check trigger label
id: check
Expand All @@ -161,6 +192,16 @@ jobs:
BASE_REPO: ${{ github.repository }}
RUN_WITHOUT_LABEL: ${{ inputs.run_without_label }}
run: |
# XL is a property of the PR's current labels, not just this event.
# That makes it win when both labels are present and preserves XL when
# removing skip-cursor-review re-triggers a previously gated review.
XL_MODE=false
if [ -n "$XL_REVIEW_LABEL" ] \
&& echo "$PR_LABELS" | jq -e --arg l "$XL_REVIEW_LABEL" 'index($l)' > /dev/null; then
XL_MODE=true
fi
echo "xl_mode=$XL_MODE" >> "$GITHUB_OUTPUT"

# PRs from forks can't run this review. The pull_request event grants
# fork PRs no access to secrets — so CURSOR_API_KEY is empty and every
# panel cell produces empty output — and a read-only GITHUB_TOKEN, so
Expand Down Expand Up @@ -195,25 +236,42 @@ jobs:
esac
fi

# Only adding the trigger label fires the matrix. Removing it cancels
# any in-progress run via the concurrency group and then no-ops here.
if [ "$LABEL_NAME" = "$REVIEW_LABEL" ] && [ "$GH_EVENT_ACTION" = "labeled" ]; then
echo "$REVIEW_LABEL label added — running."
echo "should_run=true" >> "$GITHUB_OUTPUT"
exit 0
# Either trigger label can start the matrix. XL is checked first so it
# owns the mode when callers customize both inputs to the same value.
if [ "$GH_EVENT_ACTION" = "labeled" ]; then
if [ -n "$XL_REVIEW_LABEL" ] && [ "$LABEL_NAME" = "$XL_REVIEW_LABEL" ]; then
echo "$XL_REVIEW_LABEL label added — running in XL mode."
echo "should_run=true" >> "$GITHUB_OUTPUT"
exit 0
fi

if [ "$LABEL_NAME" = "$REVIEW_LABEL" ]; then
if [ "$XL_MODE" = "true" ]; then
echo "$REVIEW_LABEL added while $XL_REVIEW_LABEL is present — XL already owns the review; skipping duplicate trigger."
echo "should_run=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "$REVIEW_LABEL label added — running."
echo "should_run=true" >> "$GITHUB_OUTPUT"
exit 0
fi
fi

# Removing skip-cursor-review while the trigger label is present
# unblocks a previously skipped review — treat it as a fresh trigger.
# Removing skip-cursor-review while either trigger is present unblocks
# a previously skipped review. XL wins when both are present.
if [ "$LABEL_NAME" = "skip-cursor-review" ] && [ "$GH_EVENT_ACTION" = "unlabeled" ]; then
if echo "$PR_LABELS" | jq -e --arg l "$REVIEW_LABEL" 'index($l)' > /dev/null; then
if [ "$XL_MODE" = "true" ]; then
echo "skip-cursor-review removed while $XL_REVIEW_LABEL is present — running in XL mode."
echo "should_run=true" >> "$GITHUB_OUTPUT"
exit 0
elif echo "$PR_LABELS" | jq -e --arg l "$REVIEW_LABEL" 'index($l)' > /dev/null; then
echo "skip-cursor-review removed while $REVIEW_LABEL is present — running."
echo "should_run=true" >> "$GITHUB_OUTPUT"
exit 0
fi
fi

echo "Label '$LABEL_NAME' is not the trigger label — skipping."
echo "Label '$LABEL_NAME' is not a review trigger — skipping."
echo "should_run=false" >> "$GITHUB_OUTPUT"

- name: Check for prior review on this commit
Expand Down Expand Up @@ -270,6 +328,7 @@ jobs:
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
XL_MODE: ${{ needs.gate.outputs.xl_mode }}
run: |
# numstat reports "-\t-" for binary files; treat those as 0 lines so
# binary-only PRs can't bypass the size cap by producing no insertion
Expand All @@ -280,9 +339,12 @@ jobs:
# shellcheck disable=SC2086
TOTAL=$(git diff --numstat "$BASE_SHA...$HEAD_SHA" -- . $DIFF_EXCLUDES \
| awk '{ if ($1 != "-") added += $1; if ($2 != "-") removed += $2 } END { print (added + removed) + 0 }')
echo "Changed lines: $TOTAL (cap: $DIFF_SIZE_CAP)"
echo "Changed lines: $TOTAL"

if [ "$TOTAL" -gt "$DIFF_SIZE_CAP" ]; then
if [ "$XL_MODE" = "true" ]; then
echo "XL mode ($XL_REVIEW_LABEL) — bypassing the $DIFF_SIZE_CAP-line cap."
echo "within_cap=true" >> "$GITHUB_OUTPUT"
elif [ "$TOTAL" -gt "$DIFF_SIZE_CAP" ]; then
echo "Diff exceeds cap — skipping review."
echo "within_cap=false" >> "$GITHUB_OUTPUT"
else
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This repo is **public** so any repo — public or private, inside or outside the
| Workflow | Purpose |
|---|---|
| [`detect-unreviewed-merge.yml`](.github/workflows/detect-unreviewed-merge.yml) | SOC 2 compliance — detects PRs merged without prior approval and opens a tracking issue in [`Comfy-Org/unreviewed-merges`](https://github.com/Comfy-Org/unreviewed-merges). |
| [`cursor-review.yml`](.github/workflows/cursor-review.yml) | Label-triggered multi-model code review. A 4-lab × 2-review-type cursor-agent panel runs adversarial + edge-case passes, a judge model consolidates them into one PR review with per-finding severity badges, and the triggerer gets Slack start/complete DMs. Advisory by default; opt in with `blocking: true` to fail a (required-status-check) gate while findings stay unresolved. Prompts and scripts live in [`.github/cursor-review/`](.github/cursor-review) — the single source of truth, so consumer repos carry only a thin caller. Self-hostable via `runs_on` (JSON, default `ubuntu-latest`) and panel models overridable via `models` (JSON array) for accounts lacking a default provider. Requires `CURSOR_API_KEY` (+ optional `SLACK_BOT_TOKEN`). |
| [`cursor-review.yml`](.github/workflows/cursor-review.yml) | Label-triggered multi-model code review. A 4-lab × 2-review-type cursor-agent panel runs adversarial + edge-case passes, a judge model consolidates them into one PR review with per-finding severity badges, and the triggerer gets Slack start/complete DMs. `cursor-review-xl` runs the same panel while bypassing the diff-size cap; shared concurrency makes XL replace an in-flight standard review. Advisory by default; opt in with `blocking: true` to fail a (required-status-check) gate while findings stay unresolved. Prompts and scripts live in [`.github/cursor-review/`](.github/cursor-review) — the single source of truth, so consumer repos carry only a thin caller. Self-hostable via `runs_on` (JSON, default `ubuntu-latest`) and panel models overridable via `models` (JSON array) for accounts lacking a default provider. Requires `CURSOR_API_KEY` (+ optional `SLACK_BOT_TOKEN`). |
| [`cursor-review-auto-label.yml`](.github/workflows/cursor-review-auto-label.yml) | Companion to `cursor-review.yml`. On PR assignment, applies the review label for an opted-in reviewer (via the CLOUD_CODE_BOT app token, so the label actually triggers the review). The opt-in roster lives in the caller's `vars.CURSOR_REVIEW_OPTED_IN_LOGINS` — no roster is baked into the workflow. Requires `vars.APP_ID` + `CLOUD_CODE_BOT_PRIVATE_KEY`. |
| [`assign-reviewers.yml`](.github/workflows/assign-reviewers.yml) | Auto-requests expertise-aware, load-balanced PR reviewers with new-folk randomization. Matches changed paths against a caller-repo `.github/reviewers.yml` (path-glob → reviewers, plus a `default_pool`), drops the author + `vars.REVIEWER_EXCLUDE`, ranks candidates by open review load (steering off anyone at/over `vars.REVIEWER_LOAD_CAP`), and may swap a slot for a `vars.REVIEWER_GROWTH_POOL` member. Requests go through the CLOUD_CODE_BOT app token so they work on fork PRs. Requires `vars.APP_ID` + `CLOUD_CODE_BOT_PRIVATE_KEY`. |
| [`assign-prs-to-author.yml`](.github/workflows/assign-prs-to-author.yml) | Housekeeping — assigns every open PR with no assignees to its author (bot-authored PRs skipped by default). Run on a schedule from a thin caller; useful when a team tracks PR ownership via assignees. The calling job needs `pull-requests: write` and `issues: write`. |
Expand Down