From 24cefcba62c9a901390d4d5b46d797bd3879fa69 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Sun, 11 Oct 2026 00:40:35 +0800 Subject: [PATCH] Count Runs That Move Between Status Lists A run is listed as pending while any of its jobs waits in a concurrency group, so it moves to in_progress the moment its last gate leaves the line. A check made in those seconds read the in_progress list before the move and the pending list after it, missed the run, and let in a second arch the limit had no room for. Runs created in the last day are now listed by creation time as well, and the run doing the check is read directly. Co-authored-by: raistlin7447 --- scripts/ci_macos_admission.py | 11 +++++++- scripts/tests/test_ci_macos_admission.py | 32 +++++++++++++++++++++--- 2 files changed, 39 insertions(+), 4 deletions(-) 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)