127 lines
7.9 KiB
TypeScript
127 lines
7.9 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
|
|
type JsonRecord = Record<string, unknown>;
|
|
|
|
function assertCondition(condition: unknown, message: string, detail: unknown = {}): void {
|
|
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
|
|
}
|
|
|
|
function asRecord(value: unknown, label: string): JsonRecord {
|
|
assertCondition(typeof value === "object" && value !== null && !Array.isArray(value), `${label} must be an object`, { value });
|
|
return value as JsonRecord;
|
|
}
|
|
|
|
function asStringArray(value: unknown, label: string): string[] {
|
|
assertCondition(Array.isArray(value) && value.every((item) => typeof item === "string"), `${label} must be a string array`, value);
|
|
return value as string[];
|
|
}
|
|
|
|
function runCli(args: string[], expectStatus: number): JsonRecord {
|
|
const result = spawnSync("bun", ["scripts/cli.ts", ...args], {
|
|
cwd: process.cwd(),
|
|
encoding: "utf8",
|
|
maxBuffer: 8 * 1024 * 1024,
|
|
});
|
|
assertCondition(result.status === expectStatus, `cli status mismatch for ${args.join(" ")}`, {
|
|
status: result.status,
|
|
stdout: result.stdout.slice(-3000),
|
|
stderr: result.stderr.slice(-3000),
|
|
});
|
|
return asRecord(JSON.parse(result.stdout) as unknown, "cli envelope");
|
|
}
|
|
|
|
function firstService(envelope: JsonRecord, label: string): JsonRecord {
|
|
const data = asRecord(envelope.data, `${label} data`);
|
|
const services = Array.isArray(data.services) ? data.services : [];
|
|
assertCondition(services.length === 1, `${label} should return exactly one service`, data);
|
|
return asRecord(services[0], `${label} service`);
|
|
}
|
|
|
|
function includes(value: unknown, expected: string): boolean {
|
|
return Array.isArray(value) && value.some((item) => item === expected);
|
|
}
|
|
|
|
const commit = "0123456789abcdef0123456789abcdef01234567";
|
|
|
|
const devPlan = firstService(runCli(["deploy", "plan", "--env", "dev", "--service", "code-queue"], 0), "dev plan");
|
|
const devArtifact = asRecord(devPlan.artifactConsumer, "dev artifactConsumer");
|
|
const devTarget = asRecord(devPlan.target, "dev target");
|
|
const devBoundary = asRecord(devPlan.boundary, "dev boundary");
|
|
const devCd = asRecord(devBoundary.cdConsumer, "dev cdConsumer");
|
|
|
|
assertCondition(devArtifact.consumerKind === "d601-k3s-managed", "dev Code Queue must be a k3s-managed artifact consumer", devArtifact);
|
|
assertCondition(devArtifact.noRuntimeSourceBuild === true, "dev Code Queue must not build source on the runtime target", devArtifact);
|
|
assertCondition(devTarget.namespace === "unidesk-dev", "dev Code Queue target namespace must be unidesk-dev", devTarget);
|
|
assertCondition(devTarget.deployment === "code-queue-scheduler-dev", "dev Code Queue should target the dev scheduler deployment", devTarget);
|
|
assertCondition(devCd.prodMutationAllowed === false, "dev Code Queue boundary must prohibit production mutation", devCd);
|
|
assertCondition(String(devCd.manualAuthorizationPoint ?? "").includes("DEV apply"), "dev Code Queue boundary should expose the DEV manual authorization point", devCd);
|
|
assertCondition(asRecord(devBoundary.ciProducer, "dev ciProducer").allowed === true, "Code Queue CI producer should be allowed for image publication", devBoundary);
|
|
assertCondition(includes(devTarget.forbiddenActions, "docker build"), "dev target must forbid runtime docker build", devTarget);
|
|
assertCondition(includes(devTarget.forbiddenActions, "NodePort"), "dev target must forbid NodePort", devTarget);
|
|
|
|
const prodPlan = firstService(runCli(["deploy", "plan", "--env", "prod", "--service", "code-queue"], 1), "prod plan");
|
|
const prodArtifact = asRecord(prodPlan.artifactConsumer, "prod artifactConsumer");
|
|
const prodTarget = asRecord(prodPlan.target, "prod target");
|
|
const prodBoundary = asRecord(prodPlan.boundary, "prod boundary");
|
|
const prodCd = asRecord(prodBoundary.cdConsumer, "prod cdConsumer");
|
|
const prodLiveApply = asRecord(prodPlan.liveApply, "prod liveApply");
|
|
const prodUnsupported = asRecord(prodPlan.unsupported, "prod unsupported");
|
|
const prodForbidden = asStringArray(prodTarget.forbiddenActions, "prod forbiddenActions");
|
|
|
|
assertCondition(prodPlan.deploymentPath === "unsupported", "prod Code Queue deployment path must be unsupported", prodPlan);
|
|
assertCondition(prodArtifact.consumerKind === "unsupported", "prod Code Queue artifact consumer must be unsupported", prodArtifact);
|
|
assertCondition(prodArtifact.registryImage === null, "prod Code Queue plan must not advertise a production registry image target", prodArtifact);
|
|
assertCondition(prodArtifact.noRuntimeSourceBuild === true, "prod Code Queue plan must still block runtime source builds", prodArtifact);
|
|
assertCondition(prodTarget.runtimeHost === null, "prod Code Queue plan must not expose a runtime host target", prodTarget);
|
|
assertCondition(prodTarget.deployCommandShape === "none", "prod Code Queue plan must not expose a deploy command shape", prodTarget);
|
|
assertCondition(prodLiveApply.allowed === false, "prod Code Queue live apply must be blocked", prodLiveApply);
|
|
assertCondition(String(prodLiveApply.reason ?? "").includes("production CD is intentionally unsupported"), "prod blocked reason should name the intentional CD gap", prodLiveApply);
|
|
assertCondition(String(prodUnsupported.reason ?? "").includes("prod artifact deploy"), "prod unsupported reason should mention prod artifact deploy", prodUnsupported);
|
|
assertCondition(prodCd.prodMutationAllowed === false, "prod Code Queue boundary must prohibit production mutation", prodCd);
|
|
assertCondition(prodCd.liveApplyCommandShape === null, "prod Code Queue boundary must not advertise a live apply command", prodCd);
|
|
assertCondition(String(prodCd.manualAuthorizationPoint ?? "").includes("future supervisor-approved"), "prod boundary should require a future supervisor-approved design", prodCd);
|
|
assertCondition(prodForbidden.includes("production namespace mutation"), "prod forbidden actions must include production namespace mutation", prodTarget);
|
|
assertCondition(prodForbidden.includes("interrupt running Code Queue tasks"), "prod forbidden actions must include task interrupt", prodTarget);
|
|
assertCondition(prodForbidden.includes("cancel running Code Queue tasks"), "prod forbidden actions must include task cancel", prodTarget);
|
|
assertCondition(JSON.stringify(prodTarget).includes("code-queue-read"), "prod excluded targets should name production Code Queue deployments", prodTarget);
|
|
|
|
const devArtifactDryRun = asRecord(runCli([
|
|
"artifact-registry",
|
|
"deploy-service",
|
|
"--env",
|
|
"dev",
|
|
"--service",
|
|
"code-queue",
|
|
"--commit",
|
|
commit,
|
|
"--dry-run",
|
|
], 0).data, "dev artifact-registry dry-run");
|
|
const artifactTarget = asRecord(devArtifactDryRun.target, "artifact dry-run target");
|
|
assertCondition(devArtifactDryRun.mutation === false, "artifact-registry dev dry-run must be non-mutating", devArtifactDryRun);
|
|
assertCondition(artifactTarget.namespace === "unidesk-dev", "artifact-registry dev dry-run must target unidesk-dev", artifactTarget);
|
|
|
|
const prodArtifactDryRun = asRecord(runCli([
|
|
"artifact-registry",
|
|
"deploy-service",
|
|
"--env",
|
|
"prod",
|
|
"--service",
|
|
"code-queue",
|
|
"--commit",
|
|
commit,
|
|
"--dry-run",
|
|
], 1).data, "prod artifact-registry dry-run");
|
|
assertCondition(prodArtifactDryRun.error === "unsupported-environment", "artifact-registry prod code-queue should be unsupported", prodArtifactDryRun);
|
|
assertCondition(Array.isArray(prodArtifactDryRun.supportedEnvironments) && prodArtifactDryRun.supportedEnvironments.length === 1 && prodArtifactDryRun.supportedEnvironments[0] === "dev", "artifact-registry prod code-queue should expose only dev as supported", prodArtifactDryRun);
|
|
|
|
process.stdout.write(`${JSON.stringify({
|
|
ok: true,
|
|
checks: [
|
|
"deploy plan exposes Code Queue CI producer and dev CD consumer boundary",
|
|
"dev Code Queue dry-run targets only unidesk-dev and forbids runtime builds/public ports",
|
|
"prod Code Queue plan is unsupported and exposes no runtime deploy target",
|
|
"prod Code Queue boundary forbids self-deploy, prod mutation, interrupt and cancel actions",
|
|
"artifact-registry dev dry-run is non-mutating while prod remains unsupported",
|
|
],
|
|
}, null, 2)}\n`);
|