Skip to content

fix(scheduler): match the run name GitHub actually sends for central review runs - #1983

Merged
seonghobae merged 1 commit into
mainfrom
fix/central-dispatch-run-name-matching
Sep 6, 2026
Merged

fix(scheduler): match the run name GitHub actually sends for central review runs#1983
seonghobae merged 1 commit into
mainfrom
fix/central-dispatch-run-name-matching

Conversation

@seonghobae

@seonghobae seonghobae commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

What is broken

scripts/ci/pr_review_merge_scheduler_core.py matched a workflow run's name
exactly against the review-workflow aliases:

run_name = str(run_data.get("name") or "")
if run_name != workflow and run_name not in workflow_aliases:
    continue

Eight workflows in this repository define run-name:, and that set contains
every workflow whose runs this matcher looks for. For such a workflow GitHub
puts the rendered run name in name, identical to display_title:

name          "OpenCode Review Dispatch ContextualWisdomLab/.github#834@e748ee61…"
display_title  (same string)
path          .github/workflows/opencode-review-dispatch.yml

Sampled 2026-09-07 over 100 runs of opencode-review-dispatch.yml: 100
rendered, 0 bare.
So the exact match dropped every production dispatch run at
that line, before the event == "repository_dispatch" branch immediately
below it that exists to read them.

What that cost

already_running never suppressed a same-head repeat
stale never populated, so older-head central runs were never cancelled

.github#1529 took 27 dispatches on one unchanged head over 100.8 hours.
Each new run's creation preceded the previous run's cancellation by about three
seconds, so the previous run was demonstrably still active when the check ran
and did not see it.

Correction to a number I published first. I initially counted "20 duplicates
of 45 active runs" by grouping on repository and PR without the workflow, so
runs of different dispatch workflows on one PR counted as duplicates of each
other. host 1 grouped by (workflow, repo#pr) and got a different shape, which
sent me back to recount. Regrouped:

active repository_dispatch runs (queued + in_progress)   33
  codeql-scan-dispatch.yml      25 runs / 12 keys / 13 same-head duplicates
  opencode-review-dispatch.yml   5 runs /  5 keys /  0 duplicates
  pr-review-autofix.yml          3 runs /  3 keys /  0 duplicates

The workflows this matcher governs show no live duplication at this instant.
The case for this change is the historical chain on .github#1529 and a
suppression that has never once fired — not a backlog visible right now. The 13
duplicates all belong to CodeQL Scan Dispatch, which this matcher does not
govern; that workflow also defines run-name:, so it is a separate lead rather
than evidence for this change.

The repository already knew this — in one place

scripts/ci/opencode_coverage_identity.py:143-147 accepts both forms:

expected_title = f"{DISPATCH_WORKFLOW_NAME} {target_repo}#{pr_number}@{head_sha}"
workflow_name = str(workflow_run.get("name") or "").strip()
if workflow_name not in {DISPATCH_WORKFLOW_NAME, expected_title}:
    raise CoverageQuoteError(...)

So the rendered form is known behaviour here, and :3257 is not an unknown
GitHub quirk but a place where the in-repo answer was missed. (Found by another
session while cross-checking this change.)

Both lanes were blinded, not just the dispatch lane. Live names:

Required OpenCode Review ContextualWisdomLab/.github#1980@8073a10e…   pull_request_target
Strix Security Scan      ContextualWisdomLab/.github#1983@43158bc1…   pull_request_target
Strix Security Scan      ContextualWisdomLab/.github#event@5c60b5d9…  push

opencode-review.yml and strix.yml runs carry rendered names too, so the exact
match dropped PR-lane runs as well.

Why the permissive form, not the exact-title form used in
opencode_coverage_identity.py.
A tight match against
dispatch_title_prefixes would also drop the #event@ push shape and would
change what reaches the non-centralised head_sha branch — a mode I cannot
observe from here. Prefix matching is strictly more permissive than the code it
replaces, so it cannot drop a run the current code accepts; the exact-title
tightening belongs with the path/workflow_id rework, not here.

Why the tests did not catch it

The existing fixture sets a bare name beside a rendered display_title — a
payload GitHub never emits for a run-name: workflow. 100% line coverage of
that branch never showed that production cannot reach it. Coverage answers
"did this line execute", not "does it execute on the input production sends".

Scope

Confined to the run comparison. OPENCODE_WORKFLOW_NAMES is unchanged: its
other consumer compares a workflow object's name, which is genuinely bare,
and prefix-matching there would loosen wrongly. active_review_run_refs has
exactly two call sites, OpenCode's and Strix's, so both are fixed here — and
the Strix side is pinned by its own test so a later narrowing to the OpenCode
aliases cannot silently reopen half of it.

Behaviour this enables, stated plainly

While a same-head central run is active, a repeat is now suppressed. A run that
never terminates would therefore hold the PR, where before the check simply
never fired. Reviving stale cancellation is separately safe: of 163
non-terminal central runs, 49 are review/dispatch kind and 4 become
cancellable, all subjects that no longer exist (3 closed/merged PRs, 1 moved
head).

Evidence

gate            2970 passed, 1 skipped, 21 subtests   (predicted 2968 + 2)
coverage        100%, 0 missing
interrogate     100%
two-way control fix present  → central_run_filter 5 passed
                fix reverted → 2 failed, one per call site (OpenCode, Strix)
                restored     → 5 passed, dirty=0

Independently confirmed by two other sessions: the 100/100 sample, the eight
run-name: workflows, and the blast-radius classification.

Developer experience: the scheduler's same-head suppression and stale-run
cancellation work against real payloads instead of a shape only the tests
produce.
User experience: a pull request stops accumulating duplicate concurrent review
runs that cancel each other, so a review that starts can finish.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Sep 6, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 14 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: d6d909fe-1f76-4e94-b0a0-e0e48e2f3cd6

📥 Commits

Reviewing files that changed from the base of the PR and between 49eb9e7 and 43158bc.

📒 Files selected for processing (2)
  • scripts/ci/pr_review_merge_scheduler_core.py
  • tests/test_pr_review_merge_scheduler.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…review runs

`active_review_run_refs` matched a workflow run's `name` exactly against the
review workflow aliases. But eight workflows in this repository define
`run-name:`, and that set contains every workflow whose runs this matcher looks
for -- `opencode-review.yml` ("Required OpenCode Review"),
`opencode-review-dispatch.yml` ("OpenCode Review Dispatch") and `strix.yml`
("Strix Security Scan"). For such a workflow GitHub reports the *rendered* run
name in `name` -- the same string as `display_title`, e.g.

    OpenCode Review Dispatch #834e748ee6...

Sampled 2026-09-07: 100 of 100 opencode-review-dispatch runs carry that form
and none carries the bare workflow name. So the exact match dropped every
production dispatch run at this line, before the `event == "repository_dispatch"`
branch immediately below that exists to read them. Two consequences:

  * `already_running` never suppressed a same-head repeat. .github#1529 took 27
    dispatches on one unchanged head over 100.8 hours; each new run's creation
    preceded the previous run's cancellation by about three seconds, so the
    previous run was demonstrably still active when the check ran and did not
    see it.
  * `stale` never populated, so older-head central runs were never cancelled.

A first count of the live queue said "20 duplicates of 45 active runs" and was
wrong: it grouped by repository and PR without the workflow, so runs of
different dispatch workflows on one PR were counted as duplicates of each other.
Regrouped by (workflow, repository, PR):

    active repository_dispatch runs, queued + in_progress   33
      codeql-scan-dispatch.yml      25 runs / 12 keys / 13 same-head duplicates
      opencode-review-dispatch.yml   5 runs /  5 keys /  0 duplicates
      pr-review-autofix.yml          3 runs /  3 keys /  0 duplicates

So the workflows this matcher governs show no live duplication at this instant.
The harm this fix addresses is the historical chain on .github#1529 and a
suppression that has never once fired, not a backlog visible right now. The 13
duplicates all belong to CodeQL Scan Dispatch, which this matcher does not
govern; that workflow also defines `run-name:`, which makes it a separate lead
rather than evidence for this change. Reviving stale cancellation is separately
safe: of 163 non-terminal central runs, 49 are review or dispatch
kind and 4 become cancellable, all of them subjects that no longer exist (3
closed or merged PRs, 1 moved head).

The fix is confined to the run comparison. `OPENCODE_WORKFLOW_NAMES` is
unchanged, because its other consumer compares a *workflow* object's name,
which is genuinely bare. `active_review_run_refs` has exactly two call sites,
OpenCode's and Strix's, so both are fixed here; the Strix side is pinned by its
own test so a later narrowing to the OpenCode aliases cannot silently reopen
half of it.

This is one instance of a class, and the file already contains the stable form.
`run.name` is compared as an identifier at four places -- `:1250`, `:3198`,
`:3254` (this one) and `:3783` -- while `:3060` keys on
`run.get("workflow_id") or run.get("path") or run.get("name")`, which cannot be
rewritten by a `run-name:`. `:1250` in particular feeds the REST fallback's
workflow-level policy boundary and would see a rendered title where it expects a
workflow name. Fixing the whole class means moving the callers from display
names to paths, which also touches how `dispatch_title_prefixes` is built, so it
is deliberately left out of this change; .github#1941 is the same root seen from
the `display_title` side. Recorded here so the next reader does not rediscover
it as a fifth instance.

Note the new behaviour this enables: while a same-head central run is active,
a repeat is now suppressed. A run that never terminates would therefore hold
the PR, where before the check simply never fired.

The existing fixture sets a bare `name` beside a rendered `display_title`, a
payload GitHub never emits for a `run-name:` workflow, which is why 100 percent
line coverage of that branch never revealed that production could not reach it.

Developer experience: the scheduler's same-head suppression and stale-run
cancellation work against real payloads instead of a shape only the tests
produce.
User experience: a pull request stops accumulating duplicate concurrent review
runs that cancel each other, so a review that starts can finish.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@seonghobae
seonghobae force-pushed the fix/central-dispatch-run-name-matching branch from 77c8b19 to 43158bc Compare September 6, 2026 18:15
@seonghobae

Copy link
Copy Markdown
Contributor Author

Verified by running it. The premise now has the control arm it was missing, the fix cannot drop a run, and it cannot over-match. Ready to merge.

The premise, tested where it could break

Three prior samples all pointed the same way, which is the shape worth distrusting, so I looked for the arm none of them had: workflows that do not declare run-name:.

workflow declares run-name: distinct run name values value
python-security.yml no 1 Python Security
sast-semgrep.yml no 1 SAST Semgrep
secret-scan.yml no 1 Secret Scan

The bare workflow name appears exactly when run-name: is absent. That turns the earlier correlation into a mechanism: the rendered title is not an artifact of a sampling window, it is the documented consequence of declaring run-name.

Then a different repository and an older page, to escape both the .github sample and the recent window:

naruon                    17 matching runs   BARE 0   rendered 17
contextual-orchestrator   20 matching runs   BARE 0   rendered 20

No bare form was found anywhere it should not be, and it was found everywhere it should be.

Can the change drop a run the current code accepts?

No, and it is provable rather than empirical. The old predicate accepts run_name == workflow or run_name in aliases. The new one accepts, for each candidate in (workflow, *aliases), run_name == candidate or run_name.startswith(candidate + " "). Every old acceptance is an equality against some candidate, and the new predicate tests that same equality first, so the accepted set is a strict superset.

Can it over-match?

Checked the opposite risk, since a prefix is looser than an equality: no workflow in this repository has a name that begins with any alias followed by a space, so no unrelated workflow can be captured. The only cross-alias match is "OpenCode Review" matching an "OpenCode Review Dispatch …" run, and those two are already members of the same alias set for this matcher's purpose.

Gate and isolated control

check result
pytest 2971 passed, 1 skipped, 21 subtests
coverage (scripts/ci) 100%, 0 missed of 13196
interrogate 100%

Arithmetic predicted before running: main is 2969 with #1980 in it, plus 2 new tests, gives 2971. That is what ran, so this adds exactly two tests and no duplicate.

Reverting only the matcher block while keeping the new tests fails exactly two, one per call site:

test_central_run_filter_accepts_the_run_name_github_actually_sends
test_central_run_filter_reads_the_rendered_strix_run_name_too

The Strix-specific test is worth keeping deliberately: it pins the second call site so a later change cannot narrow this fix to the OpenCode aliases and silently reopen the Strix path.

The 26 deletions the raw diff shows in the queue-contract file are the branch predating #1980; that test is present in the merged tree.

🤖 Addressed by Claude Code

@seonghobae
seonghobae merged commit 74224b2 into main Sep 6, 2026
4 of 16 checks passed
@seonghobae
seonghobae deleted the fix/central-dispatch-run-name-matching branch September 6, 2026 18:34
@seonghobae

Copy link
Copy Markdown
Contributor Author

Addendum: the non-centralized path is covered, and the org-wide over-match check is clean.

The concern raised was that "prefix is strictly more permissive, so nothing is dropped" went untested, with centralized_dispatch == False as the unobservable mode. Splitting that into its two halves changes where the risk actually sits.

The no-drop half is a proof, not an observation, and it is mode-independent. The predicate is local: old accepts only via equality against workflow or a member of workflow_aliases; new tests that same equality first, then additionally a prefix. Any input the old form accepted is accepted by the new one, whatever workflow and workflow_aliases happen to be. No execution path can change that, because the mode never reaches inside the comparison.

The over-match half is empirical, and that is where the mode matters. In non-centralized mode repository_dispatch_target returns the target repository itself, so the loop scans a sibling repository's runs rather than this one's. My earlier check only covered .github, so it did not cover that case. Repeating it across the whole organization:

75 non-archived repositories scanned for workflow names beginning with
"OpenCode Review " / "Required OpenCode Review " / "OpenCode Review Dispatch " / "Strix Security Scan "

collisions found: 0

So no workflow anywhere in the organization can be captured by the prefix, in either mode.

For completeness, centralized_dispatch is read exactly once inside this function, after the name filter, so it does not influence which names match — only what happens to a run once matched.

🤖 Addressed by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant