From ea07f6f250d6c75f701bb90c8ab5d031b5ecfee8 Mon Sep 17 00:00:00 2001 From: john Date: Sun, 26 Jul 2026 15:19:30 +0800 Subject: [PATCH] fix: isolate artifact rebuild from release suites --- release-gate/coverage.mjs | 3 +++ release-gate/runner.mjs | 26 +++++++++++++++++++++++++- release-gate/runner.test.mjs | 23 ++++++++++++++++++++++- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/release-gate/coverage.mjs b/release-gate/coverage.mjs index 02082f9..dd59a1a 100644 --- a/release-gate/coverage.mjs +++ b/release-gate/coverage.mjs @@ -2,6 +2,9 @@ export const AUTOMATION_SUITES = Object.freeze([ { id: 'runtime-reproducibility', mode: 'upgrade', + // Rebuilds .runtime/portal in place. Keep this suite isolated from all + // suites that boot or inspect the shared candidate runtime. + exclusive: true, scenarios: ['REL-03'], cases: { 'REL-03': ['candidate plus two clean rebuilds produce the same sorted runtime tree SHA256'], diff --git a/release-gate/runner.mjs b/release-gate/runner.mjs index 03d98c7..130a72c 100644 --- a/release-gate/runner.mjs +++ b/release-gate/runner.mjs @@ -72,6 +72,30 @@ export async function runWithConcurrency(items, concurrency, worker) { return results; } +// Some suites intentionally mutate the shared candidate artifact. Run those +// suites before the parallel batch so they cannot race suites that boot the +// same runtime while it is being rebuilt. +export async function runSuitesWithConcurrency(items, concurrency, worker) { + const results = new Array(items.length); + const parallel = []; + for (let index = 0; index < items.length; index += 1) { + if (items[index].exclusive) { + results[index] = await worker(items[index], index); + } else { + parallel.push({ item: items[index], index }); + } + } + const parallelResults = await runWithConcurrency( + parallel, + concurrency, + ({ item, index }) => worker(item, index), + ); + for (let index = 0; index < parallel.length; index += 1) { + results[parallel[index].index] = parallelResults[index]; + } + return results; +} + export async function runCommand(command, args, { cwd = ROOT, timeoutMs = 15 * 60 * 1000, @@ -220,7 +244,7 @@ export async function executeReleaseGate(options) { ); const suites = AUTOMATION_SUITES.filter((candidate) => suiteIsInMode(candidate, options.mode)); - const executions = await runWithConcurrency( + const executions = await runSuitesWithConcurrency( suites, options.suiteConcurrency, (suite) => runSuite(suite, outputDir, options.timeoutMs), diff --git a/release-gate/runner.test.mjs b/release-gate/runner.test.mjs index 6b60cce..50bdbe3 100644 --- a/release-gate/runner.test.mjs +++ b/release-gate/runner.test.mjs @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { parseRunnerArgs, runWithConcurrency } from './runner.mjs'; +import { parseRunnerArgs, runSuitesWithConcurrency, runWithConcurrency } from './runner.mjs'; test('runner accepts a bounded suite concurrency and rejects unsafe values', () => { assert.equal( @@ -31,3 +31,24 @@ test('runWithConcurrency preserves result order and never exceeds its worker bud assert.deepEqual(results, ['result-0', 'result-1', 'result-2', 'result-3']); assert.equal(peak, 2); }); + +test('exclusive suites finish before parallel suites start', async () => { + const events = []; + const results = await runSuitesWithConcurrency( + [ + { id: 'mutating', exclusive: true }, + { id: 'parallel-a' }, + { id: 'parallel-b' }, + ], + 2, + async (suite) => { + events.push(`start:${suite.id}`); + await new Promise((resolve) => setTimeout(resolve, 5)); + events.push(`end:${suite.id}`); + return suite.id; + }, + ); + assert.deepEqual(results, ['mutating', 'parallel-a', 'parallel-b']); + assert.ok(events.indexOf('end:mutating') < events.indexOf('start:parallel-a')); + assert.ok(events.indexOf('end:mutating') < events.indexOf('start:parallel-b')); +});