diff --git a/.github/cursor-review/README.md b/.github/cursor-review/README.md index 7ee26ae..305e24d 100644 --- a/.github/cursor-review/README.md +++ b/.github/cursor-review/README.md @@ -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 ▼ @@ -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@ # v1 @@ -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 @@ -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]`. | @@ -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. diff --git a/.github/cursor-review/post-review.py b/.github/cursor-review/post-review.py index 9a83bfe..70daa0e 100644 --- a/.github/cursor-review/post-review.py +++ b/.github/cursor-review/post-review.py @@ -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} diff --git a/.github/workflows/cursor-review.yml b/.github/workflows/cursor-review.yml index 31cb62f..2b7531c 100644 --- a/.github/workflows/cursor-review.yml +++ b/.github/workflows/cursor-review.yml @@ -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 @@ -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 @@ -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/*). @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/README.md b/README.md index dbebc8e..c3dc346 100644 --- a/README.md +++ b/README.md @@ -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`. |