#!/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; }