Files
pikasTech-HWLAB/scripts/ci/verify-reviewed-plan.mjs
T

104 lines
3.9 KiB
JavaScript

#!/usr/bin/env node
import { readFile } from "node:fs/promises";
import { pathToFileURL } from "node:url";
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
const args = parseArgs(process.argv.slice(2));
const actual = JSON.parse(await readFile(args.actualPlan, "utf8"));
const expected = decodeReviewedPlan(args.reviewedPlan);
const result = compareReviewedPlan(expected, actual);
process.stdout.write(`${JSON.stringify(result)}\n`);
if (!result.ok) process.exit(42);
}
export function decodeReviewedPlan(value) {
const prefix = "unidesk-release-plan-v1.";
if (!String(value).startsWith(prefix)) throw new Error("reviewed plan envelope is missing or unsupported");
const decoded = Buffer.from(String(value).slice(prefix.length), "base64url").toString("utf8");
const plan = JSON.parse(decoded);
if (plan?.version !== "v1") throw new Error("reviewed plan version is unsupported");
return plan;
}
export function compareReviewedPlan(expected, actual) {
const actualScope = scope(actual);
const expectedScope = scope(expected);
const expectedIdentity = text(expected?.planIdentity);
const actualIdentity = text(actual?.planIdentity?.value);
const expectedMetadata = {
target: text(expected?.target),
consumer: text(expected?.consumer),
sourceCommit: text(expected?.sourceCommit),
baseCommit: text(expected?.baseCommit)
};
const actualMetadata = {
target: text(actual?.planIdentity?.inputs?.target),
consumer: text(actual?.planIdentity?.inputs?.consumer),
sourceCommit: text(actual?.sourceCommitId),
baseCommit: text(actual?.baseCommitId)
};
const additions = {
buildServices: difference(actualScope.buildServices, expectedScope.buildServices),
rolloutServices: difference(actualScope.rolloutServices, expectedScope.rolloutServices),
affectedServices: difference(actualScope.affectedServices, expectedScope.affectedServices)
};
const identityMatches = Boolean(expectedIdentity) && expectedIdentity === actualIdentity;
const metadataMatches = Object.keys(expectedMetadata).every((key) => expectedMetadata[key] && expectedMetadata[key] === actualMetadata[key]);
const scopeExpanded = Object.values(additions).some((items) => items.length > 0);
return {
event: "reviewed-plan-admission",
ok: identityMatches && metadataMatches && !scopeExpanded,
identityMatches,
metadataMatches,
scopeExpanded,
expectedIdentity,
actualIdentity,
expectedMetadata,
actualMetadata,
expectedScope,
actualScope,
additions,
valuesPrinted: false
};
}
function scope(value) {
return {
buildServices: sortedStrings(value?.buildServices ?? value?.scope?.buildServices),
rolloutServices: sortedStrings(value?.rolloutServices ?? value?.scope?.rolloutServices),
affectedServices: sortedStrings(value?.affectedServices ?? value?.scope?.affectedServices)
};
}
function difference(actual, expected) {
const expectedSet = new Set(expected);
return actual.filter((item) => !expectedSet.has(item));
}
function sortedStrings(value) {
return [...new Set(Array.isArray(value) ? value.map(text).filter(Boolean) : [])].sort();
}
function text(value) {
return typeof value === "string" ? value.trim() : "";
}
function parseArgs(argv) {
const result = { actualPlan: "", reviewedPlan: "" };
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--actual-plan") result.actualPlan = readOption(argv, ++index, arg);
else if (arg === "--reviewed-plan") result.reviewedPlan = readOption(argv, ++index, arg);
else throw new Error(`unknown argument ${arg}`);
}
if (!result.actualPlan) throw new Error("--actual-plan is required");
if (!result.reviewedPlan) throw new Error("--reviewed-plan is required");
return result;
}
function readOption(argv, index, name) {
const value = argv[index];
if (!value || value.startsWith("--")) throw new Error(`${name} requires a value`);
return value;
}