104 lines
3.9 KiB
JavaScript
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;
|
|
}
|