Skip to content
Open
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
66 changes: 27 additions & 39 deletions .github/cursor-review/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,13 @@ silently vanishing — the review tells you what didn't run.
| [`prompt-judge.md`](prompt-judge.md) | Prompt the judge model uses to consolidate panel findings into one review. |
| [`extract-findings.py`](extract-findings.py) | Parses a cell's raw `cursor-agent` output into a normalized findings record. Always emits structured JSON — even on empty output or parse failure — so the consolidate step has uniform input. |
| [`post-review.py`](post-review.py) | Reads the judge's consolidated findings and posts **one** PR review with line-anchored inline comments and severity badges. |
| [`gate-unresolved.py`](gate-unresolved.py) | The opt-in blocking gate (`blocking: true`). Queries the PR's review threads and exits non-zero while any cursor-review finding thread is unresolved. |
| [`gate-unresolved.py`](gate-unresolved.py) | **Orphaned — nothing calls this today.** Implemented the opt-in blocking gate: queries the PR's review threads and exits non-zero while any cursor-review finding thread is unresolved. The job that ran it was dropped from `cursor-review.yml` in #31; see [the regression note](#the-blocking-gate-is-currently-not-available-regressed). |
| [`slack-notify.sh`](slack-notify.sh) | Sends the start/complete Slack DMs to the triggerer (no-ops without a token). |

## Adopt it in your repo

The review logic lives here; your repo adds only a **thin caller**. Pin `uses:`
to a full commit SHA (see the [top-level README](../../README.md#usage) for the
to a full commit SHA (see the [top-level README](../../README.md#pinning) for the
why and the versioning policy).

**1. Add the caller workflow** at `.github/workflows/ci-cursor-review.yml`:
Expand Down Expand Up @@ -143,45 +143,30 @@ an app token is required). The opt-in roster lives in the caller's
`vars.CURSOR_REVIEW_OPTED_IN_LOGINS`. See that workflow's header for the full
example and the `vars.APP_ID` / `CLOUD_CODE_BOT_PRIVATE_KEY` requirements.

### Optional: make the review blocking (merge gate)
### The blocking gate is currently NOT available (regressed)

By default the review is **advisory** — it posts findings as PR review threads,
but an unresolved (red) review never blocks merge. Opt into a blocking gate by
passing `blocking: true`:
> **Do not pass `blocking: true`.** `cursor-review.yml` declares no such input
> today, and GitHub rejects an undeclared input at startup — a caller that passes
> it gets a zero-job `startup_failure` with no logs to explain it.

```yaml
jobs:
cursor-review:
uses: Comfy-Org/github-workflows/.github/workflows/cursor-review.yml@<sha> # v1
with:
blocking: true
secrets:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
```
The review is **advisory**: it posts findings as PR review threads, and an
unresolved (red) review never blocks merge.

With `blocking: true`, a final **Blocking gate** job queries the PR's review
threads and **fails the check while any cursor-review finding thread is
unresolved**. Resolve the thread(s) — or push a fix and re-trigger the review —
and the gate goes green. It is idempotent: resolving threads and re-running the
check turns it green without a fresh panel.

> **This workflow cannot set branch protection.** A red check is visible but
> does not block merge on its own. To actually gate merges you must **also mark
> the `Blocking gate` check as a required status check** in the calling repo:
> *Settings → Branches → Branch protection rule* (or *Rulesets*) → **Require
> status checks to pass** → add **`Blocking gate`** (it appears once the
> workflow has run at least once with `blocking: true`). The check name is
> prefixed with your caller job id, e.g. `cursor-review / Blocking gate`.

Notes:

- `blocking: false` (the default, or simply not passing the input) is **exactly
today's behavior** — no caller changes until it opts in.
- A thread is recognized as a cursor-review thread by its originating review's
body marker, so the gate works whether the review posts as
`github-actions[bot]` or under a dedicated `bot_app_id`.
- Outdated threads (their code changed since the finding was posted) don't
block — a re-review re-posts anything still wrong as a fresh thread.
An opt-in gate did exist. A `blocking:` input and a fail-closed **Blocking gate**
job — which queried the PR's review threads and failed while any cursor-review
finding thread was unresolved — shipped in
[#16](https://github.com/Comfy-Org/github-workflows/pull/16) (BE-1891). Both were
removed from `cursor-review.yml` in
[#31](https://github.com/Comfy-Org/github-workflows/pull/31), a change whose
stated purpose was fixing judge-findings extraction; the deletion looks
unintentional. The gate's implementation
([`gate-unresolved.py`](gate-unresolved.py)) is still here, orphaned and unwired.

Restoring it is tracked separately. Note that marking `… / Consolidate panel`
required is **not** a substitute: GitHub counts a skipped required check as
passing, and that job skips whenever no review runs (no trigger label, dedupe hit,
diff over `diff_size_cap`, fork PR), so the check would go green in precisely the
cases a gate is meant to catch.

## Configuration knobs

Expand All @@ -196,7 +181,10 @@ descriptions live in the [workflow header](../workflows/cursor-review.yml).
| `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]`. |
| `blocking` | `false` | Opt-in merge gate. `true` fails the **Blocking gate** check while any cursor-review finding thread is unresolved. See [Make the review blocking](#optional-make-the-review-blocking-merge-gate). |
| `run_without_label` | `false` | Run on plain PR events instead of requiring the trigger label. Also requires widening the caller's `types:` — see [the setup guide](../../docs/callers/cursor-review.md). |

There is **no `blocking` input** — see [the regression note
above](#the-blocking-gate-is-currently-not-available-regressed).

### Escape hatches

Expand Down
10 changes: 9 additions & 1 deletion .github/workflows/detect-unreviewed-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,21 @@ name: Detect Unreviewed Merge (reusable)
# concurrency:
# group: detect-unreviewed-merge-${{ github.sha }}
# cancel-in-progress: false
# permissions:
# contents: read
# pull-requests: read
# jobs:
# detect:
# uses: Comfy-Org/unreviewed-merges/.github/workflows/detector.yml@v1
# # This repo, pinned by full commit SHA. The path below is the live one;
# # an older example pointed at Comfy-Org/unreviewed-merges/.../detector.yml,
# # which does not exist and does not resolve.
# uses: Comfy-Org/github-workflows/.github/workflows/detect-unreviewed-merge.yml@<sha> # v1
# with:
# approval-mode: latest-per-reviewer # or 'any-approval' for private repos
# secrets:
# UNREVIEWED_MERGES_TOKEN: ${{ secrets.UNREVIEWED_MERGES_TOKEN }}
#
# Setup guide: docs/callers/detect-unreviewed-merge.md

on:
workflow_call:
Expand Down
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,13 @@
__pycache__/
*.py[cod]
*.pyc

# Go build output. pr-size.yml compiles this from source into RUNNER_TEMP on
# every run, so a committed binary is dead weight — and a local `go build` here
# produces a host-arch executable that cannot run on the ubuntu-latest runners.
/scripts/check-pr-size/check-pr-size
Comment thread
mattmillerai marked this conversation as resolved.
/scripts/check-pr-size/check-pr-size.exe

# Stray local logs
err.log
*.err.log
26 changes: 21 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,16 @@ tests — run the matching command above for whatever you touched.
- `README.md` — the public workflow catalog: per-workflow purpose, the SHA-pin
usage pattern, and the versioning policy. Keep its table in sync when you add
a workflow.
- `docs/callers/` — one setup guide per reusable workflow: a complete,
copy-pasteable caller (including `on:` and the exact permission grant),
required secrets/vars, and per-workflow footguns. Add a page when you add a
workflow; the README table links to it.

## Reusable workflow catalog (what each does)

- `cursor-review.yml` — label-triggered multi-model PR review (4-lab × 2-type
panel → judge → one PR review with severity badges). Advisory by default;
`blocking: true` gates on unresolved findings.
panel → judge → one PR review with severity badges). Advisory: it posts a
review, it does not gate.
- `cursor-review-auto-label.yml` — translates PR assignment/open into the review
label (via an app token, since a `GITHUB_TOKEN`-applied label won't fire runs).
- `groom.yml` — scheduled/dispatch org-wide code-cleanup sweep: read-only finder
Expand Down Expand Up @@ -95,7 +99,7 @@ tests — run the matching command above for whatever you touched.
workflow files, commit messages, and PR text.
- **Pin everything by full commit SHA**, with a trailing `# v1` comment — both
the `uses:` in callers and every third-party action here. Bare `@v1` fails the
pin-validation (`pinact`, `zizmor`) that consumer CI runs. See README "Usage".
pin-validation (`pinact`, `zizmor`) that consumer CI runs. See README "Pinning".
- **Scripts are the single source of truth**, loaded at run time from a pinned
ref of THIS repo — never from the caller's checkout. That's what makes the
reviewer/checker tamper-proof: a PR can't rewrite the logic judging it. The
Expand All @@ -108,9 +112,18 @@ tests — run the matching command above for whatever you touched.
callers). Do not fork the script — a forked copy is how other shared org
machinery has drifted.
- **New reusable workflow?** `on: workflow_call` + a header comment documenting
inputs/secrets/triggers + a caller-pattern example, then update the README
table (README "Adding a new reusable workflow"). Move the floating major tag
inputs/secrets/triggers + a caller-pattern example, then a
`docs/callers/<name>.md` setup guide and a row in the README table (see
CONTRIBUTING.md "Adding a new reusable workflow"). Move the floating major tag
after merge.
- **Document only inputs that exist.** GitHub rejects an unknown input at startup,
so a phantom input in the docs is a broken caller for whoever copies it. Check
`on.workflow_call.inputs` before documenting a knob. The `cursor-review`
`blocking:` input is the worked example: it shipped in #16, was deleted from the
workflow by #31 (a judge-extraction fix) while its docs and
`gate-unresolved.py` were left behind, and the stale docs outlived it here, in
the README, and in `.github/cursor-review/README.md`. **Deleting an input is a
docs change too** — grep the repo for its name in the same commit.
- **Versioning:** semver-style major tags (`v1`, `v2`). Breaking changes bump the
major; backwards-compatible changes move the existing tag in place
(`git tag -f v1 <sha> && git push -f origin v1`). This tag force-move is the
Expand All @@ -126,6 +139,9 @@ tests — run the matching command above for whatever you touched.
## Deeper docs

- [`README.md`](README.md) — public catalog, SHA-pin usage, versioning.
- [`docs/callers/`](docs/callers/) — per-workflow setup guides (copy-pasteable callers).
- [`CONTRIBUTING.md`](CONTRIBUTING.md) — tests to run, breaking-change rules, enrollment.
- [`SECURITY.md`](SECURITY.md) — disclosure process + the agent credential boundary.
- [`.github/cursor-review/README.md`](.github/cursor-review/README.md) — review panel internals + adoption.
- [`.github/agents-md-integrity/README.md`](.github/agents-md-integrity/README.md) — the checker + its knobs.
- [`.github/bump-callers/README.md`](.github/bump-callers/README.md) — the shared bumper + its fleets.
101 changes: 101 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Contributing

Thanks for helping out. One thing to understand before you start:

> **Every change here is live-fire.** Other repositories call these workflows at
> pinned SHAs, and moving the `v1` tag pushes your change to every caller pinned
> to the tag on their next run. There is no staging environment. A workflow that
> is merely *wrong* fails someone else's CI; one that is wrong about
> **permissions** fails it at startup with no logs to explain why.

## Before you open a PR

Run the tests for whatever you touched. They are the same commands CI runs:

```bash
# groom (ledger + dedup)
python3 -m unittest discover -s .github/groom/tests -p 'test_*.py' -v

# cursor-review (finding extraction, bot identity)
python3 -m unittest discover -s .github/cursor-review/tests -p 'test_*.py' -v

# AGENTS.md standard checker
python3 -m unittest discover -s .github/agents-md-integrity/tests -p 'test_*.py' -v

# caller-bump machinery
shellcheck -x .github/bump-callers/bump-callers.sh .github/bump-callers/tests/test_bump_callers.sh
bash .github/bump-callers/tests/test_bump_callers.sh

# PR-size classifier
cd scripts/check-pr-size && go vet ./... && go test ./...
```

Do **not** commit build outputs. `scripts/check-pr-size/` is compiled from source
by `pr-size.yml` at run time; a committed binary is dead weight (and the wrong
architecture for the `ubuntu-latest` runners).

## Changing a reusable workflow

Ask two questions in order:

**1. Does this change what a caller must send or grant?** Adding a required
input, adding a required secret, or making a nested job request a *new*
permission are all **breaking**, even though nothing in the caller's YAML
changed. GitHub validates the permission grant at **startup**, so a caller that
was fine yesterday fails with an opaque "workflow file issue" and zero jobs. If
the answer is yes, bump the major tag (`v1` → `v2`) and let callers opt in.

**2. Does it change assets loaded at run time?** Several workflows fetch prompts,
briefs, or checker scripts from `workflows_ref` while running. A caller that
pins `uses:` to a SHA but leaves `workflows_ref: main` will mix your new assets
with its old workflow. When you change assets and workflow together, say so in
the PR body so callers bump both.

## Adding a new reusable workflow

1. Add `.github/workflows/<descriptive-name>.yml` with `on: workflow_call:`.
Document every input and secret inline.
2. Declare **minimum permissions per job**, not at the workflow level. Callers
must grant the union of what your nested jobs request — keep that union small
and state it in the header comment.
3. Add a setup guide at `docs/callers/<descriptive-name>.md` following the shape
of the existing ones: a **complete, copy-pasteable** caller (including `on:`),
the exact permission grant, required vs optional secrets and `vars`, and any
footguns. A guide that omits `on:` or the permission grant is not a guide.
4. Add a one-line row to the [README](README.md#workflows) table linking to it.
5. If the workflow should be adopted broadly, add a `vars.<NAME>_CALLERS` roster
and a `bump-<name>-callers.yml` job so pins get bumped automatically. See
[.github/bump-callers/](.github/bump-callers/).
6. Add a `test-<name>.yml` if it ships scripts.

## Enrolling a repository as a caller

Two steps. Missing the second is the common mistake:

1. Add the caller workflow to the consumer repo (see
[`docs/callers/`](docs/callers/)).
2. **Add the repo to the matching `vars.<NAME>_CALLERS` roster** on this repo.
That roster is what `bump-*-callers.yml` reads to keep pins current. A repo
absent from it keeps its original SHA forever, drifts behind the reusable, and
eventually breaks when the two stop being compatible.

## Security-sensitive areas

The AI workflows (`cursor-review.yml`, `groom.yml`) aim to split model execution
from credential use: the agent jobs run with `contents: read` and mint no GitHub
token, emitting a patch or findings that a **separate** job applies as a GitHub
App. Preserving that boundary matters more than convenience — do not give an agent
job a write token to save a step.

The split is clean for `groom.yml` and for `cursor-review.yml`'s 8 panel cells.
It is **not** clean for cursor-review's judge: `Consolidate panel` runs the judge
model and posts the review in one `pull-requests: write` job. Treat that as a known
rough edge to work *toward* the boundary, not as licence to widen it elsewhere. See
[SECURITY.md](SECURITY.md).

## Review

Reviewers and assignees are routed automatically from
[`.github/reviewers.yml`](.github/reviewers.yml) by path. If you are changing a
bucket's globs, read the comments at the top of that file first — it explains why
the buckets are the durable part and the names are not.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Comfy Org

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Loading