fix(release-gate): skip duplicate CI work and stop live LLM blowups
Shared paths like db.mjs were pulling PAGE/DATA live agent suites into every hotfix. Keep those cases for actual page-data changes, resume passed suites on the same artifact, and fail fast on Docker/port issues instead of rerunning 100+ scenarios. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+33
-2
@@ -17,6 +17,25 @@ export const CORE_SCENARIO_IDS = Object.freeze([
|
||||
'COMP-09',
|
||||
]);
|
||||
|
||||
// Live LLM product scenarios. Dependency closure from db.mjs / shared infra
|
||||
// still selects the DATA/PAGE deterministic suites; these four-to-six cases
|
||||
// only run when page-delivery or Page Data product code actually changed.
|
||||
export const LIVE_LLM_SCENARIO_IDS = Object.freeze([
|
||||
'PAGE-01',
|
||||
'PAGE-02',
|
||||
'DATA-01',
|
||||
'DATA-02',
|
||||
'DATA-03',
|
||||
'DATA-04',
|
||||
]);
|
||||
|
||||
const LIVE_LLM_PATH_PATTERNS = Object.freeze([
|
||||
/(?:^|\/)page-data-[^/]+\.mjs$/i,
|
||||
/(?:^|\/)mindspace-public-finish-sync\.mjs$/i,
|
||||
/(?:^|\/)mindspace-page-data[^/]*\.mjs$/i,
|
||||
/^scripts\/run-release-gate-page(?:-data)?-scenarios\.mjs$/i,
|
||||
]);
|
||||
|
||||
const CRITICAL_IMPACT_RULES = Object.freeze([
|
||||
{
|
||||
groups: ['AGENT', 'CFG', 'UI'],
|
||||
@@ -58,6 +77,7 @@ const NON_RUNTIME_PATHS = Object.freeze([
|
||||
/^\.gitea\/workflows\//i,
|
||||
/^\.runtime\//i,
|
||||
/^docs\//i,
|
||||
/^scenarios\//i,
|
||||
/^\.cursor\//i,
|
||||
/^\.codex\//i,
|
||||
/^scripts\/dev(?:-|\.|\/)/i,
|
||||
@@ -79,7 +99,7 @@ const IMPACT_RULES = Object.freeze([
|
||||
{ groups: ['SCHED'], pattern: /(?:schedule|scheduler|reminder|cron)/i },
|
||||
{ groups: ['SEARCH'], pattern: /(?:search|weather|market|news-provider)/i },
|
||||
{ groups: ['XLS'], pattern: /(?:excel|xlsx|spreadsheet)/i },
|
||||
{ groups: ['IMGPG'], pattern: /(?:image|thumbnail|cover|imgproxy)/i },
|
||||
{ groups: ['IMGPG'], pattern: /(?:image-to-page|image-generation|imgproxy|thumbnails?|user-image-url|plaza-cover)/i },
|
||||
{ groups: ['FILE'], pattern: /(?:file|attachment|upload|document|pdf|docx|csv)/i },
|
||||
{ groups: ['MS'], pattern: /mindspace/i },
|
||||
{ groups: ['PAGE'], pattern: /(?:public-(?:page|finish)|published-page|publication|page-delivery|mindspace-public)/i },
|
||||
@@ -114,6 +134,13 @@ function matchesAny(patterns, relativePath) {
|
||||
return patterns.some((pattern) => pattern.test(relativePath));
|
||||
}
|
||||
|
||||
export function pathTriggersLiveLlmScenarios(changedPaths) {
|
||||
return normalizePaths(changedPaths).some((relativePath) => (
|
||||
!matchesAny(NON_RUNTIME_PATHS, relativePath)
|
||||
&& LIVE_LLM_PATH_PATTERNS.some((pattern) => pattern.test(relativePath))
|
||||
));
|
||||
}
|
||||
|
||||
function closeGroupDependencies(initialGroups) {
|
||||
const groups = new Set(initialGroups);
|
||||
const pending = [...groups];
|
||||
@@ -172,10 +199,13 @@ export function selectImpactScenarios({
|
||||
}
|
||||
const impactGroups = closeGroupDependencies(directGroups);
|
||||
const strategy = impactGroups.length > 0 ? 'impact' : 'core';
|
||||
const includeLiveLlm = pathTriggersLiveLlmScenarios(normalizedPaths);
|
||||
const selected = new Set(CORE_SCENARIO_IDS);
|
||||
|
||||
for (const scenario of catalog) {
|
||||
if (impactGroups.includes(scenario.group)) selected.add(scenario.id);
|
||||
if (!impactGroups.includes(scenario.group)) continue;
|
||||
if (!includeLiveLlm && LIVE_LLM_SCENARIO_IDS.includes(scenario.id)) continue;
|
||||
selected.add(scenario.id);
|
||||
}
|
||||
|
||||
const selectedIds = catalog
|
||||
@@ -193,5 +223,6 @@ export function selectImpactScenarios({
|
||||
full_gate_reasons: [],
|
||||
selected_ids: selectedIds,
|
||||
selected_total: selectedIds.length,
|
||||
live_llm_selected: includeLiveLlm,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -54,6 +54,10 @@ test('critical paths expand mapped domains and unmapped paths block release', as
|
||||
critical.impact_groups,
|
||||
['AGENT', 'AUTH', 'CFG', 'CHAT', 'DATA', 'FILE', 'MS', 'PAGE'],
|
||||
);
|
||||
assert.equal(critical.live_llm_selected, false);
|
||||
assert.equal(critical.selected_ids.includes('DATA-01'), false);
|
||||
assert.equal(critical.selected_ids.includes('PAGE-01'), false);
|
||||
assert.equal(critical.selected_ids.includes('DATA-06'), true);
|
||||
assert.deepEqual(critical.full_gate_reasons, []);
|
||||
|
||||
assert.throws(
|
||||
@@ -73,6 +77,49 @@ test('critical paths expand mapped domains and unmapped paths block release', as
|
||||
);
|
||||
});
|
||||
|
||||
test('chat image turn-scope does not expand the image-to-page live domain', async () => {
|
||||
const catalog = await loadScenarioCatalog();
|
||||
const selection = selectImpactScenarios({
|
||||
catalog,
|
||||
changedPaths: ['chat-image-turn-scope.mjs', 'wechat-mp.mjs'],
|
||||
});
|
||||
assert.equal(selection.impact_groups.includes('IMGPG'), false);
|
||||
assert.equal(selection.selected_ids.includes('PAGE-01'), false);
|
||||
assert.equal(selection.selected_ids.includes('DATA-01'), false);
|
||||
assert.equal(selection.selected_ids.includes('WX-01'), true);
|
||||
assert.equal(selection.selected_ids.includes('CHAT-01'), true);
|
||||
});
|
||||
|
||||
test('Page Data product code still selects live LLM DATA scenarios', async () => {
|
||||
const catalog = await loadScenarioCatalog();
|
||||
const selection = selectImpactScenarios({
|
||||
catalog,
|
||||
changedPaths: ['page-data-routes.mjs'],
|
||||
});
|
||||
assert.equal(selection.live_llm_selected, true);
|
||||
assert.equal(selection.selected_ids.includes('DATA-01'), true);
|
||||
assert.equal(selection.selected_ids.includes('DATA-06'), true);
|
||||
});
|
||||
|
||||
test('image-generation files still map to IMGPG', async () => {
|
||||
const catalog = await loadScenarioCatalog();
|
||||
const selection = selectImpactScenarios({
|
||||
catalog,
|
||||
changedPaths: ['mindspace-image-generation.mjs'],
|
||||
});
|
||||
assert.equal(selection.impact_groups.includes('IMGPG'), true);
|
||||
});
|
||||
|
||||
test('scenario fixtures are non-runtime and do not block mapping', async () => {
|
||||
const catalog = await loadScenarioCatalog();
|
||||
const selection = selectImpactScenarios({
|
||||
catalog,
|
||||
changedPaths: ['scenarios/ai-usage-survey.json', 'docs/branch-disposition.md'],
|
||||
});
|
||||
assert.equal(selection.strategy, 'core');
|
||||
assert.equal(selection.live_llm_selected, false);
|
||||
});
|
||||
|
||||
test('release policy changes use mapped REL and CFG domains without selecting the catalog', async () => {
|
||||
const catalog = await loadScenarioCatalog();
|
||||
const selection = selectImpactScenarios({
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import fs from 'node:fs';
|
||||
import net from 'node:net';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
export const GATE_SUITE_PORTS = Object.freeze({
|
||||
'runtime-sanitized-data-upgrade': 19085,
|
||||
'page-data-product-scenarios': 19086,
|
||||
'page-content-delivery-scenarios': 19087,
|
||||
'runtime-production-homolog-cold-start': 19081,
|
||||
});
|
||||
|
||||
const DOCKER_SOCKET_CANDIDATES = [
|
||||
process.env.DOCKER_HOST?.replace(/^unix:\/\//, ''),
|
||||
`${process.env.HOME ?? ''}/.docker/run/docker.sock`,
|
||||
'/var/run/docker.sock',
|
||||
].filter(Boolean);
|
||||
|
||||
export function dockerSocketExists() {
|
||||
return DOCKER_SOCKET_CANDIDATES.some((socketPath) => {
|
||||
try {
|
||||
return fs.existsSync(socketPath);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function assertDockerDaemonAvailable() {
|
||||
if (!dockerSocketExists()) {
|
||||
throw new Error(
|
||||
'Release gate REL-11 needs Docker; start Docker Desktop and retry. Missing docker.sock.',
|
||||
);
|
||||
}
|
||||
const result = spawnSync('docker', ['info'], {
|
||||
encoding: 'utf8',
|
||||
timeout: 15_000,
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error(
|
||||
`Release gate REL-11 needs a running Docker daemon: ${(result.stderr || result.error?.message || 'docker info failed').trim().slice(0, 500)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function assertLoopbackPortAvailable(port, host = '127.0.0.1') {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = net.createServer();
|
||||
server.once('error', (error) => {
|
||||
if (error?.code === 'EADDRINUSE') {
|
||||
reject(new Error(
|
||||
`Release gate port ${host}:${port} is already in use; stop the stale local gate process before retrying`,
|
||||
));
|
||||
return;
|
||||
}
|
||||
reject(error);
|
||||
});
|
||||
server.once('listening', () => {
|
||||
server.close((closeError) => {
|
||||
if (closeError) reject(closeError);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
server.listen(port, host);
|
||||
});
|
||||
}
|
||||
|
||||
export async function preflightImpactSuites(suites) {
|
||||
const ports = [...new Set(
|
||||
suites
|
||||
.map((suite) => GATE_SUITE_PORTS[suite.id])
|
||||
.filter((port) => Number.isInteger(port)),
|
||||
)].sort((left, right) => left - right);
|
||||
for (const port of ports) {
|
||||
await assertLoopbackPortAvailable(port);
|
||||
}
|
||||
if (suites.some((suite) => suite.id === 'runtime-linux-dependency-closure')) {
|
||||
assertDockerDaemonAvailable();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import net from 'node:net';
|
||||
import test from 'node:test';
|
||||
|
||||
import { GATE_SUITE_PORTS, assertLoopbackPortAvailable } from './preflight.mjs';
|
||||
|
||||
test('gate suites pin isolated loopback ports', () => {
|
||||
assert.equal(GATE_SUITE_PORTS['runtime-sanitized-data-upgrade'], 19085);
|
||||
assert.equal(GATE_SUITE_PORTS['page-data-product-scenarios'], 19086);
|
||||
assert.equal(GATE_SUITE_PORTS['page-content-delivery-scenarios'], 19087);
|
||||
});
|
||||
|
||||
test('assertLoopbackPortAvailable rejects an occupied port', async () => {
|
||||
const server = net.createServer();
|
||||
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||||
const { port } = server.address();
|
||||
await assert.rejects(
|
||||
() => assertLoopbackPortAvailable(port),
|
||||
/already in use/,
|
||||
);
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
await assertLoopbackPortAvailable(port);
|
||||
});
|
||||
@@ -143,6 +143,10 @@ test('production canary verifies the exact Gate artifact before any 103 prefligh
|
||||
assert.match(source, /run-release-gate-impact\.mjs/);
|
||||
assert.match(source, /MEMIND_RELEASE_BASE_COMMIT/);
|
||||
assert.match(source, /-z "\$\{DEPLOYED_SHA\}".*"\$\{DRY_RUN\}" -ne 1/);
|
||||
assert.match(source, /resolve-release-ci-status\.mjs/);
|
||||
assert.match(source, /skipping duplicate local npm test\/verify/);
|
||||
const ciSkipIndex = source.indexOf('resolve-release-ci-status.mjs');
|
||||
assert.ok(ciSkipIndex > 0 && ciSkipIndex < gateIndex, 'CI reuse must happen before Gate verification');
|
||||
});
|
||||
|
||||
test('production canary keeps stable 8081 live and switches only after verified backups and fallback', async () => {
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const REPOSITORY_CHECK_IDS = new Set(['REL-01', 'REL-02', 'REL-04']);
|
||||
|
||||
export function commandFilesForSuite(suite) {
|
||||
return (suite.command ?? [])
|
||||
.slice(1)
|
||||
.filter((arg) => typeof arg === 'string' && !arg.startsWith('-'))
|
||||
.map((arg) => arg.replaceAll('\\', '/'));
|
||||
}
|
||||
|
||||
export function suiteInvalidatedByChanges(suite, changedPaths) {
|
||||
const normalized = new Set((changedPaths ?? []).map((item) => String(item).replaceAll('\\', '/')));
|
||||
if (normalized.has('release-gate/coverage.mjs')) return true;
|
||||
if ([...normalized].some((relativePath) => relativePath.startsWith('scripts/run-release-gate-'))) {
|
||||
return suite.command?.some((arg) => String(arg).includes('run-release-gate-')) ?? false;
|
||||
}
|
||||
return commandFilesForSuite(suite).some((filePath) => normalized.has(filePath));
|
||||
}
|
||||
|
||||
export function previousScenarioMap(report) {
|
||||
return new Map((report?.scenarios ?? []).map((scenario) => [scenario.id, scenario]));
|
||||
}
|
||||
|
||||
export function shouldRerunImpactSuite({
|
||||
suite,
|
||||
selectedIds,
|
||||
previousReport,
|
||||
artifactSha256,
|
||||
commitSha,
|
||||
changedPathsSincePrevious = [],
|
||||
}) {
|
||||
const selectedInSuite = suite.scenarios.filter((scenarioId) => selectedIds.has(scenarioId));
|
||||
if (selectedInSuite.length === 0) return false;
|
||||
if (!previousReport || previousReport.artifact_sha256 !== artifactSha256) return true;
|
||||
if (suiteInvalidatedByChanges(suite, changedPathsSincePrevious)) return true;
|
||||
if (suite.mode === 'scenarios' && previousReport.commit_sha !== commitSha) return true;
|
||||
|
||||
const previous = previousScenarioMap(previousReport);
|
||||
return selectedInSuite.some((scenarioId) => previous.get(scenarioId)?.status !== 'passed');
|
||||
}
|
||||
|
||||
export function carryForwardScenario(previousScenario) {
|
||||
return {
|
||||
...previousScenario,
|
||||
evidence: [
|
||||
...(previousScenario.evidence ?? []),
|
||||
'carried_forward=true',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export async function findImpactResumeReport({
|
||||
reportRoot,
|
||||
artifactSha256,
|
||||
commitSha,
|
||||
}) {
|
||||
let entries = [];
|
||||
try {
|
||||
entries = await fs.readdir(reportRoot, { withFileTypes: true });
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') return null;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const candidates = [];
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory() || entry.name === 'local') continue;
|
||||
const reportPath = path.join(reportRoot, entry.name, 'report.json');
|
||||
try {
|
||||
const report = JSON.parse(await fs.readFile(reportPath, 'utf8'));
|
||||
if (report?.mode !== 'impact') continue;
|
||||
if (report.artifact_sha256 !== artifactSha256) continue;
|
||||
if (!Array.isArray(report.scenarios)) continue;
|
||||
candidates.push({ report, reportPath, commitSha: report.commit_sha });
|
||||
} catch {
|
||||
// ignore unreadable reports
|
||||
}
|
||||
}
|
||||
if (candidates.length === 0) return null;
|
||||
|
||||
const sameCommit = candidates.find((candidate) => candidate.commitSha === commitSha);
|
||||
if (sameCommit) return sameCommit;
|
||||
candidates.sort((left, right) => (
|
||||
Date.parse(right.report.completed_at ?? 0) - Date.parse(left.report.completed_at ?? 0)
|
||||
));
|
||||
return candidates[0];
|
||||
}
|
||||
|
||||
export { REPOSITORY_CHECK_IDS };
|
||||
@@ -0,0 +1,99 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
carryForwardScenario,
|
||||
commandFilesForSuite,
|
||||
shouldRerunImpactSuite,
|
||||
suiteInvalidatedByChanges,
|
||||
} from './resume.mjs';
|
||||
|
||||
const chatSuite = {
|
||||
id: 'chat-routing-contract',
|
||||
mode: 'deterministic',
|
||||
scenarios: ['CHAT-01', 'CHAT-02'],
|
||||
command: [process.execPath, '--test', 'chat-router.test.mjs'],
|
||||
};
|
||||
|
||||
const liveSuite = {
|
||||
id: 'page-data-product-scenarios',
|
||||
mode: 'scenarios',
|
||||
scenarios: ['DATA-01', 'DATA-02'],
|
||||
command: [process.execPath, 'scripts/run-release-gate-page-data-scenarios.mjs'],
|
||||
};
|
||||
|
||||
const previousReport = {
|
||||
mode: 'impact',
|
||||
commit_sha: 'a'.repeat(40),
|
||||
artifact_sha256: 'b'.repeat(64),
|
||||
scenarios: [
|
||||
{ id: 'CHAT-01', status: 'passed', evidence: ['suite=chat-routing-contract'] },
|
||||
{ id: 'CHAT-02', status: 'passed', evidence: ['suite=chat-routing-contract'] },
|
||||
{ id: 'DATA-01', status: 'failed', evidence: ['suite=page-data-product-scenarios'] },
|
||||
{ id: 'DATA-02', status: 'passed', evidence: ['suite=page-data-product-scenarios'] },
|
||||
],
|
||||
};
|
||||
|
||||
test('commandFilesForSuite skips node and flags', () => {
|
||||
assert.deepEqual(
|
||||
commandFilesForSuite({ command: [process.execPath, '--test', 'chat-router.test.mjs'] }),
|
||||
['chat-router.test.mjs'],
|
||||
);
|
||||
});
|
||||
|
||||
test('same commit and artifact skips passed suites and reruns failed live suites', () => {
|
||||
const selectedIds = new Set(['CHAT-01', 'CHAT-02', 'DATA-01', 'DATA-02']);
|
||||
assert.equal(shouldRerunImpactSuite({
|
||||
suite: chatSuite,
|
||||
selectedIds,
|
||||
previousReport,
|
||||
artifactSha256: previousReport.artifact_sha256,
|
||||
commitSha: previousReport.commit_sha,
|
||||
}), false);
|
||||
assert.equal(shouldRerunImpactSuite({
|
||||
suite: liveSuite,
|
||||
selectedIds,
|
||||
previousReport,
|
||||
artifactSha256: previousReport.artifact_sha256,
|
||||
commitSha: previousReport.commit_sha,
|
||||
}), true);
|
||||
});
|
||||
|
||||
test('artifact mismatch forces every selected suite to rerun', () => {
|
||||
assert.equal(shouldRerunImpactSuite({
|
||||
suite: chatSuite,
|
||||
selectedIds: new Set(['CHAT-01', 'CHAT-02']),
|
||||
previousReport,
|
||||
artifactSha256: 'c'.repeat(64),
|
||||
commitSha: previousReport.commit_sha,
|
||||
}), true);
|
||||
});
|
||||
|
||||
test('live suites rerun on a new commit even when previous cases passed', () => {
|
||||
const passedLive = {
|
||||
...previousReport,
|
||||
scenarios: [
|
||||
{ id: 'DATA-01', status: 'passed' },
|
||||
{ id: 'DATA-02', status: 'passed' },
|
||||
],
|
||||
};
|
||||
assert.equal(shouldRerunImpactSuite({
|
||||
suite: liveSuite,
|
||||
selectedIds: new Set(['DATA-01', 'DATA-02']),
|
||||
previousReport: passedLive,
|
||||
artifactSha256: passedLive.artifact_sha256,
|
||||
commitSha: 'd'.repeat(40),
|
||||
}), true);
|
||||
});
|
||||
|
||||
test('changing a suite command file invalidates that suite', () => {
|
||||
assert.equal(suiteInvalidatedByChanges(chatSuite, ['chat-router.test.mjs']), true);
|
||||
assert.equal(suiteInvalidatedByChanges(chatSuite, ['wechat-mp.mjs']), false);
|
||||
assert.equal(suiteInvalidatedByChanges(chatSuite, ['release-gate/coverage.mjs']), true);
|
||||
});
|
||||
|
||||
test('carryForwardScenario keeps prior evidence and marks reuse', () => {
|
||||
const carried = carryForwardScenario({ id: 'CHAT-01', status: 'passed', evidence: ['old'] });
|
||||
assert.equal(carried.status, 'passed');
|
||||
assert.deepEqual(carried.evidence, ['old', 'carried_forward=true']);
|
||||
});
|
||||
+54
-5
@@ -6,7 +6,14 @@ import { assertPortalRuntimePath, hashArtifact, inspectPortalRuntime } from './a
|
||||
import { loadScenarioCatalog } from './catalog.mjs';
|
||||
import { AUTOMATION_SUITES, validateAutomationSuites } from './coverage.mjs';
|
||||
import { selectImpactScenarios } from './impact.mjs';
|
||||
import { preflightImpactSuites } from './preflight.mjs';
|
||||
import { loadActiveRegressionCorpus } from './regression-corpus.mjs';
|
||||
import {
|
||||
REPOSITORY_CHECK_IDS,
|
||||
carryForwardScenario,
|
||||
findImpactResumeReport,
|
||||
shouldRerunImpactSuite,
|
||||
} from './resume.mjs';
|
||||
import {
|
||||
buildIncrementalReport,
|
||||
findCarryForwardBaseline,
|
||||
@@ -419,24 +426,61 @@ export async function executeImpactReleaseGate(options) {
|
||||
const suites = AUTOMATION_SUITES.filter(
|
||||
(suite) => suite.scenarios.some((scenarioId) => selectedIds.has(scenarioId)),
|
||||
);
|
||||
const resume = await findImpactResumeReport({
|
||||
reportRoot: options.reportRoot,
|
||||
artifactSha256: artifact.sha256,
|
||||
commitSha,
|
||||
});
|
||||
let changedPathsSincePrevious = [];
|
||||
if (resume?.commitSha && resume.commitSha !== commitSha) {
|
||||
try {
|
||||
changedPathsSincePrevious = await listChangedPathsBetween(resume.commitSha, commitSha);
|
||||
} catch {
|
||||
changedPathsSincePrevious = ['release-gate/coverage.mjs'];
|
||||
}
|
||||
}
|
||||
const suitesToRun = suites.filter((suite) => shouldRerunImpactSuite({
|
||||
suite,
|
||||
selectedIds,
|
||||
previousReport: resume?.report ?? null,
|
||||
artifactSha256: artifact.sha256,
|
||||
commitSha,
|
||||
changedPathsSincePrevious,
|
||||
}));
|
||||
const skippedSuites = suites.filter((suite) => !suitesToRun.includes(suite));
|
||||
const previousById = new Map((resume?.report?.scenarios ?? []).map((scenario) => [scenario.id, scenario]));
|
||||
for (const suite of skippedSuites) {
|
||||
for (const scenarioId of suite.scenarios) {
|
||||
if (!selectedIds.has(scenarioId) || REPOSITORY_CHECK_IDS.has(scenarioId)) continue;
|
||||
const previous = previousById.get(scenarioId);
|
||||
if (!previous) continue;
|
||||
const current = byId.get(scenarioId);
|
||||
Object.assign(current, carryForwardScenario(previous));
|
||||
}
|
||||
}
|
||||
|
||||
if (suitesToRun.length > 0) {
|
||||
await preflightImpactSuites(suitesToRun);
|
||||
}
|
||||
const executions = await runSuitesWithConcurrency(
|
||||
suites,
|
||||
suitesToRun,
|
||||
options.suiteConcurrency,
|
||||
(suite) => runSuite(suite, outputDir, options.timeoutMs),
|
||||
);
|
||||
for (let index = 0; index < suites.length; index += 1) {
|
||||
const suite = suites[index];
|
||||
for (let index = 0; index < suitesToRun.length; index += 1) {
|
||||
const suite = suitesToRun[index];
|
||||
const execution = executions[index];
|
||||
for (const scenarioId of suite.scenarios) {
|
||||
if (!selectedIds.has(scenarioId)) continue;
|
||||
const scenario = byId.get(scenarioId);
|
||||
scenario.status = execution.code === 0 && !execution.timedOut ? 'passed' : 'failed';
|
||||
scenario.reason = scenario.status === 'passed' ? null : 'automation_suite_failed';
|
||||
scenario.evidence.push(
|
||||
scenario.evidence = [
|
||||
`suite=${suite.id}`,
|
||||
`log=${execution.logPath}`,
|
||||
...suite.cases[scenarioId].map((assertedCase) => `asserted_case=${assertedCase}`),
|
||||
);
|
||||
'carried_forward=false',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,6 +501,11 @@ export async function executeImpactReleaseGate(options) {
|
||||
startedAt,
|
||||
completedAt,
|
||||
});
|
||||
report.resume = {
|
||||
baseline_commit: resume?.commitSha ?? null,
|
||||
carried_suites: skippedSuites.map((suite) => suite.id),
|
||||
reran_suites: suitesToRun.map((suite) => suite.id),
|
||||
};
|
||||
await writeGateReport(report, outputDir);
|
||||
return { report, outputDir, selection };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user