feat(workflow): add risk-based release gates

This commit is contained in:
john
2026-07-27 10:38:24 +08:00
parent c88623855f
commit dfab78c75a
17 changed files with 794 additions and 69 deletions
+52 -1
View File
@@ -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),