From fd7e8fd5a90afdef7b79cc7c1d04a4072aa54dcd Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Sun, 11 Oct 2026 23:16:26 +0800 Subject: [PATCH] Log API Calls and the Rate Limit Left on Each macOS Admission Check The calls per check grow with the number of active runs, and the token's hourly limit is shared by the whole repository, so each check now logs how many calls it made and what the last response says is left. --- scripts/ci_macos_admission.py | 36 +++++++++++++++++++++--- scripts/tests/test_ci_macos_admission.py | 36 ++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/scripts/ci_macos_admission.py b/scripts/ci_macos_admission.py index 71876c505c..0b5089ac2b 100644 --- a/scripts/ci_macos_admission.py +++ b/scripts/ci_macos_admission.py @@ -131,6 +131,10 @@ class Api: def __init__(self, token, url="https://api.github.com"): self.token = token self.url = url.rstrip("/") + # The token's hourly limit is shared by every workflow in the repository, + # so what the last response says is left shows their use too. + self.calls = 0 + self.remaining = self.limit = "?" def get(self, path, **params): query = urllib.parse.urlencode(params) @@ -139,8 +143,22 @@ class Api: "Authorization": f"Bearer {self.token}", "X-GitHub-Api-Version": "2022-11-28", }) - with urllib.request.urlopen(request, timeout=30) as response: - return json.load(response) + self.calls += 1 + try: + with urllib.request.urlopen(request, timeout=30) as response: + self.note_rate_limit(response.headers) + return json.load(response) + except urllib.error.HTTPError as error: + # A rate-limited request fails, and its headers say so. + self.note_rate_limit(error.headers) + raise + + def note_rate_limit(self, headers): + self.remaining = headers.get("X-RateLimit-Remaining", "?") + self.limit = headers.get("X-RateLimit-Limit", "?") + + def usage(self, calls_before): + return f" API calls this check: {self.calls - calls_before}, rate limit left: {self.remaining} of {self.limit}" def list_runs(api, repo, workflow, **params): @@ -227,9 +245,19 @@ def main(): api = Api(os.environ["GH_TOKEN"], os.environ.get("GITHUB_API_URL", "https://api.github.com")) run_id = int(os.environ["GITHUB_RUN_ID"]) limit = int(os.environ.get("MACOS_RUNNER_LIMIT") or 5) + + def measure_now(): + calls_before = api.calls + try: + total, lines = measure(api, repo, workflow, run_id, datetime.datetime.now(datetime.timezone.utc), + priority=os.environ.get("PRIORITY") == "true") + except Exception: + print(api.usage(calls_before).strip()) + raise + return total, lines + [api.usage(calls_before)] + reason = wait( - lambda: measure(api, repo, workflow, run_id, datetime.datetime.now(datetime.timezone.utc), - priority=os.environ.get("PRIORITY") == "true"), + measure_now, limit, wait_minutes=int(os.environ.get("WAIT_MINUTES") or 240), poll_seconds=int(os.environ.get("POLL_SECONDS") or 180), diff --git a/scripts/tests/test_ci_macos_admission.py b/scripts/tests/test_ci_macos_admission.py index 29e80e3a25..b872545d3f 100644 --- a/scripts/tests/test_ci_macos_admission.py +++ b/scripts/tests/test_ci_macos_admission.py @@ -9,6 +9,7 @@ import os import sys import unittest import urllib.error +from unittest import mock sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) @@ -289,6 +290,41 @@ class MeasureTest(unittest.TestCase): self.assertFalse(any("event" in params for _, params in api.calls)) +class ApiTest(unittest.TestCase): + class Response: + def __init__(self, headers): + self.headers = headers + + def __enter__(self): + return self + + def __exit__(self, *_): + return False + + def read(self): + return b"{}" + + def test_reports_calls_and_the_rate_limit_left(self): + api = admission.Api("token") + responses = [self.Response({}), + self.Response({"X-RateLimit-Remaining": "812", "X-RateLimit-Limit": "1000"})] + with mock.patch.object(admission.urllib.request, "urlopen", side_effect=responses): + api.get("first") + calls_before = api.calls + api.get("second") + self.assertEqual(api.usage(0), " API calls this check: 2, rate limit left: 812 of 1000") + self.assertEqual(api.usage(calls_before), " API calls this check: 1, rate limit left: 812 of 1000") + + def test_reads_the_rate_limit_from_a_failed_request(self): + api = admission.Api("token") + headers = {"X-RateLimit-Remaining": "0", "X-RateLimit-Limit": "1000"} + error = urllib.error.HTTPError("url", 403, "Forbidden", headers, None) + with mock.patch.object(admission.urllib.request, "urlopen", side_effect=error), \ + self.assertRaises(urllib.error.HTTPError): + api.get("runs") + self.assertEqual(api.usage(0), " API calls this check: 1, rate limit left: 0 of 1000") + + class WaitTest(unittest.TestCase): def run_wait(self, results, limit=5, wait_minutes=60, poll_seconds=120): results = iter(results)