Files
OmniRoute/scripts/quality/validate-release-acceptance.mjs
Bob.Hou e7214c72fc ci(acceptance): emit a shadow release-acceptance report next to release-green (#13701)
Adds a shadow release-acceptance report alongside release-green: an inventory/reduce/oracle pipeline under `scripts/quality/release-acceptance/` with a JSON schema, fixtures and a workflow that uploads the report as an artifact.

Contained by design, which is why it merges as-is: it runs only on push to `release/v*` and on manual dispatch (never on pull requests), the step is `continue-on-error`, `permissions: contents: read`, `persist-credentials: false`, and it consumes no secrets. Nothing in the product changes; the report is advisory until we decide to promote it.

Validated as a combined board first (this PR merged with the 11 siblings of the same batch on the release tip): eslint on every changed file with the suppressions file, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 275 passing / 0 failing focused node:test cases across the 28 test files the batch touches. Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run.

Thanks @HouMinXi!
2026-09-16 03:17:52 -03:00

82 lines
2.7 KiB
JavaScript

#!/usr/bin/env node
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import Ajv from "ajv";
import { reduce } from "./release-acceptance/reduce.mjs";
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
function loadJson(p) {
return JSON.parse(readFileSync(p, "utf8"));
}
export function exitFor(verdict) {
if (verdict === "VERIFIED") return 0;
if (verdict === "FAILED") return 1;
return 2;
}
export function reduceManifests(plan, manifests) {
const records = [];
for (const m of manifests) {
if (Array.isArray(m.gates)) records.push(...m.gates);
else records.push(m);
}
return reduce(plan, records);
}
export function validateReport(report, schema) {
const ajv = new Ajv({ allErrors: true, strict: false });
const validate = ajv.compile(schema);
return { ok: validate(report), errors: validate.errors };
}
function parseArgs(argv) {
const out = { plan: null, manifests: null, out: join(ROOT, "release-acceptance-report.json") };
for (let i = 2; i < argv.length; i++) {
if (argv[i] === "--plan") out.plan = argv[++i];
else if (argv[i] === "--manifests") out.manifests = argv[++i];
else if (argv[i] === "--out") out.out = argv[++i];
}
return out;
}
export async function main(argv = process.argv) {
const args = parseArgs(argv);
const plan = loadJson(args.plan);
const schema = loadJson(join(ROOT, "config/quality/release-acceptance.schema.json"));
const files = readdirSync(args.manifests)
.filter((f) => f.endsWith(".json"))
.map((f) => loadJson(join(args.manifests, f)));
const reduced = reduceManifests(plan, files);
const report = {
schema_version: 1,
identity: plan.identity,
required_gates: plan.required_gates ?? [],
gates: reduced.gates,
evidence_errors: reduced.evidence_errors,
verdict: reduced.verdict,
artifact: plan.artifact ?? null,
};
const { ok, errors } = validateReport(report, schema);
if (!ok) {
if (report.verdict !== "FAILED") report.verdict = "UNVERIFIED";
const gate =
Array.isArray(plan.required_gates) && plan.required_gates.length > 0
? plan.required_gates[0]
: { gate_id: "schema", suite_id: null, shard_index: null, shard_total: null };
report.evidence_errors = [
...(report.evidence_errors ?? []),
{ code: "schema_invalid", gate, detail: JSON.stringify(errors) },
];
}
mkdirSync(dirname(args.out), { recursive: true });
writeFileSync(args.out, JSON.stringify(report, null, 2) + "\n");
return exitFor(report.verdict);
}
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
main().then((code) => process.exit(code));
}