mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 16:02:56 +00:00
* 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
101 lines
3.9 KiB
JavaScript
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)
|
|
}
|
|
}
|