From 8ae3d2a4f10f0a543324ab35b3c4deec75c0d484 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Jul 2026 09:46:12 +0000 Subject: [PATCH 1/2] fix: scheduler dispatches review when only pull_request_target OpenCode run is active When a PR is synchronized, the OpenCode required workflow fires on pull_request_target and the PR Review Merge Scheduler fires concurrently on the same event. The scheduler queries the PR's statusCheckRollup and sees the opencode-review check run as IN_PROGRESS, concluding "OpenCode review is already in progress" and waiting. The problem: pull_request_target runs of the OpenCode workflow always skip the real review job via `github.event_name == 'repository_dispatch'` gate. Once the pull_request_target run completes (with the review job SKIPPED), no workflow_run retry fires for required workflows, so the scheduler never dispatches the real review. Fix 1 (active_review_run_refs): Exclude pull_request and pull_request_target event runs from the target repo when classifying active review runs. These events never execute the review job and must not be counted as active reviews. Fix 2 (inspect_pr): When opencode_progress_state returns "running", call active_opencode_run_refs to verify an actual repository_dispatch run is active. If only PR-event runs exist, override the state to "absent" so the review is dispatched immediately instead of stalling. Adds tests for both fix points with 100% coverage maintained. --- scripts/ci/pr_review_merge_scheduler.py | 18 ++++- tests/test_pr_review_merge_scheduler.py | 92 ++++++++++++++++++++++++- 2 files changed, 108 insertions(+), 2 deletions(-) diff --git a/scripts/ci/pr_review_merge_scheduler.py b/scripts/ci/pr_review_merge_scheduler.py index c13adfe9..0cfa4d3e 100644 --- a/scripts/ci/pr_review_merge_scheduler.py +++ b/scripts/ci/pr_review_merge_scheduler.py @@ -1920,6 +1920,12 @@ def active_review_run_refs( continue if run_repo != target_repo: continue + # pull_request and pull_request_target runs always skip the review job + # via the ``github.event_name == 'repository_dispatch'`` gate in the + # review workflow. Treat them as non-reviews so the scheduler is not + # stalled waiting for a run that will never produce review evidence. + if run_data.get("event") in {"pull_request", "pull_request_target"}: + continue run_head = str(run_data.get("head_sha") or "").lower() pull_requests = run_data.get("pull_requests") or [] if run_head == head: @@ -2559,7 +2565,17 @@ def request_branch_update(freshness_reason: str, *, suffix: str = "") -> Decisio opencode_state = opencode_progress_state(pr, stale_after_minutes=stale_opencode_minutes) if opencode_state == "running": - return decide("wait", "OpenCode review is already in progress") + # A pull_request_target run of the OpenCode workflow always skips the + # real review job via the ``github.event_name == 'repository_dispatch'`` + # gate. Verify that an actual repository_dispatch run is active before + # waiting; if only PR-event runs exist the scheduler would stall + # indefinitely because no workflow_run retry fires for required + # workflows. When no dispatch run is found, override the state to + # absent so the review is dispatched immediately. + current_run_refs, _ = active_opencode_run_refs(repo, workflow, pr) + if current_run_refs: + return decide("wait", "OpenCode review is already in progress") + opencode_state = "absent" if ( os.environ.get("GITHUB_EVENT_NAME") == "workflow_run" diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py index 1fe10da7..1e054939 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -2232,7 +2232,91 @@ def fake_active_runs(repo, statuses=("queued", "in_progress")): assert sched.force_cancel_workflow_runs("owner/repo", []) == {} -def test_active_run_filters_and_stale_opencode_dry_run(monkeypatch): +def test_active_run_refs_excludes_pull_request_target_runs_in_target_repo(monkeypatch): + """pull_request_target runs in the target repo skip the review job and must not be treated as active reviews.""" + head_sha = "a" * 40 + target_runs = [ + { + "id": 9500, + "name": "Required OpenCode Review", + "event": "pull_request_target", + "head_sha": head_sha, + "pull_requests": [{"number": 1}], + }, + { + "id": 9501, + "name": "Required OpenCode Review", + "event": "pull_request", + "head_sha": head_sha, + "pull_requests": [{"number": 1}], + }, + ] + + def fake_active_runs(repo, statuses=("queued", "in_progress")): + del statuses + return target_runs if repo == "owner/repo" else [] + + monkeypatch.setattr(sched, "active_workflow_runs", fake_active_runs) + monkeypatch.setenv("SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY", "owner/repo") + + assert sched.active_opencode_run_refs( + "owner/repo", + "Required OpenCode Review", + make_pr(headRefOid=head_sha), + ) == ([], []) + + +def test_inspect_pr_dispatches_when_only_pr_event_opencode_run_is_active(monkeypatch): + """Scheduler dispatches a review when the only active OpenCode run is a pull_request_target run. + + pull_request_target runs always skip the review job via the + ``github.event_name == 'repository_dispatch'`` gate in the OpenCode workflow. + The scheduler must not wait for such a run and must dispatch a real review instead. + """ + head_sha = "a" * 40 + pt_run = { + "id": 9999, + "name": "Required OpenCode Review", + "event": "pull_request_target", + "head_sha": head_sha, + "pull_requests": [{"number": 1}], + } + + monkeypatch.setattr( + sched, + "active_workflow_runs", + lambda repo, statuses=("queued", "in_progress"): [pt_run] if repo == "owner/repo" else [], + ) + # No cross-repo dispatch complexity: dispatch_repo == target_repo. + monkeypatch.setenv("SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY", "owner/repo") + + dispatched = [] + monkeypatch.setattr( + sched, + "dispatch_opencode_review", + lambda repo, workflow, pr, dry_run: dispatched.append(workflow) or "dispatched", + ) + + # PR has a running OpenCode check from the concurrent pull_request_target run + # plus completed Strix evidence (so OpenCode dispatch is the next step). + running_pr = make_pr( + headRefOid=head_sha, + statusCheckRollup={ + "contexts": { + "nodes": [ + opencode_check(), + strix_check(), + ] + } + }, + ) + result = inspect(running_pr) + assert result.action == "review_dispatch" + assert "same-head OpenCode dispatched" in result.reason + assert dispatched == ["OpenCode Review"] + + + runs = [ { "id": 9200, @@ -3915,6 +3999,12 @@ def test_inspect_pr_handles_approved_reviews_and_dispatch(monkeypatch): assert external_blocked.action == "wait" assert "fork or external PR heads are excluded from scheduler direct merge and auto-merge" in external_blocked.reason + # An active repository_dispatch run is present: scheduler waits. + monkeypatch.setattr( + sched, + "active_opencode_run_refs", + lambda repo, workflow, pr: ([("owner/repo", "1")], []), + ) running = make_pr(statusCheckRollup={"contexts": {"nodes": [opencode_check()]}}) assert inspect(running).reason == "OpenCode review is already in progress" From 51f8352eb41353241e98860d37d162f46ae6bdcf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Jul 2026 09:47:47 +0000 Subject: [PATCH 2/2] fix: restore orphaned test_active_run_filters_and_stale_opencode_dry_run function def --- tests/test_pr_review_merge_scheduler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py index 1e054939..6be4392b 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -2316,7 +2316,7 @@ def test_inspect_pr_dispatches_when_only_pr_event_opencode_run_is_active(monkeyp assert dispatched == ["OpenCode Review"] - +def test_active_run_filters_and_stale_opencode_dry_run(monkeypatch): runs = [ { "id": 9200,