diff --git a/scripts/ci-plan.mjs b/scripts/ci-plan.mjs index e748eba..53ba1c7 100644 --- a/scripts/ci-plan.mjs +++ b/scripts/ci-plan.mjs @@ -10,6 +10,7 @@ const envIdentityExcludedPaths = new Set([ "scripts/ci-plan.mjs", "scripts/ci-plan.test.mjs", "scripts/ci/verify-reviewed-plan.mjs", + "scripts/ci/verify-reviewed-plan.test.mjs", ]); if (import.meta.url === pathToFileURL(process.argv[1]).href) { diff --git a/scripts/ci-plan.test.mjs b/scripts/ci-plan.test.mjs index cde2491..7491215 100644 --- a/scripts/ci-plan.test.mjs +++ b/scripts/ci-plan.test.mjs @@ -21,6 +21,7 @@ test("plan reuses a present env image without expanding build scope", async () = await writeFile(join(root, "scripts", "ci-plan.mjs"), "planner-only\n"); await writeFile(join(root, "scripts", "ci-plan.test.mjs"), "planner-test-only\n"); await writeFile(join(root, "scripts", "ci", "verify-reviewed-plan.mjs"), "admission-only\n"); + await writeFile(join(root, "scripts", "ci", "verify-reviewed-plan.test.mjs"), "admission-test-only\n"); assert.equal(calculateEnvIdentity({ root, envIdentityFiles: ["src", "scripts"], diff --git a/scripts/ci/verify-reviewed-plan.mjs b/scripts/ci/verify-reviewed-plan.mjs index 1c75c5c..738e7f5 100644 --- a/scripts/ci/verify-reviewed-plan.mjs +++ b/scripts/ci/verify-reviewed-plan.mjs @@ -1,34 +1,43 @@ #!/usr/bin/env node import { readFile } from "node:fs/promises"; +import { pathToFileURL } from "node:url"; -const [actualPath, envelopePath] = process.argv.slice(2); -if (!actualPath || !envelopePath) throw new Error("usage: verify-reviewed-plan.mjs "); -const actual = JSON.parse(await readFile(actualPath, "utf8")); -const envelope = (await readFile(envelopePath, "utf8")).trim(); -const prefix = "unidesk-release-plan-v1."; -if (!envelope.startsWith(prefix)) throw new Error("reviewed plan envelope is missing or unsupported"); -const expected = JSON.parse(Buffer.from(envelope.slice(prefix.length), "base64url").toString("utf8")); -const actualScope = scope(actual); -const expectedScope = scope(expected); -const additions = Object.fromEntries(Object.keys(actualScope).map((key) => [key, actualScope[key].filter((item) => !expectedScope[key].includes(item))])); -const result = { - event: "reviewed-plan-admission", - ok: expected.version === "v1" - && expected.planIdentity === actual.planIdentity?.value - && expected.target === actual.planIdentity?.inputs?.target - && expected.consumer === actual.planIdentity?.inputs?.consumer - && expected.sourceCommit === actual.sourceCommitId - && expected.baseCommit === actual.baseCommitId - && Object.values(additions).every((items) => items.length === 0), - expectedIdentity: expected.planIdentity ?? null, - actualIdentity: actual.planIdentity?.value ?? null, - expectedScope, - actualScope, - additions, - valuesPrinted: false, -}; -process.stdout.write(`${JSON.stringify(result)}\n`); -if (!result.ok) process.exit(42); +if (import.meta.url === pathToFileURL(process.argv[1]).href) { + const [actualPath, envelopePath] = process.argv.slice(2); + if (!actualPath || !envelopePath) throw new Error("usage: verify-reviewed-plan.mjs "); + const actual = JSON.parse(await readFile(actualPath, "utf8")); + const envelope = (await readFile(envelopePath, "utf8")).trim(); + const result = verifyReviewedPlan(actual, envelope); + process.stdout.write(`${JSON.stringify(result)}\n`); + if (!result.ok) process.exit(42); +} + +export function verifyReviewedPlan(actual, envelope) { + const consumer = actual?.planIdentity?.inputs?.consumer; + if (typeof consumer !== "string" || consumer.length === 0) throw new Error("actual plan consumer is missing"); + const prefix = `unidesk-release-plan-v1.${consumer}.`; + if (!envelope.startsWith(prefix)) throw new Error("reviewed plan envelope is missing, unsupported, or belongs to another consumer"); + const expected = JSON.parse(Buffer.from(envelope.slice(prefix.length), "base64url").toString("utf8")); + const actualScope = scope(actual); + const expectedScope = scope(expected); + const additions = Object.fromEntries(Object.keys(actualScope).map((key) => [key, actualScope[key].filter((item) => !expectedScope[key].includes(item))])); + return { + event: "reviewed-plan-admission", + ok: expected.version === "v1" + && expected.planIdentity === actual.planIdentity?.value + && expected.target === actual.planIdentity?.inputs?.target + && expected.consumer === consumer + && expected.sourceCommit === actual.sourceCommitId + && expected.baseCommit === actual.baseCommitId + && Object.values(additions).every((items) => items.length === 0), + expectedIdentity: expected.planIdentity ?? null, + actualIdentity: actual.planIdentity?.value ?? null, + expectedScope, + actualScope, + additions, + valuesPrinted: false, + }; +} function scope(value) { const sorted = (items) => [...new Set(Array.isArray(items) ? items.filter((item) => typeof item === "string" && item) : [])].sort(); diff --git a/scripts/ci/verify-reviewed-plan.test.mjs b/scripts/ci/verify-reviewed-plan.test.mjs new file mode 100644 index 0000000..150523c --- /dev/null +++ b/scripts/ci/verify-reviewed-plan.test.mjs @@ -0,0 +1,47 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { verifyReviewedPlan } from "./verify-reviewed-plan.mjs"; + +const consumer = "agentrun-nc01-v02"; +const actual = { + sourceCommitId: "a".repeat(40), + baseCommitId: "b".repeat(40), + buildServices: [], + rolloutServices: ["agentrun-mgr"], + affectedServices: ["agentrun-mgr"], + planIdentity: { + value: `sha256:${"c".repeat(64)}`, + inputs: { target: "NC01", consumer }, + }, +}; + +function envelope(overrides = {}, envelopeConsumer = consumer) { + const reviewed = { + version: "v1", + planIdentity: actual.planIdentity.value, + target: "NC01", + consumer, + sourceCommit: actual.sourceCommitId, + baseCommit: actual.baseCommitId, + buildServices: [], + rolloutServices: ["agentrun-mgr"], + affectedServices: ["agentrun-mgr"], + ...overrides, + }; + return `unidesk-release-plan-v1.${envelopeConsumer}.${Buffer.from(JSON.stringify(reviewed)).toString("base64url")}`; +} + +test("accepts the consumer-qualified reviewed plan envelope", () => { + assert.equal(verifyReviewedPlan(actual, envelope()).ok, true); +}); + +test("rejects an envelope addressed to another consumer", () => { + assert.throws(() => verifyReviewedPlan(actual, envelope({}, "agentrun-nc01-release")), /another consumer/u); +}); + +test("rejects actual scope that expands beyond the reviewed plan", () => { + const expanded = { ...actual, buildServices: ["agentrun-mgr"] }; + const result = verifyReviewedPlan(expanded, envelope()); + assert.equal(result.ok, false); + assert.deepEqual(result.additions.buildServices, ["agentrun-mgr"]); +});