mirror of
https://github.com/Comfy-Org/github-workflows.git
synced 2026-11-05 00:17:37 +00:00
[PR #303] [MERGED] fix(cursor-review): own a per-PR concurrency group so skip-cursor-review cancels an in-flight panel #304
Labels
No labels
cursor-review-catalog-drift
groom
groom
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Comfy-Org/github-workflows#304
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/Comfy-Org/github-workflows/pull/303
Author: @mattmillerai
Created: 9/18/2026
Status: ✅ Merged
Merged: 9/18/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-15263-cursor-review-concurrency📝 Commits (2)
6220edcfix(cursor-review): own a per-PR concurrency group so skip-cursor-review cancels an in-flight panel4f09aabfix(cursor-review): guard the trigger slot against an empty review_label, stop paying for a cancelled run, and correct the caller-group docs📊 Changes
4 files changed (+450 additions, -14 deletions)
View changed files
📝
.github/cursor-review/README.md(+16 -1)➕
.github/cursor-review/tests/test_workflow_concurrency.py(+286 -0)📝
.github/workflows/cursor-review.yml(+103 -7)📝
docs/callers/cursor-review.md(+45 -6)📄 Description
ELI-5
Applying
skip-cursor-reviewto a PR while a review panel was already running did nothing to that panel. It kept going and posted its review anyway. This gives the reusable workflow its own "one run per PR" group, and deliberately puts the trigger label and the veto label in the same slot — so applying the veto now cancels the panel that is in flight. Every other label gets its own slot, so adding an unrelated label still never kills a running review.What was wrong
The documented caller concurrency group is
cursor-review-pr-<n>-${{ github.event.label.name }}.labeled: skip-cursor-reviewandlabeled: cursor-reviewtherefore resolve to different groups and cannot cancel each other. The veto started a run that no-opped in the Gate while the panel it was meant to stop ran to completion and posted. The Gate's own comment claimed the opposite: "Removing it cancels any in-progress run via the concurrency group and then no-ops here."What changed
.github/workflows/cursor-review.yml— a workflow-levelconcurrency:block, above the existing workflow-levelenv:. Same shapepr-size.ymlandpr-area-label.ymlalready use here, so it reaches every caller at its next pin bump with no caller edit and no permission change.Slot rule, as rendered by the group expression:
labeled/unlabeledof the trigger labeltriggerlabeled/unlabeledofskip-cursor-reviewtriggeropened/reopened/ready_for_review/synchronize, only underrun_without_label: truetriggerlabel-<name>pull_request_review_threadlabel-Gate comments corrected — the stale "removing it cancels any in-progress run" claim now states which events share the
triggerslot, and that a caller pinned below this commit still relies on its own label-scoped group, which covers the trigger label alone. The unblock comment notes the slot has already cancelled anything in flight and that the dup-check still covers an unblock after a completed review at this SHA. The veto and unblock logic is unchanged.Docs, four sites —
docs/callers/cursor-review.md(a new "The reusable owns a group of its own" section, a new "Veto mid-flight" gotcha for older pins, a new blocking-gate gotcha, and the blocking-gate line amended to "stops new panels from running and cancels a running one"),.github/cursor-review/README.md(the caller example's group line and the "Escape hatches" skip bullet), and the caller pattern in thecursor-review.ymlheader. Every caller example says KEEP THIS: the reusable's group refines a caller group, it does not replace one (see "Review round 1" below). All of them carry both hard rulespr-sizecarries — never name a caller groupcursor-review-reusable-*(that deadlocks the caller's own run), and call the reusable from a dedicated workflow file, because cancellation is run-scoped..github/cursor-review/tests/test_workflow_concurrency.py— pins the workflow-levelconcurrencykey,cancel-in-progress: true, thecursor-review-reusable-prefix, the per-PR key, both labels inside the same&& 'trigger'disjunction, theformat('label-{0}'namespacing, and a deadlock guard asserting no other file under.github/workflows/mentions the prefix.Review round 1 (commit 2)
The panel raised 10 findings; 8 are addressed in code, 2 in docs + a follow-up.
review_labeldragged every non-label event intotrigger. The input isrequired: false, so a caller can pass'', and GitHub coerces a missinggithub.event.labeland''alike in a mixed comparison — sopull_request_review_thread: resolvedcompared EQUAL and joinedtrigger, cancelling a running panel. Exactly the case the block's own comment called impossible. Guarded withinputs.review_label != '', with its own test.run_without_label: falsethe reusable's group does not supersede on push, so a widened or blocking caller that drops its PR-number-only group posts reviews against superseded head SHAs — and underblocking: truegates red on threads for code that no longer exists. In the other direction, a PR-number-only caller group is coarser than the reusable's: it cancels the whole caller run on any label or thread event, before the per-slot isolation can refine anything. Both sides are now stated, and every example says KEEP THIS.pr-sizehard rule was dropped. Cancellation is run-scoped, so the reusable's group takes down the whole caller run — and unlike the deadlock rule, this one arrives silently at a pin bump with no caller edit to warn anyone. Now shipped in all three doc sites.always()onconsolidateandnotify-complete→!cancelled(). Cancelling is now an advertised escape hatch, and on a cancelled runalways()spent up to 30 minutes on a judge whose payload nothing could post, then DMed the triggerer "Cursor review failed to post" for a deliberate veto. Cell-level failures are unaffected —cancelled()asks whether the RUN was cancelled, so a red or cancelled matrix cell still reaches the judge and still notifies.blocking-gatedeliberately keepsalways(). Three reviewers proposed the opposite. Its comment argues the case at length: a cancelled run that skipped the gate would mint a green required check, which is the fail-open BE-4691 added the job to close. Not changed.run_without_labelarm — or swapping its||for&&— kept it green. It now splits on the TOP-LEVEL||and requires the veto to be its own unconditional operand. Mutation-checked: all four mutations fail it (see Verification).label-<name>occupant is not cancelled at all.*.yamltoo.Two findings are documented rather than fixed, both stated plainly in the docs:
==is case-insensitive while the gate's own test is a case-sensitive shell=, so areview_labelthat differs from the real label only in case reachestrigger, cancels the panel and then no-ops. GitHub expressions have no case-sensitive string compare and nolower(), so this is not fixable in the expression — it is a caller-config requirement, and now a documented one. Reachable only via that misconfiguration.Blocking gateverdict racy. The cancelled run trips the "review did not land" guard and reports red; the veto run hasshould_run=false, falls through to the live thread query and reports green — same head SHA, nothing orders them. Which verdict a vetoed PR should get is a policy question about a deliberately fail-closed job, not a concurrency bug, so it is written up in the blocking-gate gotchas and proposed as a follow-up rather than decided here.Judgment calls
run_without_labeldisjunct is an explicit action allowlist, not an empty-label.nametest. The plan sketchedinputs.run_without_label && github.event.label.name == '' && github.event_name == 'pull_request'. That also admitsedited,closed,assigned,converted_to_draftand any otherpull_requestaction a caller widens itstypes:to — each of which would then cancel a running panel and immediately no-op in the Gate. The allowlist mirrors the Gate's owncaselist exactly (opened|reopened|ready_for_review|synchronize), so the concurrency slot and the "should this run" decision cannot drift apart. It also sidesteps GitHub's loosenull == ''coercion on a missinggithub.event.label.test_workflow_job_isolation.pydeliberately parses with regex — this repo is stdlib-only andtest-cursor-review-scripts.ymlinstalls no requirements, so ayamlimport simply would not run in CI. It reuses that suite'sWORKFLOWpath and comment-stripping discipline instead.triggerdisjunction is located with a balanced-paren walk, not a regex. A fixed-depth regex stops matching the moment the condition nests one level deeper and reports "the slot rule is gone" when it is merely nested.ci-cursor-review.ymlcaller is left alone. It pins by SHA, and moving that pin isbump-cursor-review-callers.yml's job, not this PR's. Its group (cursor-review-pr-…) does not collide with the new one.Verification
python3 -m unittest discover -s .github/cursor-review/tests -p 'test_*.py'— 599 passed, 0 failed (6 new).python3 -m unittest discover -s .github/workflow-pins/tests -p 'test_*.py'— 341 passed, 0 failed.python3 .github/workflow-pins/check_workflow_pins.py— OK, 11 workflows declareworkflows_ref, none with a default, every ref checkout guarded.python3 .github/agents-md-integrity/check_agents_md.py --root .— passed (2 pre-existing warnings, unchanged by this PR).actionlint .github/workflows/cursor-review.yml(v1.7.7) — the same two findings as onmain, byte-identical, none in the new block: the pre-existingSC2153 HEAD_REFfalse positive and the knownjob.workflow_shaproperty (the BE-4169/BE-8077 carve-out AGENTS.md documents). Baseline captured by stashing this diff and re-running.inputs-in-concurrencyquestion the plan flagged is answered empirically: injecting${{ secrets.NOPE }}into the group made actionlint print "contextsecretsis not allowed here. available contexts aregithub,inputs,vars" against this exact key — so the context list is validated,inputsis on it, and the check is non-vacuous. Reverted immediately.cancel-in-progress: false→ fails; drop theformat('label-{0}'namespacing → fails; rename the group tocursor-review-pr-→ fails; giveci-cursor-review.ymlthecursor-review-reusable-prefix → the deadlock guard fails; delete the wholeconcurrency:block → all six fail.Residual
The live end-to-end verification named in the plan was not run, and cannot be run from this PR. The plan asks for: apply
cursor-review, wait for panel cells to start, applyskip-cursor-review, confirm the run showscancelledand no review is posted; removeskip-cursor-reviewand confirm exactly one fresh panel; and apply an unrelated label mid-panel and confirm nothing is cancelled. This repo self-enrols viaci-cursor-review.yml, which pinscursor-review.ymlby merged-main SHA precisely so a PR cannot change the reviewer judging it — so the panel that runs on this PR is the old one, and the new block is not exercised by it. Repointing that caller at this branch would defeat the tamper-proofing AGENTS.md requires, so it was not done. Someone should run that sequence on a throwaway PR once this is merged and the pin has moved, and in particular confirm the two things static analysis cannot: that a called workflow's workflow-levelconcurrencycancels the panel matrix jobs (not merely the caller'suses:job), and that the cancelled run posts no review. Thepr-size.yml/pr-area-label.ymlprecedent in this repo is the evidence this shape works; it is precedent, not a run of this expression.The 4108-run / 29-caller history behind the plan was not re-measured. Those callers are private and their roster lives in the
CURSOR_REVIEW_CALLERSsecret, which is write-only and unreadable from here, so the "no incident found" premise is carried forward from the spike rather than re-verified. Nothing in this PR depends on it beyond priority.The portion not fixed, swept here: 16 reusable workflows in this repo declare
on: workflow_call; 6 now own a workflow-levelconcurrency:group (cursor-review,pr-size,pr-area-label,groom,linear-ticket, and — pre-existing — none other), leaving 10 that do not. That is not a latent copy of this bug: none of the 10 pairs a minutes-long job with a mid-flight veto label, which is the combination that makes a split group observable. Checked by shape (askip-/bypassgrep over each), not by run history.Explicitly out of scope, per the plan: the thread-resolve-cancels-panel wart for widened-
types:callers, the over-cap comment race, and theworkflows_refguard. Draft PR #58 was not rebased on or merged into this; if it is ever revived it must adopt the slot expression added here rather than routingskip-cursor-reviewto a slot of its own.Provenance
python3 -m unittest discover -s .github/cursor-review/tests -p 'test_*.py': 600 passed, 0 failed (7 in the new concurrency suite);python3 -m unittest discover -s .github/workflow-pins/tests -p 'test_*.py': 341 passed, 0 failed;check_workflow_pins.py: OK (11 workflows declareworkflows_ref, none with a default, every ref checkout guarded);check_agents_md.py --root .: passed (2 pre-existing warnings, unchanged);cursor-review.ymlre-parsed as YAML and the group expression paren-balanced after the edit. The hardened test was mutation-checked four ways, each reverted: veto demoted into the nestedrun_without_labelarm → fails;||between the two label comparisons swapped for&&→ fails; theinputs.review_label != ''guard removed → fails; veto given its own slot (the PR #58 shape) → fails. Commit 1'sactionlintv1.7.7 baseline (2 findings, byte-identical tomain, none in the new block) was not re-run — no actionlint binary on this host — so the guard against a new lint finding in the edited expression rests on the YAML parse and the paren check.run_without_labeldisjunct uses an explicit action allowlist rather than the plan's empty-label.nametest, and the new test parses the workflow with the sibling suite's stdlib regex approach rather than PyYAML (this suite's CI installs no requirements) — both under "Judgment calls" above. The plan's live end-to-end verification step is unmet and is written up under "Residual". Commit 2 departs from the panel's recommendation on one finding —blocking-gatekeepsalways()rather than moving to!cancelled()— for the reason its own comment gives; see "Review round 1".🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.