[PR #262] [MERGED] fix(cursor-review): stop the success DM claiming a review a read-only token never posted #265

Closed
opened 2026-10-26 22:37:54 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/Comfy-Org/github-workflows/pull/262
Author: @mattmillerai
Created: 9/5/2026
Status: ✅ Merged
Merged: 9/8/2026
Merged by: @mattmillerai

Base: main ← Head: matt/be-10017-post-review-posted-output


📝 Commits (2)

  • ab7211f fix(cursor-review): stop the success DM claiming a review a read-only token never posted
  • 782908b fix(cursor-review): stop the DM over-claiming on a posted-but-unadjudicated round (BE-10017)

📊 Changes

3 files changed (+403 additions, -12 deletions)

View changed files

📝 .github/cursor-review/post-review.py (+48 -7)
📝 .github/cursor-review/tests/test_post_review_delivery.py (+308 -4)
📝 .github/workflows/cursor-review.yml (+47 -1)

📄 Description

ELI-5

When the cursor-review panel finishes, the workflow Slack-DMs whoever triggered
it: "One consolidated review is on the PR." It decided that purely from
whether the posting job went green.

But the posting script goes green on purpose in one case where nothing was
posted
: if the run's token is read-only, GitHub rejects the review with a 403,
and rather than losing the review the script writes it into the run's job
summary and exits 0. So that DM was congratulating people over a PR with no
review on it — and pointing them nowhere.

Now the script says whether the review reached the PR, and the DM believes the
statement instead of the exit code. Same case, new message: a warning saying
the review is in the job summary and that the caller's permissions: block is
missing pull-requests: write.

What changed

.github/cursor-review/post-review.py — a fourth key on the existing
emit_delivery output: posted, true when some review body reached the PR,
whatever it said.

Why not reuse delivered (BE-4691), which already exists? Because it answers a
different question, and the two disagree in both directions:

path exit delivered posted
normal review posted 0 true true
error review ("Review failed") posted 0 false true
all-cells-failed no-findings review posted 0 false true
422 → anchor-free fallback posted 0 true true
read-only 403 → job summary 0 false false
genuine POST failure 1 false false

delivered is the blocking gate's question — "did an adjudicated review land,
so an empty thread query means clean?" — and it is deliberately false for the
two bodies that report a failure. Gating the DM on it would tell an author the
review could not be posted while it sits on their PR.

Placement: posted=True at exactly the two returncode == 0 sites
(post_or_degrade, and the main review path), before the truncated-summary
handling, so a clamped-but-posted review stays posted=true. It rides in the
same once-guarded emission as delivered rather than in a helper of its own —
the two are decided by the same branches, and a second emitter would need a
second guard kept in sync across every path. The emitter also normalizes
posted = posted or delivered, so a future delivery site that forgets the kwarg
cannot emit the incoherent "delivered but never posted".

post_error_review needed no change — it posts through post_or_degrade, so
the placement there covers it (verified: it emits delivered=false posted=true).

.github/workflows/cursor-review.yml — the post-review job surfaces
posted: ${{ steps.post.outputs.posted }} (the step already carried id: post
since #236) and notify-complete reads it as REVIEW_POSTED. After review
round 1 the DM reads delivered as well, and the green-job path is three
branches rather than two — see below. The two existing failure branches are
untouched.

Exit codes are unchanged. All four write_step_summary(..., note=POST_FAILED_SUMMARY_NOTE)
sites still raise SystemExit(1) and still produce the existing failure DM;
only the read-only-403 degradation was silent-green, and only its message
moved.

Coordination with #236

#236 (the credential split) merged at 75685ed, so this is built on the
post-#236 shape: the post lives in the checkout-free post-review job and the
DM already keys on needs.post-review.result. No conflict remains.

Verification

Both directions of the bug are pinned as unit tests, driving main() with a
stubbed gh and reading the real $GITHUB_OUTPUT file:

  • read-only 403 (Resource not accessible by integration, and a bare HTTP 403)
    → exit 0 and posted=false;
  • success → posted=true; 422-fallback and clamped-body → posted=true;
  • error review / all-cells-failed → delivered=false but posted=true;
  • genuine POST failure → SystemExit(1), never posted=true;
  • crash before deciding → nothing written (unset reads as not-true at the
    workflow, which is the fail-closed direction);
  • $GITHUB_OUTPUT unset → helper no-ops, no crash.

Plus a NotifyCompleteGateTest class that reads cursor-review.yml itself, so
the workflow half cannot silently regress: the job surfaces both outputs, the DM
reads both, and each of the three green-job branches' text is pinned inside
the branch it describes (see "Review round 1" below for how).
cursor-review.yml is already in test-cursor-review-scripts.yml's path
filter, so a workflow-only edit runs them.

Falsification of the new denial text: the DM's new "could not be posted on the
PR" is emitted only when the script itself reports posted=false. Before adding
it I checked that the script has no second posting path to try —
gh_post_review has exactly two call sites (post-review.py:370, :1416), both
covered here, and exactly one endpoint (POST /repos/{repo}/pulls/{n}/reviews,
:223). The 403 branch is not a new dead-end either: it is pre-existing behavior
that already wrote the review to the job summary and returned; this change only
stops the DM from misdescribing it.

Review round 1

Three real defects the panel found, all fixed in 782908b; the five threads are
resolved with the reasoning on each.

The degraded DETAIL asserted one cause for a branch with many. It read "the
workflow token lacks pull-requests: write"
, but is_read_only_token_error
treats any HTTP 403 as the read-only degradation (pinned by
test_a_bare_403_is_read_as_read_only_too), so an SSO/IP-allowlist,
secondary-rate-limit or archived-repo rejection lands in the same branch — as
does a bot_app_id App installation without pull-request write, where the
caller's permissions: block is not the knob at all, and a workflows_ref
resolving to a post-review.py too old to emit posted. Every one of those
would have sent an author to fix a block that is already correct. The DETAIL now
states the observation as fact and the cause as likelihood:

No review was posted on the PR — the full text is in the run's job summary.
Most often the run's token lacks pull-requests: write (check the caller's
permissions block); another 403 (SSO/IP allowlist, rate limit, archived repo)
or a stale workflows_ref pin reaches this too.

The alternative — teaching the script to classify GitHub's 403 sub-reasons out
of stderr — was rejected: it is the same brittle string-matching that produced
the over-assertion, and it cannot reach the unset-posted skew case at all.

The success branch still fired on a round the blocking gate refuses. posted
is deliberately true for the two delivered=false bodies — the "Review failed"
error review and the all-cells-failed review — so a crashed judge still produced
"Cursor review complete / One consolidated review is on the PR." The DM now
reads REVIEW_DELIVERED (the job output already at line 2167) as well:

delivered posted DM
true true ✅ Cursor review complete — "One consolidated review is on the PR."
false true ⚠️ Cursor review incomplete — "A review was posted on the PR, but it adjudicated nothing…"
false false ⚠️ Cursor review degraded — the message above

Checked before shipping that the new middle branch cannot swallow a healthy
round: delivered=true at every non-failure post — the main review path
(post-review.py:1423), the clean no-findings review (delivers=not all_failed)
and the 422 anchor-free fallback (post_or_degrade's default). The DM and the
gate now agree on which rounds count.

emit_delivery's comment lied about its own line. It said "assert the
invariant" over posted = posted or delivered, which silently repairs it. The
comment was fixed rather than the behaviour, and now says why: this runs
immediately after a review was successfully posted, so raising would turn a
forgotten kwarg into a red job and a "did not succeed" DM on a PR that visibly
has its review. It also records the direction that is not repairable here (a
site that posts and forgets posted=True — nothing in the function can see the
POST) and that direction's blast radius, which is bounded on the safe side: the
parameter defaults to False, so the worst case is a spurious "degraded" DM,
never a false success and never a green gate.

Two test defects. assertIn("pull-requests: write", self.text) could not
fail independently — that literal appears elsewhere in cursor-review.yml, so
the assertion meant to pin "the degraded branch names the cause" would have
stayed green with the cause deleted. All workflow assertions now go through one
assert_inside_branch(guard, phrase) helper that requires the phrase to be
unique and its offset to fall between its branch's guard and the next elif.
And test_a_review_that_could_not_even_be_attempted_exits_one was mislabelled:
run_main([]) returns on the no-findings branch long before the anchor split, so
it pinned a different failure than its comment claimed and the
no-inline-comments branch's posted=false was uncovered. It is renamed for the
branch it reaches, and a new case covers the real one with a finding whose anchor
misses the diff — verified by the run log emitting "no inline comments to drop",
not by reading the code. Suite is 403 passing, up from 400.

Not changed: READ_ONLY_SUMMARY_NOTE (post-review.py:259), the job summary
header, still names the read-only token as the cause with the same certainty the
DM has now dropped. It is pre-existing text on a path this PR does not otherwise
touch, and it is read next to the failing run, where the cause is visible —
cosmetic enough not to be worth widening the diff or a follow-up ticket.

Residual

Not covered by this PR — actionable on its own:

  • The Slack DM itself was never executed. The notify-complete branch is
    verified by asserting against the workflow source, not by running it: producing
    the real degraded DM needs a live run of cursor-review.yml from a caller whose
    token deliberately lacks pull-requests: write, plus a SLACK_BOT_TOKEN.
    Neither is reachable from a unit-test-only change, and standing up an
    intentionally under-permissioned caller is a mutation of a consumer repo, out of
    scope here. The first real read-only-token run after this merges is what confirms
    the message end to end.
  • test_post_review.py was not the file touched. The new cases went into
    test_post_review_delivery.py, which already owns the $GITHUB_OUTPUT contract
    and has the main()-driving harness this needs; putting them in
    test_post_review.py would have meant duplicating that harness. Same suite, same
    CI job, -p 'test_*.py' discovers both. The harness was extracted into a
    MainDriverMixin so the new class reuses it without re-running the existing
    class's cases.
  • Naming deviates from the request in one place: the job-level output is
    posted, not review_posted, to match its three siblings (delivered,
    gated_findings, ungated_findings) on the same block. The env var the DM reads
    is REVIEW_POSTED as specified. Nothing outside this file consumes either name
    today (vars.CURSOR_REVIEW_CALLERS consumers call the workflow, they do not read
    its job outputs), so this is not a breaking rename — but if some caller is later
    written against a documented output name, that name is posted.
  • test_workflow_job_isolation.py needed no change, as the request predicted;
    confirmed by running the whole suite rather than by reading it, so its parser was
    exercised against the new outputs: entry but its intent was not re-derived.
  • pytest was not used. The repo is stdlib-only with no pytest dependency and
    CI runs python3 -m unittest discover; the acceptance criterion's pytest
    invocation was satisfied with the equivalent unittest run over the same
    directory.

Provenance

  • Authored by: agent-work loop
  • Verified: re-run after review round 1 — python3 -m unittest discover -s .github/cursor-review/tests -p 'test_*.py' — 403 passed, 0 failed (20 new); python3 -m unittest discover -s .github/groom/tests -p 'test_*.py' — 374 passed, 1 skipped; python3 -m unittest discover -s .github/agents-md-integrity/tests -p 'test_*.py' — 46 passed; shellcheck -x .github/cursor-review/install-cursor-cli.sh .github/cursor-review/slack-notify.sh .github/bump-callers/bump-callers.sh — clean; actionlint .github/workflows/cursor-review.yml — one pre-existing warning at :548 (job.workflow_sha), untouched by this diff; python3 .github/agents-md-integrity/check_agents_md.py --root . — passed (2 pre-existing warnings). The Slack DM itself is still asserted against the workflow source, never executed (see ## Residual).
  • Deviations: output named posted rather than review_posted; new tests in test_post_review_delivery.py rather than test_post_review.py; unittest rather than pytest. Each is explained under ## Residual. Round 1 added a third green-job DM branch (Cursor review incomplete) that was not in the original request — it is the same class of false claim one branch further in, raised by 3 of 6 reviewers.

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/Comfy-Org/github-workflows/pull/262 **Author:** [@mattmillerai](https://github.com/mattmillerai) **Created:** 9/5/2026 **Status:** ✅ Merged **Merged:** 9/8/2026 **Merged by:** [@mattmillerai](https://github.com/mattmillerai) **Base:** `main` ← **Head:** `matt/be-10017-post-review-posted-output` --- ### 📝 Commits (2) - [`ab7211f`](https://github.com/Comfy-Org/github-workflows/commit/ab7211f2fa38b0628138db36f029724873f7c178) fix(cursor-review): stop the success DM claiming a review a read-only token never posted - [`782908b`](https://github.com/Comfy-Org/github-workflows/commit/782908bd8e39018ebc2d8c5df9daf2597c504b66) fix(cursor-review): stop the DM over-claiming on a posted-but-unadjudicated round (BE-10017) ### 📊 Changes **3 files changed** (+403 additions, -12 deletions) <details> <summary>View changed files</summary> 📝 `.github/cursor-review/post-review.py` (+48 -7) 📝 `.github/cursor-review/tests/test_post_review_delivery.py` (+308 -4) 📝 `.github/workflows/cursor-review.yml` (+47 -1) </details> ### 📄 Description ## ELI-5 When the cursor-review panel finishes, the workflow Slack-DMs whoever triggered it: *"One consolidated review is on the PR."* It decided that purely from whether the posting job went green. But the posting script goes green on purpose in one case where **nothing was posted**: if the run's token is read-only, GitHub rejects the review with a 403, and rather than losing the review the script writes it into the run's job summary and exits 0. So that DM was congratulating people over a PR with no review on it — and pointing them nowhere. Now the script *says* whether the review reached the PR, and the DM believes the statement instead of the exit code. Same case, new message: a **warning** saying the review is in the job summary and that the caller's `permissions:` block is missing `pull-requests: write`. ## What changed **`.github/cursor-review/post-review.py`** — a fourth key on the existing `emit_delivery` output: `posted`, true when *some* review body reached the PR, whatever it said. Why not reuse `delivered` (BE-4691), which already exists? Because it answers a different question, and the two disagree **in both directions**: | path | exit | `delivered` | `posted` | |---|---|---|---| | normal review posted | 0 | `true` | `true` | | error review ("Review failed") posted | 0 | **`false`** | **`true`** | | all-cells-failed no-findings review posted | 0 | **`false`** | **`true`** | | 422 → anchor-free fallback posted | 0 | `true` | `true` | | **read-only 403 → job summary** | **0** | `false` | **`false`** | | genuine POST failure | 1 | `false` | `false` | `delivered` is the *blocking gate's* question — "did an adjudicated review land, so an empty thread query means clean?" — and it is deliberately `false` for the two bodies that report a failure. Gating the DM on it would tell an author the review could not be posted while it sits on their PR. Placement: `posted=True` at exactly the two `returncode == 0` sites (`post_or_degrade`, and the main review path), before the truncated-summary handling, so a clamped-but-posted review stays `posted=true`. It rides in the same once-guarded emission as `delivered` rather than in a helper of its own — the two are decided by the same branches, and a second emitter would need a second guard kept in sync across every path. The emitter also normalizes `posted = posted or delivered`, so a future delivery site that forgets the kwarg cannot emit the incoherent "delivered but never posted". `post_error_review` needed no change — it posts through `post_or_degrade`, so the placement there covers it (verified: it emits `delivered=false posted=true`). **`.github/workflows/cursor-review.yml`** — the `post-review` job surfaces `posted: ${{ steps.post.outputs.posted }}` (the step already carried `id: post` since #236) and `notify-complete` reads it as `REVIEW_POSTED`. After review round 1 the DM reads `delivered` as well, and the green-job path is three branches rather than two — see below. The two existing failure branches are untouched. **Exit codes are unchanged.** All four `write_step_summary(..., note=POST_FAILED_SUMMARY_NOTE)` sites still `raise SystemExit(1)` and still produce the existing failure DM; only the read-only-403 degradation was silent-green, and only its *message* moved. ## Coordination with #236 #236 (the credential split) merged at `75685ed`, so this is built on the post-#236 shape: the post lives in the checkout-free `post-review` job and the DM already keys on `needs.post-review.result`. No conflict remains. ## Verification Both directions of the bug are pinned as unit tests, driving `main()` with a stubbed `gh` and reading the real `$GITHUB_OUTPUT` file: - read-only 403 (`Resource not accessible by integration`, and a bare `HTTP 403`) → **exit 0** *and* `posted=false`; - success → `posted=true`; 422-fallback and clamped-body → `posted=true`; - error review / all-cells-failed → `delivered=false` **but** `posted=true`; - genuine POST failure → `SystemExit(1)`, never `posted=true`; - crash before deciding → nothing written (unset reads as not-`true` at the workflow, which is the fail-closed direction); - `$GITHUB_OUTPUT` unset → helper no-ops, no crash. Plus a `NotifyCompleteGateTest` class that reads `cursor-review.yml` itself, so the workflow half cannot silently regress: the job surfaces both outputs, the DM reads both, and each of the three green-job branches' text is pinned *inside* the branch it describes (see "Review round 1" below for how). `cursor-review.yml` is already in `test-cursor-review-scripts.yml`'s path filter, so a workflow-only edit runs them. Falsification of the new denial text: the DM's new "could not be posted on the PR" is emitted only when the script itself reports `posted=false`. Before adding it I checked that the script has no second posting path to try — `gh_post_review` has exactly two call sites (post-review.py:370, :1416), both covered here, and exactly one endpoint (`POST /repos/{repo}/pulls/{n}/reviews`, :223). The 403 branch is not a new dead-end either: it is pre-existing behavior that already wrote the review to the job summary and returned; this change only stops the DM from misdescribing it. ## Review round 1 Three real defects the panel found, all fixed in `782908b`; the five threads are resolved with the reasoning on each. **The degraded DETAIL asserted one cause for a branch with many.** It read *"the workflow token lacks `pull-requests: write`"*, but `is_read_only_token_error` treats **any** `HTTP 403` as the read-only degradation (pinned by `test_a_bare_403_is_read_as_read_only_too`), so an SSO/IP-allowlist, secondary-rate-limit or archived-repo rejection lands in the same branch — as does a `bot_app_id` App installation without pull-request write, where the caller's `permissions:` block is not the knob at all, and a `workflows_ref` resolving to a `post-review.py` too old to emit `posted`. Every one of those would have sent an author to fix a block that is already correct. The DETAIL now states the observation as fact and the cause as likelihood: > No review was posted on the PR — the full text is in the run's job summary. > Most often the run's token lacks `pull-requests: write` (check the caller's > permissions block); another 403 (SSO/IP allowlist, rate limit, archived repo) > or a stale `workflows_ref` pin reaches this too. The alternative — teaching the script to classify GitHub's 403 sub-reasons out of stderr — was rejected: it is the same brittle string-matching that produced the over-assertion, and it cannot reach the unset-`posted` skew case at all. **The success branch still fired on a round the blocking gate refuses.** `posted` is deliberately `true` for the two `delivered=false` bodies — the "Review failed" error review and the all-cells-failed review — so a crashed judge still produced *"Cursor review complete / One consolidated review is on the PR."* The DM now reads `REVIEW_DELIVERED` (the job output already at line 2167) as well: | `delivered` | `posted` | DM | |---|---|---| | true | true | ✅ `Cursor review complete` — "One consolidated review is on the PR." | | **false** | **true** | ⚠️ `Cursor review incomplete` — "A review was posted on the PR, but it adjudicated nothing…" | | false | false | ⚠️ `Cursor review degraded` — the message above | Checked before shipping that the new middle branch cannot swallow a healthy round: `delivered=true` at every non-failure post — the main review path (`post-review.py:1423`), the clean no-findings review (`delivers=not all_failed`) and the 422 anchor-free fallback (`post_or_degrade`'s default). The DM and the gate now agree on which rounds count. **`emit_delivery`'s comment lied about its own line.** It said "assert the invariant" over `posted = posted or delivered`, which silently *repairs* it. The comment was fixed rather than the behaviour, and now says why: this runs immediately after a review was successfully posted, so raising would turn a forgotten kwarg into a red job and a "did not succeed" DM on a PR that visibly has its review. It also records the direction that is **not** repairable here (a site that posts and forgets `posted=True` — nothing in the function can see the POST) and that direction's blast radius, which is bounded on the safe side: the parameter defaults to `False`, so the worst case is a spurious "degraded" DM, never a false success and never a green gate. **Two test defects.** `assertIn("pull-requests: write", self.text)` could not fail independently — that literal appears elsewhere in `cursor-review.yml`, so the assertion meant to pin "the degraded branch names the cause" would have stayed green with the cause deleted. All workflow assertions now go through one `assert_inside_branch(guard, phrase)` helper that requires the phrase to be unique *and* its offset to fall between its branch's guard and the next `elif`. And `test_a_review_that_could_not_even_be_attempted_exits_one` was mislabelled: `run_main([])` returns on the no-findings branch long before the anchor split, so it pinned a different failure than its comment claimed and the no-inline-comments branch's `posted=false` was uncovered. It is renamed for the branch it reaches, and a new case covers the real one with a finding whose anchor misses the diff — verified by the run log emitting "no inline comments to drop", not by reading the code. Suite is 403 passing, up from 400. Not changed: `READ_ONLY_SUMMARY_NOTE` (`post-review.py:259`), the *job summary* header, still names the read-only token as the cause with the same certainty the DM has now dropped. It is pre-existing text on a path this PR does not otherwise touch, and it is read next to the failing run, where the cause is visible — cosmetic enough not to be worth widening the diff or a follow-up ticket. ## Residual Not covered by this PR — actionable on its own: - **The Slack DM itself was never executed.** The `notify-complete` branch is verified by asserting against the workflow source, not by running it: producing the real degraded DM needs a live run of `cursor-review.yml` from a caller whose token deliberately lacks `pull-requests: write`, plus a `SLACK_BOT_TOKEN`. Neither is reachable from a unit-test-only change, and standing up an intentionally under-permissioned caller is a mutation of a consumer repo, out of scope here. The first real read-only-token run after this merges is what confirms the message end to end. - **`test_post_review.py` was not the file touched.** The new cases went into `test_post_review_delivery.py`, which already owns the `$GITHUB_OUTPUT` contract and has the `main()`-driving harness this needs; putting them in `test_post_review.py` would have meant duplicating that harness. Same suite, same CI job, `-p 'test_*.py'` discovers both. The harness was extracted into a `MainDriverMixin` so the new class reuses it without re-running the existing class's cases. - **Naming deviates from the request in one place:** the job-level output is `posted`, not `review_posted`, to match its three siblings (`delivered`, `gated_findings`, `ungated_findings`) on the same block. The env var the DM reads is `REVIEW_POSTED` as specified. Nothing outside this file consumes either name today (`vars.CURSOR_REVIEW_CALLERS` consumers call the workflow, they do not read its job outputs), so this is not a breaking rename — but if some caller is later written against a documented output name, that name is `posted`. - **`test_workflow_job_isolation.py` needed no change**, as the request predicted; confirmed by running the whole suite rather than by reading it, so its parser was exercised against the new `outputs:` entry but its intent was not re-derived. - **`pytest` was not used.** The repo is stdlib-only with no `pytest` dependency and CI runs `python3 -m unittest discover`; the acceptance criterion's `pytest` invocation was satisfied with the equivalent `unittest` run over the same directory. ## Provenance - **Authored by:** agent-work loop - **Verified:** re-run after review round 1 — `python3 -m unittest discover -s .github/cursor-review/tests -p 'test_*.py'` — 403 passed, 0 failed (20 new); `python3 -m unittest discover -s .github/groom/tests -p 'test_*.py'` — 374 passed, 1 skipped; `python3 -m unittest discover -s .github/agents-md-integrity/tests -p 'test_*.py'` — 46 passed; `shellcheck -x .github/cursor-review/install-cursor-cli.sh .github/cursor-review/slack-notify.sh .github/bump-callers/bump-callers.sh` — clean; `actionlint .github/workflows/cursor-review.yml` — one pre-existing warning at :548 (`job.workflow_sha`), untouched by this diff; `python3 .github/agents-md-integrity/check_agents_md.py --root .` — passed (2 pre-existing warnings). The Slack DM itself is still asserted against the workflow source, never executed (see `## Residual`). - **Deviations:** output named `posted` rather than `review_posted`; new tests in `test_post_review_delivery.py` rather than `test_post_review.py`; `unittest` rather than `pytest`. Each is explained under `## Residual`. Round 1 added a third green-job DM branch (`Cursor review incomplete`) that was not in the original request — it is the same class of false claim one branch further in, raised by 3 of 6 reviewers. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-26 22:37:54 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Comfy-Org/github-workflows#265
No description provided.