Files
orca/config/scripts/ci-unit-selection.mjs
T
OrcaWinandOrcaWin 006bbb28e7 ci: run PR unit shards on free hosted ARM as 12 shards (#34906)
* ci: run PR unit shards on free hosted ARM as 12 shards

Enterprise's 1,000-job limit makes more, smaller shards free. A 12-shard
cold run on ubuntu-24.04-arm measured 361s median / 412s max per shard,
against ~390s / ~441s for five warm Blacksmith 8-vCPU shards.

The Blacksmith-store module cache producer becomes dispatch-only since
hosted shards cannot read it.

* TEMP: force 12 shards on this PR (repo var is still 5); drop before merge

* test: size the full-inventory routing check from FULL_SHARD_COUNT

* Revert TEMP 12-shard override; the repository variable is now 12

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-28 21:10:45 -07:00

101 lines
3.4 KiB
JavaScript

import { unitConsumers } from './ci-unit-dependency-graph.mjs'
import { balanceFiles } from './ci-shard-assignment.mjs'
import { UNIT_SETUP_FILES } from './ci-unit-files.mjs'
const RUNTIME_ENTRY_POINTS = new Set([
'config/vitest.config.ts',
'config/scripts/run-vitest.mjs',
...UNIT_SETUP_FILES
])
function isUnitRuntimeEntryPoint(file) {
return (
RUNTIME_ENTRY_POINTS.has(file) ||
(/^config\/scripts\/vitest[^/]*\.(?:ts|mjs)$/.test(file) && !file.includes('.test.'))
)
}
export function selectUnitFiles(files, changed, graphInput) {
const full = (reason) => ({ files, reason, full: true })
if (!changed.length) {
return full('Missing changed-path evidence')
}
if (changed.some((file) => !file.startsWith('src/'))) {
return full('Global, deleted, renamed or unknown input')
}
const graph = typeof graphInput === 'function' ? graphInput() : graphInput
if (changed.some((file) => !graph.files.has(file))) {
return full('Global, deleted, renamed or unknown input')
}
for (const file of changed) {
const consumers = unitConsumers([file], graph.reverse)
if ([...consumers].some(isUnitRuntimeEntryPoint)) {
return full('Shared unit runtime input')
}
if (!files.some((test) => consumers.has(test))) {
return full('No proven test coverage for changed inputs')
}
}
const affected = unitConsumers(changed, graph.reverse)
for (const file of graph.opaqueConsumers ?? unitConsumers(graph.opaque, graph.reverse)) {
affected.add(file)
}
const selected = files.filter((file) => affected.has(file))
if (!selected.length) {
return full('No proven test coverage for changed inputs')
}
return {
files: selected,
reason: 'Transitive imports plus indirect-input consumers',
full: false,
checkedInputOwners: graph.checkedInputOwners ?? []
}
}
// Job slots, not cores, drive queueing: eight shards consumed 68% of daily slots. Five budget ~8.8min/shard versus 10.5min p95 queue.
// Full PR/daily callers use five; selected drafts may use fewer.
export const FULL_SHARD_COUNT = 12
export function planUnitSelection({
files,
changed,
graph,
timings,
event,
mode = 'shadow',
fullShardCount = FULL_SHARD_COUNT
}) {
const candidate = selectUnitFiles(files, changed, graph)
const selected = mode === 'selected' && event?.pull_request?.draft === true && !candidate.full
const executionFiles = selected ? candidate.files : files
const totalMs = balanceFiles(executionFiles, 1, timings).shards[0].durationMs
const count = selected
? Math.max(1, Math.min(FULL_SHARD_COUNT, Math.ceil(totalMs / 900_000), executionFiles.length))
: fullShardCount
return {
version: 1,
mode: selected ? 'selected' : 'shadow',
selectionAvailable: !candidate.full,
reason: candidate.reason,
checkedInputOwners: candidate.checkedInputOwners ?? [],
files,
candidateFiles: candidate.files,
executionFiles,
shards: Array.from({ length: count }, (_, index) => ({ index: index + 1, count }))
}
}
export function auditUnitSelection(plan, results) {
const candidates = new Set(plan.candidateFiles)
const omittedFailures = Object.entries(results)
.filter(([file, state]) => state === 'failed' && !candidates.has(file))
.map(([file]) => file)
return {
mode: plan.mode,
discovered: plan.files.length,
candidate: candidates.size,
executed: Object.keys(results).length,
omittedFailures
}
}