From 8b9f8787490b5ac08d784e271f073a05e5237397 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Tue, 27 Oct 2026 01:39:30 -0700 Subject: [PATCH] ci: split busy hours in the runner demand report instead of failing (#34324) The runs API serves at most 1000 results per query. On 2026-10-27 one hour crossed that and the daily report threw. Halve any oversized interval until each query fits. Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> --- config/scripts/ci-runner-demand.mjs | 37 +++++++++++++++++++---------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/config/scripts/ci-runner-demand.mjs b/config/scripts/ci-runner-demand.mjs index 0a60d1f40f6..a5b101d0e95 100644 --- a/config/scripts/ci-runner-demand.mjs +++ b/config/scripts/ci-runner-demand.mjs @@ -20,6 +20,29 @@ async function api(path, env) { return response.json() } +// The runs API serves at most 1000 results per query, so busy intervals are halved until each fits. +async function collectRuns(sinceMs, untilMs, request) { + const range = `${new Date(sinceMs).toISOString()}..${new Date(untilMs).toISOString()}` + const path = `actions/runs?per_page=100&created=${encodeURIComponent(range)}` + const first = await request(path) + if (first.total_count > 1000) { + const seconds = (untilMs - sinceMs) / 1000 + 1 + if (seconds < 2) { + throw new Error('Run inventory exceeds API limit within one second') + } + const middle = sinceMs + Math.floor(seconds / 2) * 1000 + return [ + ...(await collectRuns(sinceMs, middle - 1000, request)), + ...(await collectRuns(middle, untilMs, request)) + ] + } + const runs = [...first.workflow_runs] + for (let page = 2; page <= Math.ceil(first.total_count / 100); page++) { + runs.push(...(await request(`${path}&page=${page}`)).workflow_runs) + } + return runs +} + export async function collectRunnerDemand(env = process.env, request = (path) => api(path, env)) { const end = new Date(env.CI_METRICS_END ?? Date.now()) end.setUTCMinutes(0, 0, 0) @@ -28,18 +51,8 @@ export async function collectRunnerDemand(env = process.env, request = (path) => } const runs = [] for (let hour = 0; hour < 24; hour++) { - const until = new Date(end.getTime() - hour * 3_600_000 - 1000) - const since = new Date(end.getTime() - (hour + 1) * 3_600_000) - const range = `${since.toISOString()}..${until.toISOString()}` - const path = `actions/runs?per_page=100&created=${encodeURIComponent(range)}` - const first = await request(path) - if (first.total_count > 1000) { - throw new Error('Hourly run inventory exceeds API limit; split the interval') - } - runs.push(...first.workflow_runs) - for (let page = 2; page <= Math.ceil(first.total_count / 100); page++) { - runs.push(...(await request(`${path}&page=${page}`)).workflow_runs) - } + const until = end.getTime() - hour * 3_600_000 - 1000 + runs.push(...(await collectRuns(until - 3_599_000, until, request))) } const samples = sampleWorkflowRuns(runs) // Bound API concurrency and preserve every selected observation, including zero-job runs.