feat(release): add incremental gate and auto CI status for 103 publish
When the runtime artifact is unchanged, carry forward prior gate results and re-run only REL scenarios; resolve REL-02 from Gitea commit status and allow stable promotion when canary matches the same artifact bundle. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+98
-3
@@ -6,11 +6,18 @@ import { assertPortalRuntimePath, hashArtifact, inspectPortalRuntime } from './a
|
||||
import { loadScenarioCatalog } from './catalog.mjs';
|
||||
import { AUTOMATION_SUITES, validateAutomationSuites } from './coverage.mjs';
|
||||
import { loadActiveRegressionCorpus } from './regression-corpus.mjs';
|
||||
import { createGateReport, writeGateReport } from './report.mjs';
|
||||
import {
|
||||
buildIncrementalReport,
|
||||
findCarryForwardBaseline,
|
||||
isReleaseScenario,
|
||||
mergeIncrementalScenarios,
|
||||
} from './incremental.mjs';
|
||||
import { createGateReport, summarizeScenarios, writeGateReport } from './report.mjs';
|
||||
import { assertSafeGateEnvironment, assertSafePortalBase } from './safety.mjs';
|
||||
import { resolveReleaseCiStatus } from './ci-status.mjs';
|
||||
|
||||
const ROOT = path.resolve(new URL('..', import.meta.url).pathname);
|
||||
const MODES = new Set(['deterministic', 'scenarios', 'browser', 'providers', 'upgrade', 'all']);
|
||||
const MODES = new Set(['deterministic', 'scenarios', 'browser', 'providers', 'upgrade', 'all', 'incremental']);
|
||||
|
||||
async function git(...args) {
|
||||
const result = await runCommand('git', args, { cwd: ROOT, timeoutMs: 30_000 });
|
||||
@@ -26,6 +33,8 @@ export function parseRunnerArgs(argv) {
|
||||
reportRoot: path.join(ROOT, '.release-gate'),
|
||||
timeoutMs: 15 * 60 * 1000,
|
||||
suiteConcurrency: Number(process.env.RELEASE_GATE_SUITE_CONCURRENCY ?? 4),
|
||||
baselineCommit: null,
|
||||
deployedCommit: null,
|
||||
};
|
||||
for (let index = 2; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
@@ -35,6 +44,8 @@ export function parseRunnerArgs(argv) {
|
||||
else if (arg === '--report-root') options.reportRoot = path.resolve(argv[++index] ?? '');
|
||||
else if (arg === '--timeout-ms') options.timeoutMs = Number(argv[++index]);
|
||||
else if (arg === '--suite-concurrency') options.suiteConcurrency = Number(argv[++index]);
|
||||
else if (arg === '--baseline-commit') options.baselineCommit = argv[++index] ?? '';
|
||||
else if (arg === '--deployed-commit') options.deployedCommit = argv[++index] ?? '';
|
||||
else if (arg === '-h' || arg === '--help') options.help = true;
|
||||
else throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
@@ -191,7 +202,9 @@ async function applyRepositoryChecks(results, artifactPath) {
|
||||
`ignored_generated_paths=${status !== relevantStatus}`,
|
||||
);
|
||||
|
||||
const ciStatus = String(process.env.MEMIND_RELEASE_CI_STATUS ?? '').trim().toLowerCase();
|
||||
const ciStatus = remoteSha === headSha
|
||||
? await resolveReleaseCiStatus(headSha)
|
||||
: 'missing';
|
||||
const rel02 = byId.get('REL-02');
|
||||
rel02.status = remoteSha === headSha && ciStatus === 'success' ? 'passed' : 'failed';
|
||||
rel02.reason = rel02.status === 'passed'
|
||||
@@ -296,3 +309,85 @@ export async function executeReleaseGate(options) {
|
||||
await writeGateReport(report, outputDir);
|
||||
return { report, outputDir };
|
||||
}
|
||||
|
||||
export async function executeIncrementalReleaseGate(options) {
|
||||
const startedAt = new Date();
|
||||
assertSafeGateEnvironment({ targets: [options.portalBase] });
|
||||
assertSafePortalBase(options.portalBase);
|
||||
if (!options.artifact) {
|
||||
throw new Error('incremental release gate requires --artifact');
|
||||
}
|
||||
options.artifact = assertPortalRuntimePath(options.artifact, { repoRoot: ROOT });
|
||||
|
||||
const catalog = await loadScenarioCatalog({ root: ROOT });
|
||||
validateAutomationSuites(catalog);
|
||||
const commitSha = await git('rev-parse', 'HEAD');
|
||||
const branch = await git('branch', '--show-current');
|
||||
const outputDir = path.join(options.reportRoot, commitSha);
|
||||
const artifact = await hashArtifact(options.artifact);
|
||||
|
||||
const baseline = await findCarryForwardBaseline({
|
||||
reportRoot: options.reportRoot,
|
||||
artifactSha256: artifact.sha256,
|
||||
preferredCommitSha: options.baselineCommit ?? null,
|
||||
deployedCommitSha: options.deployedCommit ?? null,
|
||||
});
|
||||
if (!baseline) {
|
||||
throw new Error(
|
||||
'No valid baseline Gate report found for the current artifact; run the full release gate first.',
|
||||
);
|
||||
}
|
||||
|
||||
const reexecuted = catalog
|
||||
.filter((scenario) => isReleaseScenario(scenario.id))
|
||||
.map(scenarioResult);
|
||||
const reexecutedById = new Map(reexecuted.map((result) => [result.id, result]));
|
||||
await applyRepositoryChecks(reexecuted, options.artifact);
|
||||
|
||||
const upgradeSuites = AUTOMATION_SUITES.filter((suite) => suite.mode === 'upgrade');
|
||||
const executions = await runSuitesWithConcurrency(
|
||||
upgradeSuites,
|
||||
options.suiteConcurrency,
|
||||
(suite) => runSuite(suite, outputDir, options.timeoutMs),
|
||||
);
|
||||
for (let index = 0; index < upgradeSuites.length; index += 1) {
|
||||
const suite = upgradeSuites[index];
|
||||
const execution = executions[index];
|
||||
for (const scenarioId of suite.scenarios) {
|
||||
if (!isReleaseScenario(scenarioId)) continue;
|
||||
const scenario = reexecutedById.get(scenarioId);
|
||||
scenario.status = execution.code === 0 && !execution.timedOut ? 'passed' : 'failed';
|
||||
scenario.reason = scenario.status === 'passed' ? null : 'automation_suite_failed';
|
||||
scenario.evidence.push(
|
||||
`suite=${suite.id}`,
|
||||
`log=${execution.logPath}`,
|
||||
...suite.cases[scenarioId].map((assertedCase) => `asserted_case=${assertedCase}`),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mergedScenarios = mergeIncrementalScenarios({
|
||||
baselineScenarios: baseline.report.scenarios,
|
||||
reexecutedScenarios: reexecuted,
|
||||
});
|
||||
const completedAt = new Date();
|
||||
const report = buildIncrementalReport({
|
||||
baselineReport: baseline.report,
|
||||
baselineReportPath: path.relative(ROOT, baseline.reportPath),
|
||||
commitSha,
|
||||
branch,
|
||||
artifactSha256: artifact.sha256,
|
||||
artifact: {
|
||||
path: path.relative(ROOT, options.artifact),
|
||||
kind: artifact.kind,
|
||||
files: artifact.files,
|
||||
bytes: artifact.bytes,
|
||||
},
|
||||
scenarios: mergedScenarios,
|
||||
startedAt,
|
||||
completedAt,
|
||||
});
|
||||
report.summary = summarizeScenarios(mergedScenarios);
|
||||
await writeGateReport(report, outputDir);
|
||||
return { report, outputDir, baseline };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user