feat(workflow): add risk-based release gates
This commit is contained in:
@@ -3,8 +3,10 @@ import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
import { assertPortalRuntimePath, hashArtifact } from '../release-gate/artifact.mjs';
|
||||
import { loadScenarioCatalog } from '../release-gate/catalog.mjs';
|
||||
import { selectImpactScenarios } from '../release-gate/impact.mjs';
|
||||
import { validateGateReport } from '../release-gate/report.mjs';
|
||||
import { runCommand } from '../release-gate/runner.mjs';
|
||||
import { listChangedPathsBetween, runCommand } from '../release-gate/runner.mjs';
|
||||
|
||||
const ROOT = path.resolve(new URL('..', import.meta.url).pathname);
|
||||
|
||||
@@ -28,6 +30,22 @@ async function git(...args) {
|
||||
return result.stdout.trim();
|
||||
}
|
||||
|
||||
function comparableSelection(selection) {
|
||||
return {
|
||||
policy_version: selection.policy_version,
|
||||
strategy: selection.strategy,
|
||||
catalog_total: selection.catalog_total,
|
||||
core_ids: selection.core_ids,
|
||||
impact_groups: selection.impact_groups,
|
||||
changed_paths: selection.changed_paths,
|
||||
unmapped_paths: selection.unmapped_paths,
|
||||
full_gate_reasons: selection.full_gate_reasons,
|
||||
selected_ids: selection.selected_ids,
|
||||
selected_total: selection.selected_total,
|
||||
base_commit: selection.base_commit,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const options = parseArgs(process.argv);
|
||||
options.artifact = assertPortalRuntimePath(options.artifact, { repoRoot: ROOT });
|
||||
@@ -43,6 +61,39 @@ try {
|
||||
expectedArtifactSha256: artifact.sha256,
|
||||
expectedBranch: 'main',
|
||||
});
|
||||
if (report.mode === 'impact' && /^[0-9a-f]{40}$/i.test(report?.selection?.base_commit ?? '')) {
|
||||
try {
|
||||
const [catalog, changedPaths, ancestry] = await Promise.all([
|
||||
loadScenarioCatalog({ root: ROOT }),
|
||||
listChangedPathsBetween(report.selection.base_commit, commitSha),
|
||||
runCommand(
|
||||
'git',
|
||||
['merge-base', '--is-ancestor', report.selection.base_commit, commitSha],
|
||||
{ cwd: ROOT, timeoutMs: 30_000 },
|
||||
),
|
||||
]);
|
||||
const expectedSelection = {
|
||||
...selectImpactScenarios({
|
||||
catalog,
|
||||
changedPaths,
|
||||
forceFullReasons: ancestry.code === 0
|
||||
? []
|
||||
: [`deployed_commit_not_ancestor:${report.selection.base_commit}`],
|
||||
}),
|
||||
base_commit: report.selection.base_commit,
|
||||
};
|
||||
if (
|
||||
JSON.stringify(comparableSelection(report.selection))
|
||||
!== JSON.stringify(comparableSelection(expectedSelection))
|
||||
) {
|
||||
validation.errors.push('impact selection does not match the candidate Git diff');
|
||||
validation.valid = false;
|
||||
}
|
||||
} catch (error) {
|
||||
validation.errors.push(`impact selection cannot be reproduced: ${error.message}`);
|
||||
validation.valid = false;
|
||||
}
|
||||
}
|
||||
if (!validation.valid) {
|
||||
const blocking = report.scenarios.filter(
|
||||
(scenario) => !['passed', 'not_applicable'].includes(scenario.status),
|
||||
|
||||
Reference in New Issue
Block a user