Files
orca/config/scripts/ci-runner-demand.mjs
T
Neil 9179b93ebf ci: reduce repeated runner work and validate affected-test selection (#23540)
* ci: stage heavy checks and measure affected-test selection

* fix(ci): exercise the real Git boundary in unit selection planning

* Harden review cancellation and CI demand reporting
2026-09-27 23:25:20 -07:00

101 lines
3.9 KiB
JavaScript

import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs'
import { pathToFileURL } from 'node:url'
import { runnerDemand, sampleWorkflowRuns } from './ci-runner-metrics.mjs'
async function api(path, env) {
const response = await fetch(
`${env.GITHUB_API_URL ?? 'https://api.github.com'}/repos/${env.GITHUB_REPOSITORY}/${path}`,
{
headers: {
Authorization: `Bearer ${env.GH_TOKEN}`,
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28'
},
signal: AbortSignal.timeout(30_000)
}
)
if (!response.ok) {
throw new Error(`Actions API: ${response.status}`)
}
return response.json()
}
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)
if (!Number.isFinite(end.getTime())) {
throw new Error('Invalid window end')
}
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 samples = sampleWorkflowRuns(runs)
// Bound API concurrency and preserve every selected observation, including zero-job runs.
for (let index = 0; index < samples.length; index += 4) {
await Promise.all(
samples.slice(index, index + 4).map(async (sample) => {
const path = `actions/runs/${sample.run.id}/jobs?per_page=100`
const first = await request(path)
sample.jobs = first.jobs
for (let page = 2; page <= Math.ceil(first.total_count / 100); page++) {
sample.jobs.push(...(await request(`${path}&page=${page}`)).jobs)
}
})
)
}
const report = {
start: new Date(end.getTime() - 86_400_000).toISOString(),
end: end.toISOString(),
method:
'Stratified by workflow/conclusion, six sampled runs per stratum; latest attempts only; incomplete jobs excluded',
...runnerDemand(runs, samples)
}
return { report, runs, samples }
}
export function demandMarkdown(report) {
return [
`## CI demand: ${report.start} to ${report.end}`,
'',
`Estimated full job duration for runs created in this window (not window-clipped occupancy); ${report.sampledRuns}/${report.populationRuns} runs sampled. ${report.method}.`,
'',
'| Workflow | Runs | Runner hours | Cancelled-run hours | Minutes/completed PR run |',
'| --- | ---: | ---: | ---: | ---: |',
...report.workflows.map(
(row) =>
`| ${row.workflow} | ${row.runs} | ${(row.runnerMinutes / 60).toFixed(1)} | ${(row.cancelledRunnerMinutes / 60).toFixed(1)} | ${row.runnerMinutesPerCompletedPrRun?.toFixed(1) ?? '—'} |`
),
'',
'| Runner labels | Hours | Queue/provisioning p95 minutes |',
'| --- | ---: | ---: |',
...report.pools.map(
(pool) =>
`| ${pool.label} | ${(pool.runnerMinutes / 60).toFixed(1)} | ${pool.queueP95Minutes?.toFixed(1) ?? '—'} |`
),
''
].join('\n')
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const evidence = await collectRunnerDemand()
mkdirSync('ci-demand', { recursive: true })
writeFileSync('ci-demand/evidence.json', JSON.stringify(evidence))
const markdown = demandMarkdown(evidence.report)
writeFileSync('ci-demand/report.md', markdown)
if (process.env.GITHUB_STEP_SUMMARY) {
appendFileSync(process.env.GITHUB_STEP_SUMMARY, markdown)
}
}