GitNexus/gitnexus/scripts/shard-timings-reporter.ts
Gergő Magyar 13d06d41c4
feat(ci): rebalance test shards from recorded timings (#3748)
* feat(ci): rebalance test shards from recorded timings

* fix(ci): preserve artifact retries for shard timings

Find shard-plan generation by its stable step ID and clarify the native runner cohort count (#3748).

Note: pre-existing reference-oracle parser type failures in two test_oracle_assets cases remain outside this change.

* fix(ci): keep retry timing snapshots immutable

Require the existing snapshot on reruns so a failed download cannot refresh history and change shard assignments. Cover the workflow contract and document recovery when the artifact is unavailable.

* docs(ci): document shard timing profile selection

* fix(ci): balance cold shard estimates consistently

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
2026-10-28 11:58:29 +00:00

57 lines
2.3 KiB
TypeScript

import path from 'node:path';
import type { TestModule, TestRunEndReason, Vitest } from 'vitest/node';
import { TimingsReporter } from 'vitest-shard-balancer';
import { ALL_CROSS_PLATFORM } from './cross-platform-tests.js';
import { shardTimingKey, shardTimingsFile } from './shard-timings.js';
export default class ShardTimingsReporter extends TimingsReporter {
private context!: Vitest;
constructor() {
// The package's whole-config check cannot describe our native subset.
// Check the exact cohort below, before excluding skipped modules from learning.
super({ timingsFile: shardTimingsFile(), requireAllFiles: false });
}
override onInit(context: Vitest): void {
this.context = context;
super.onInit(context);
}
override async onTestRunEnd(
modules: readonly TestModule[],
errors: readonly unknown[] = [],
reason: TestRunEndReason = 'interrupted',
): Promise<void> {
if (!this.context.config.mergeReports) return;
if (errors.length || reason !== 'passed') {
throw new Error('Refusing to learn shard timings from an unsuccessful test run');
}
const root = this.context.config.root;
const native = (process.env.GITNEXUS_SHARD_PROFILE ?? 'coverage') !== 'coverage';
const nativeFiles = new Set<string>(ALL_CROSS_PLATFORM);
const specs = (await this.context.globTestSpecifications()).filter(
(spec) =>
!native || nativeFiles.has(path.relative(root, spec.moduleId).split(path.sep).join('/')),
);
const expected = new Set(
specs.map((spec) => shardTimingKey(root, spec.moduleId, spec.project.name)),
);
const actual = new Set<string>();
for (const module of modules) {
const key = shardTimingKey(root, module.moduleId, module.project.name);
if (actual.has(key) || !expected.has(key)) {
throw new Error(`Duplicate or unexpected test module in shard reports: ${key}`);
}
actual.add(key);
}
const missing = [...expected].filter((key) => !actual.has(key));
if (!actual.size || missing.length) {
throw new Error(
`Incomplete shard reports; missing ${missing.length} test modules: ${missing.join(', ')}`,
);
}
// A platform-specific skip must not turn a previously expensive file into zero work.
await super.onTestRunEnd(modules.filter((module) => module.state() === 'passed'));
}
}