Skip to content
Draft
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
18 changes: 17 additions & 1 deletion scripts/ci/pr_review_merge_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"
Expand Down
90 changes: 90 additions & 0 deletions tests/test_pr_review_merge_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2232,6 +2232,90 @@ def fake_active_runs(repo, statuses=("queued", "in_progress")):
assert sched.force_cancel_workflow_runs("owner/repo", []) == {}


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"]


def test_active_run_filters_and_stale_opencode_dry_run(monkeypatch):
runs = [
{
Expand Down Expand Up @@ -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"

Expand Down
Loading