Skip to content
Merged
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
21 changes: 21 additions & 0 deletions scripts/ci/pr_review_merge_scheduler_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2845,6 +2845,26 @@ def rerun_actions_job(repo: str, job_id: str, *, dry_run: bool, action: str) ->
reset_active_workflow_runs_cache()


def actions_run_has_no_jobs(repo: str, run_id: int) -> bool:
"""Return whether GitHub created no jobs for a completed workflow run."""
payload = json.loads(
run_github_read(
[
"gh",
"api",
"--method",
"GET",
f"repos/{validate_github_repository(repo)}/actions/runs/{int(run_id)}/jobs",
"-f",
"filter=all",
"-F",
"per_page=1",
]
)
)
return payload.get("total_count") == 0


def recover_current_head_startup_failures(
repo: str,
pr: dict[str, Any],
Expand Down Expand Up @@ -2890,6 +2910,7 @@ def recover_current_head_startup_failures(
if run.get("head_sha") == head_sha
and run.get("status") == "completed"
and run.get("conclusion") == "startup_failure"
and actions_run_has_no_jobs(repo, int(run["id"]))
]
if (
retryable
Expand Down
47 changes: 47 additions & 0 deletions tests/test_pr_review_merge_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -4906,6 +4906,7 @@ def fake_read(args):
)

monkeypatch.setattr(sched, "run_github_read", fake_read)
monkeypatch.setattr(sched, "actions_run_has_no_jobs", lambda _repo, _run_id: True)
monkeypatch.setattr(
sched,
"restamp_pr_head_after_startup_failure",
Expand Down Expand Up @@ -4952,6 +4953,7 @@ def test_recover_current_head_startup_failures_does_not_restamp_twice(monkeypatc
}
),
)
monkeypatch.setattr(sched, "actions_run_has_no_jobs", lambda _repo, _run_id: True)
monkeypatch.setattr(
sched,
"restamp_pr_head_after_startup_failure",
Expand Down Expand Up @@ -4990,6 +4992,7 @@ def test_recover_current_head_startup_failures_restamps_codeql_alone(
"run_github_read",
lambda _args: json.dumps({"workflow_runs": [run]}),
)
monkeypatch.setattr(sched, "actions_run_has_no_jobs", lambda _repo, _run_id: True)
monkeypatch.setattr(
sched,
"restamp_pr_head_after_startup_failure",
Expand All @@ -5004,6 +5007,50 @@ def test_recover_current_head_startup_failures_restamps_codeql_alone(
assert restamps == [("owner/repo", head_sha, {"dry_run": False})]


def test_recover_current_head_startup_failures_ignores_runs_with_jobs(monkeypatch):
head_sha = "a" * 40
run = {
"id": 92,
"workflow_id": 12,
"name": "Required OpenCode Review",
"event": "pull_request_target",
"head_sha": head_sha,
"status": "completed",
"conclusion": "startup_failure",
"created_at": "2026-09-04T01:02:00Z",
}
monkeypatch.setattr(
sched,
"run_github_read",
lambda _args: json.dumps({"workflow_runs": [run]}),
)
monkeypatch.setattr(sched, "actions_run_has_no_jobs", lambda _repo, _run_id: False)
monkeypatch.setattr(
sched,
"restamp_pr_head_after_startup_failure",
lambda *_args, **_kwargs: pytest.fail("a run with jobs is not a pre-job failure"),
)

assert sched.recover_current_head_startup_failures(
"owner/repo", make_pr(headRefOid=head_sha), dry_run=False
) == []


def test_actions_run_has_no_jobs_checks_every_attempt(monkeypatch):
calls = []
monkeypatch.setattr(
sched,
"run_github_read",
lambda args: calls.append(args) or json.dumps({"total_count": 0, "jobs": []}),
)

assert sched.actions_run_has_no_jobs("owner/repo", 92)
assert calls == [[
"gh", "api", "--method", "GET", "repos/owner/repo/actions/runs/92/jobs",
"-f", "filter=all", "-F", "per_page=1",
]]


def test_inspect_pr_recovers_startup_failure_before_other_actions(monkeypatch):
monkeypatch.setenv("GITHUB_ACTIONS", "true")
monkeypatch.setattr(
Expand Down
Loading