mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
885c1486bb
* test(ci): single-retry policy for enumerated contention-flaky files * fix: satisfy fallow * test(ci): read failures through a lane reporter so timeouts stay distinguishable * test(ci): cover the lane reporter and drop its duplicated boilerplate * chore(fallow): own the retry lane's tool-loaded export seams * test(ci): block retries on non-test failures and classify timeouts structurally * test(ci): decide retry eligibility from runner metadata and route gate verdicts through blockers * refactor(ci): name the retry policy's rules in code instead of comments * test(ci): mark runner-aborted timeouts inside the runner instead of inferring them * test(ci): make timeout provenance a per-run secret, not a writable flag Cover direct task.meta mutation in the real child-Vitest fixture gate. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(ci): retry the failed files in the first run's project and coverage modes Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: drop deleted repo-health file from the retry list after #1480 Rebase onto main post-#1480: the SkillGym/repo-health descope deleted scripts/repo-health/run.test.ts, whose CONTENTION_RETRY_FILES entry would now fail this PR's own missing-file check, and inlined the slow-test budgets into the reporter, resolving the budgets-module import. Envelope comments now point at scripts/lib/lane-envelope.ts instead of the closed #1430. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep --------- Co-authored-by: Michał Pierzchała <thymikee@gmail.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
138 lines
5.3 KiB
TypeScript
138 lines
5.3 KiB
TypeScript
// CI entrypoint for the contention single-retry policy (#1419).
|
|
//
|
|
// node --experimental-strip-types scripts/lib/contention-retry-run.ts --coverage
|
|
//
|
|
// Runs Vitest once; if the only failures are timeout-shaped and live in the
|
|
// enumerated retry list (scripts/lib/contention-retry.ts), reruns exactly those
|
|
// files once. Every retried file is named in the job summary, and the run writes
|
|
// the shared lane envelope (scripts/lib/lane-envelope.ts) so retry counts stay observable.
|
|
|
|
import crypto from 'node:crypto';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { runCmdStreaming, runCmdSync } from '../../src/utils/exec.ts';
|
|
import { parseScriptArgs } from './cli-args.ts';
|
|
import { firstRunArgs, retryRunArgs, type RunModes } from './contention-retry-args.ts';
|
|
import { runWithContentionRetry, type TestRun } from './contention-retry-lane.ts';
|
|
import { FAILURE_FILE_ENV } from './contention-retry-reporter.ts';
|
|
import { RUNNER_TIMEOUT_TOKEN_ENV } from './runner-timeout-meta.ts';
|
|
import { processBlockers } from './contention-retry-blockers.ts';
|
|
import { parseFailureReport, type RunBlocker, type TestFailure } from './contention-retry.ts';
|
|
|
|
const USAGE = `Usage: node --experimental-strip-types scripts/lib/contention-retry-run.ts [options]
|
|
|
|
--coverage Run the full suite under coverage (the CI Coverage job)
|
|
--project <a,b> Vitest projects for the first run (default: all)
|
|
--report <file> Failure report path (default: .tmp/contention-retry/failures.json)
|
|
--envelope <file> Lane envelope path (default: .tmp/contention-retry/lane-envelope.json)
|
|
--summary <file> Markdown summary sink (default: $GITHUB_STEP_SUMMARY)
|
|
`;
|
|
|
|
const repoRoot = path.resolve(import.meta.dirname, '../..');
|
|
const DEFAULT_REPORT = '.tmp/contention-retry/failures.json';
|
|
const DEFAULT_ENVELOPE = '.tmp/contention-retry/lane-envelope.json';
|
|
|
|
const args = parseScriptArgs(process.argv.slice(2), USAGE, {
|
|
coverage: { type: 'boolean', default: false },
|
|
project: { type: 'string' },
|
|
report: { type: 'string', default: DEFAULT_REPORT },
|
|
envelope: { type: 'string', default: DEFAULT_ENVELOPE },
|
|
summary: { type: 'string' },
|
|
});
|
|
|
|
const reportPath = path.resolve(repoRoot, args.report);
|
|
const envelopePath = path.resolve(repoRoot, args.envelope);
|
|
const summaryPath = args.summary ?? process.env.GITHUB_STEP_SUMMARY;
|
|
|
|
/** The capability the runner needs to mark a timeout; test modules never see it. */
|
|
const TIMEOUT_TOKEN = crypto.randomUUID();
|
|
|
|
async function runVitest(extra: string[], report: string): Promise<TestRun> {
|
|
fs.mkdirSync(path.dirname(report), { recursive: true });
|
|
fs.rmSync(report, { force: true });
|
|
let output = '';
|
|
const capture = (chunk: string, write: (text: string) => void): void => {
|
|
output += chunk;
|
|
write(chunk);
|
|
};
|
|
const result = await runCmdStreaming('pnpm', ['exec', 'vitest', 'run', ...extra], {
|
|
cwd: repoRoot,
|
|
env: { ...process.env, [FAILURE_FILE_ENV]: report, [RUNNER_TIMEOUT_TOKEN_ENV]: TIMEOUT_TOKEN },
|
|
allowFailure: true,
|
|
onStdoutChunk: (chunk) => capture(chunk, (text) => process.stdout.write(text)),
|
|
onStderrChunk: (chunk) => capture(chunk, (text) => process.stderr.write(text)),
|
|
});
|
|
const reported = readReport(report);
|
|
const ok = result.exitCode === 0;
|
|
return {
|
|
ok,
|
|
failures: reported.failures,
|
|
blockers: [
|
|
...reported.blockers,
|
|
...processBlockers({ ok, output, failureCount: reported.failures.length }),
|
|
],
|
|
};
|
|
}
|
|
|
|
function readReport(report: string): {
|
|
failures: readonly TestFailure[];
|
|
blockers: readonly RunBlocker[];
|
|
} {
|
|
if (!fs.existsSync(report)) return { failures: [], blockers: [] };
|
|
return parseFailureReport(JSON.parse(fs.readFileSync(report, 'utf8')), repoRoot);
|
|
}
|
|
|
|
function vitestVersion(): string {
|
|
const manifest: unknown = JSON.parse(
|
|
fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'),
|
|
);
|
|
const version = (manifest as { devDependencies?: Record<string, string> }).devDependencies
|
|
?.vitest;
|
|
return version ?? 'unknown';
|
|
}
|
|
|
|
function configHash(): string {
|
|
const hash = crypto.createHash('sha256');
|
|
for (const file of [
|
|
'vitest.config.ts',
|
|
'scripts/lib/contention-retry.ts',
|
|
'scripts/lib/contention-retry-args.ts',
|
|
'scripts/lib/contention-retry-reporter.ts',
|
|
'scripts/lib/contention-retry-blockers.ts',
|
|
'scripts/lib/run-blocker-bus.ts',
|
|
'scripts/vitest-runner-timeout-setup.ts',
|
|
]) {
|
|
hash.update(fs.readFileSync(path.join(repoRoot, file)));
|
|
}
|
|
return `sha256:${hash.digest('hex').slice(0, 16)}`;
|
|
}
|
|
|
|
const modes: RunModes = {
|
|
projects: (args.project ?? '')
|
|
.split(',')
|
|
.map((entry) => entry.trim())
|
|
.filter(Boolean),
|
|
coverage: args.coverage,
|
|
};
|
|
|
|
const startedAtMs = Date.now();
|
|
const result = await runWithContentionRetry({
|
|
runAll: () => runVitest(firstRunArgs(modes), reportPath),
|
|
runFiles: (files) =>
|
|
runVitest(retryRunArgs(modes, files), reportPath.replace(/\.json$/, '.retry.json')),
|
|
commit: runCmdSync('git', ['rev-parse', 'HEAD'], { cwd: repoRoot }).stdout.trim(),
|
|
configHash: configHash(),
|
|
vitestVersion: vitestVersion(),
|
|
startedAtMs,
|
|
});
|
|
|
|
fs.mkdirSync(path.dirname(envelopePath), { recursive: true });
|
|
fs.writeFileSync(envelopePath, `${JSON.stringify(result.envelope, null, 2)}\n`);
|
|
|
|
if (result.summary) {
|
|
process.stdout.write(`\n${result.summary}`);
|
|
if (summaryPath) fs.appendFileSync(summaryPath, result.summary);
|
|
}
|
|
|
|
process.exitCode = result.ok ? 0 : 1;
|