diff --git a/scripts/ci_macos_admission.py b/scripts/ci_macos_admission.py index 35453dde04..71876c505c 100644 --- a/scripts/ci_macos_admission.py +++ b/scripts/ci_macos_admission.py @@ -57,6 +57,8 @@ NIGHTLY_LEAD = datetime.timedelta(minutes=10) NIGHTLY_GRACE = datetime.timedelta(minutes=45) ACTIVE = {"queued", "in_progress", "waiting", "pending", "requested"} +# Runs created this recently are listed whatever their status. +RECENT = datetime.timedelta(hours=24) FAILURES_BEFORE_ADMIT = 3 @@ -164,9 +166,16 @@ def measure(api, repo, workflow, run_id, now, priority=False): total, lines = 0, [] # A run is listed as queued whenever one of its jobs waits for a runner, and # as pending whenever one waits in a concurrency group, so runs in any of these - # can hold runners. A run can move between the lists between the calls. + # can hold runners. A run that moves between the lists between the calls is + # in neither, as one is whenever the job ahead of this one leaves the line, so + # runs created recently are listed by creation time as well, and this run is + # read directly. + since = (now - RECENT).strftime("%Y-%m-%dT%H:%M:%SZ") runs = {run["id"]: run for status in ("in_progress", "queued", "pending", "waiting") for run in list_runs(api, repo, workflow, status=status)} + runs.update((run["id"], run) for run in list_runs(api, repo, workflow, created=f">={since}") + if run["status"] != "completed") + runs[run_id] = api.get(f"repos/{repo}/actions/runs/{run_id}") for run in runs.values(): jobs = api.get(f"repos/{repo}/actions/runs/{run['id']}/jobs", filter="latest", per_page=100)["jobs"] diff --git a/scripts/tests/test_ci_macos_admission.py b/scripts/tests/test_ci_macos_admission.py index c9f8ddee38..29e80e3a25 100644 --- a/scripts/tests/test_ci_macos_admission.py +++ b/scripts/tests/test_ci_macos_admission.py @@ -165,13 +165,16 @@ class FakeApi: def get(self, path, **params): self.calls.append((path, params)) - if path.endswith("/runs") and "status" in params: - runs = self.responses.get(("runs", params["status"]), []) + if path.endswith("/runs") and ("status" in params or "created" in params): + runs = self.responses.get(("runs", params.get("status", "recent")), []) start = (params["page"] - 1) * params["per_page"] return {"total_count": len(runs), "workflow_runs": runs[start:start + params["per_page"]]} if path.endswith("/runs"): return {"workflow_runs": self.responses.get(("runs", params.get("event")), [])[:1]} - return {"jobs": self.responses[path]} + if path.endswith("/jobs"): + return {"jobs": self.responses.get(path, [])} + # A run read by its id, by default a pull request with no jobs listed. + return self.responses.get(path, dict(PR, id=int(path.rsplit("/", 1)[-1]))) def jobs_path(run_id): @@ -257,6 +260,29 @@ class MeasureTest(unittest.TestCase): total, _ = admission.measure(api, "o/r", "build_all.yml", 2, NOON) self.assertEqual(total, 0) + def test_counts_its_own_run_when_no_status_list_has_it(self): + # The run moves from pending to in_progress as its last gate leaves the + # line, and is in neither list while that gate checks. + jobs = [gate("x86_64"), build("x86_64", "queued"), gate("arm64", "in_progress", None)] + api = FakeApi({jobs_path(2): jobs}) + self.assertEqual(admission.measure(api, "o/r", "build_all.yml", 2, NOON)[0], 1) + + def test_counts_a_recent_run_that_no_status_list_has(self): + api = FakeApi({("runs", "recent"): [PR], jobs_path(2): [gate("arm64"), build("arm64")]}) + self.assertEqual(admission.measure(api, "o/r", "build_all.yml", 99, NOON)[0], 1) + + def test_lists_recent_runs_from_a_day_before(self): + api = FakeApi({}) + admission.measure(api, "o/r", "build_all.yml", 99, NOON) + self.assertIn(">=2026-10-09T12:00:00Z", [params.get("created") for _, params in api.calls]) + + def test_finished_recent_runs_are_not_read(self): + done = dict(PUSH, status="completed") + api = FakeApi({("runs", "recent"): [done]}) + total, _ = admission.measure(api, "o/r", "build_all.yml", 99, NOON) + self.assertEqual(total, 0) + self.assertNotIn(jobs_path(1), [path for path, _ in api.calls]) + def test_no_nightly_lookup_outside_its_window(self): api = FakeApi({}) admission.measure(api, "o/r", "build_all.yml", 99, NOON)