test: extend deploy json drift contract to decision center

This commit is contained in:
Codex
2026-05-21 14:12:50 +00:00
parent dff355c5a4
commit f12fd2fb58
7 changed files with 182 additions and 83 deletions
+1 -1
View File
@@ -59,7 +59,7 @@ const plannedDeployJsonFields = [
"runtime.requiredSecretKeys",
];
const phaseTwoExecutorContractServices = new Set(["dev/mdtodo"]);
const phaseTwoExecutorContractServices = new Set(["dev/decision-center", "dev/mdtodo"]);
function assertCondition(condition: unknown, message: string, detail: unknown = {}): void {
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
@@ -28,6 +28,12 @@ function asArray(value: unknown, label: string): unknown[] {
return value;
}
interface ContractCase {
serviceId: string;
expectedRuntimeImage: string;
driftPort: number;
}
function deployService(environment: "dev" | "prod", serviceId: string): DeployJsonServiceContract {
const deploy = asRecord(JSON.parse(readFileSync(rootPath("deploy.json"), "utf8")) as unknown, "deploy.json");
const environments = asRecord(deploy.environments, "deploy.json.environments");
@@ -38,90 +44,127 @@ function deployService(environment: "dev" | "prod", serviceId: string): DeployJs
return parseDeployJsonServiceContract(raw, `deploy.json.environments.${environment}.services.${serviceId}`);
}
const service = deployService("dev", "mdtodo");
const artifact = asRecord(service.artifact, "mdtodo artifact");
const consumer = asRecord(service.consumer, "mdtodo consumer");
const targetContract = asRecord(consumer.target, "mdtodo consumer target");
const runtime = asRecord(service.runtime, "mdtodo runtime");
const memory = asRecord(runtime.memory, "mdtodo runtime memory");
const commit = String(service.commitId);
const sourceOfTruth = deployJsonSourceOfTruth(service, "dev");
const deploySource = readFileSync(rootPath("scripts/src/deploy.ts"), "utf8");
assertCondition(deploySource.includes('"--deploy-json-service", encodeDeployJsonServiceContract(service)'), "deploy executor must pass deploy.json service contract into artifact-registry", {});
const applyPlan = asRecord(await runArtifactRegistryCommand([
"deploy-service",
"--env",
"dev",
"--service",
"mdtodo",
"--commit",
commit,
"--source-repo",
service.repo,
"--deploy-ref",
"origin/master:deploy.json#environments.dev.services.mdtodo",
"--deploy-json-service",
encodeDeployJsonServiceContract(service),
"--dry-run",
]), "artifact-registry dry-run result");
const applyTarget = asRecord(applyPlan.target, "artifact-registry target");
const applyRegistry = asRecord(applyPlan.registry, "artifact-registry registry");
const applyRuntime = asRecord(applyPlan.runtime, "artifact-registry runtime");
const applyRuntimeMemory = asRecord(applyRuntime.memory, "artifact-registry runtime memory");
const applyDriftCheck = asRecord(applyPlan.driftCheck, "artifact-registry driftCheck");
assertCondition(applyPlan.sourceOfTruth !== undefined, "artifact-registry dry-run should expose deploy.json sourceOfTruth", applyPlan);
assertCondition(JSON.stringify(applyPlan.sourceOfTruth) === JSON.stringify(sourceOfTruth), "artifact-registry sourceOfTruth must enumerate deploy.json fields", applyPlan.sourceOfTruth);
assertCondition(applyDriftCheck.ok === true, "artifact-registry dry-run must report a passing drift preflight", applyDriftCheck);
assertCondition(applyRegistry.repository === artifact.repository, "artifact-registry dry-run repository must come from deploy.json", applyRegistry);
assertCondition(applyRegistry.tag === commit, "artifact-registry dry-run tag must be deploy.json commitId", applyRegistry);
assertCondition(applyRegistry.imageRef === `127.0.0.1:5000/${artifact.repository}:${commit}`, "artifact-registry imageRef must be deploy.json artifact repository + commit", applyRegistry);
assertCondition(applyTarget.namespace === targetContract.namespace, "artifact-registry dry-run namespace must come from deploy.json", applyTarget);
assertCondition(applyTarget.deployment === targetContract.deployment, "artifact-registry dry-run deployment must come from deploy.json", applyTarget);
assertCondition(applyTarget.service === targetContract.service, "artifact-registry dry-run service must come from deploy.json", applyTarget);
assertCondition(asArray(applyTarget.deployments, "artifact-registry deployments").some((item) => asRecord(item, "deployment").name === targetContract.deployment), "artifact-registry deployments must come from deploy.json target", applyTarget);
assertCondition(applyTarget.stableImage === targetContract.stableImage, "artifact-registry stableImage must come from deploy.json", applyTarget);
assertCondition(applyTarget.runtimeImage === `unidesk-mdtodo:${commit}`, "artifact-registry runtimeImage must derive from deploy.json stableImage + commit", applyTarget);
assertCondition(applyTarget.manifestRepoPath === targetContract.manifestRepoPath, "artifact-registry manifest path must come from deploy.json", applyTarget);
assertCondition(applyRuntime.sourceOfTruth === "deploy.json", "artifact-registry runtime source must be deploy.json", applyRuntime);
assertCondition(applyRuntime.containerPort === runtime.containerPort, "artifact-registry runtime port must come from deploy.json", applyRuntime);
assertCondition(applyRuntime.healthPath === runtime.healthPath, "artifact-registry runtime healthPath must come from deploy.json", applyRuntime);
assertCondition(applyRuntimeMemory.request === memory.request && applyRuntimeMemory.limit === memory.limit, "artifact-registry memory must come from deploy.json", applyRuntimeMemory);
const manifestMirror = k3sManifestExecutorMirror(service);
assertCondition(manifestMirror !== null, "mdtodo deploy.json contract should locate a k8s manifest mirror");
const cleanDrifts = compareDeployJsonExecutorMirrors(service, "dev", [manifestMirror!]);
assertCondition(cleanDrifts.length === 0, "current k8s manifest mirror should match deploy.json contract", cleanDrifts);
const driftMirror: DeployJsonExecutorMirror = {
...manifestMirror!,
runtime: {
...manifestMirror!.runtime,
containerPort: 4268,
memory: {
...manifestMirror!.runtime?.memory,
limit: "384Mi",
},
const cases: ContractCase[] = [
{
serviceId: "mdtodo",
expectedRuntimeImage: "unidesk-mdtodo",
driftPort: 4268,
},
};
const drift = compareDeployJsonExecutorMirrors(service, "dev", [driftMirror]);
const driftResult = deployJsonDriftResult(service, "dev", drift);
const driftPayload = asRecord(driftResult.drift, "drift result payload");
const driftItems = asArray(driftPayload.items, "drift result items").map((item) => asRecord(item, "drift item"));
assertCondition(driftResult.ok === false, "drift result must be non-ok", driftResult);
assertCondition(driftResult.error === "deploy-json-drift", "drift result should use structured deploy-json-drift error", driftResult);
assertCondition(driftItems.some((item) => item.field === "runtime.containerPort" && item.expected === 4267 && item.actual === 4268), "drift result should report port mismatch", driftItems);
assertCondition(driftItems.some((item) => item.field === "runtime.memory.limit" && item.expected === "512Mi" && item.actual === "384Mi"), "drift result should report memory mismatch", driftItems);
{
serviceId: "decision-center",
expectedRuntimeImage: "unidesk-decision-center",
driftPort: 4278,
},
];
const verifiedServices: string[] = [];
for (const item of cases) {
const service = deployService("dev", item.serviceId);
const artifact = asRecord(service.artifact, `${item.serviceId} artifact`);
const consumer = asRecord(service.consumer, `${item.serviceId} consumer`);
const targetContract = asRecord(consumer.target, `${item.serviceId} consumer target`);
const runtime = asRecord(service.runtime, `${item.serviceId} runtime`);
const memory = asRecord(runtime.memory, `${item.serviceId} runtime memory`);
const health = asRecord(runtime.health, `${item.serviceId} runtime health`);
const commit = String(service.commitId);
const sourceOfTruth = deployJsonSourceOfTruth(service, "dev");
const applyPlan = asRecord(await runArtifactRegistryCommand([
"deploy-service",
"--env",
"dev",
"--service",
item.serviceId,
"--commit",
commit,
"--source-repo",
service.repo,
"--deploy-ref",
`origin/master:deploy.json#environments.dev.services.${item.serviceId}`,
"--deploy-json-service",
encodeDeployJsonServiceContract(service),
"--dry-run",
]), `${item.serviceId} artifact-registry dry-run result`);
const applyTarget = asRecord(applyPlan.target, `${item.serviceId} artifact-registry target`);
const applyRegistry = asRecord(applyPlan.registry, `${item.serviceId} artifact-registry registry`);
const applyRuntime = asRecord(applyPlan.runtime, `${item.serviceId} artifact-registry runtime`);
const applyRuntimeMemory = asRecord(applyRuntime.memory, `${item.serviceId} artifact-registry runtime memory`);
const applyRuntimeHealth = asRecord(applyRuntime.health, `${item.serviceId} artifact-registry runtime health`);
const applyDriftCheck = asRecord(applyPlan.driftCheck, `${item.serviceId} artifact-registry driftCheck`);
assertCondition(applyPlan.sourceOfTruth !== undefined, `${item.serviceId} artifact-registry dry-run should expose deploy.json sourceOfTruth`, applyPlan);
assertCondition(JSON.stringify(applyPlan.sourceOfTruth) === JSON.stringify(sourceOfTruth), `${item.serviceId} artifact-registry sourceOfTruth must enumerate deploy.json fields`, applyPlan.sourceOfTruth);
assertCondition(applyDriftCheck.ok === true, `${item.serviceId} artifact-registry dry-run must report a passing drift preflight`, applyDriftCheck);
assertCondition(applyRegistry.repository === artifact.repository, `${item.serviceId} artifact-registry dry-run repository must come from deploy.json`, applyRegistry);
assertCondition(applyRegistry.tag === commit, `${item.serviceId} artifact-registry dry-run tag must be deploy.json commitId`, applyRegistry);
assertCondition(applyRegistry.imageRef === `127.0.0.1:5000/${artifact.repository}:${commit}`, `${item.serviceId} artifact-registry imageRef must be deploy.json artifact repository + commit`, applyRegistry);
assertCondition(applyTarget.namespace === targetContract.namespace, `${item.serviceId} artifact-registry dry-run namespace must come from deploy.json`, applyTarget);
assertCondition(applyTarget.deployment === targetContract.deployment, `${item.serviceId} artifact-registry dry-run deployment must come from deploy.json`, applyTarget);
assertCondition(applyTarget.service === targetContract.service, `${item.serviceId} artifact-registry dry-run service must come from deploy.json`, applyTarget);
assertCondition(asArray(applyTarget.deployments, `${item.serviceId} artifact-registry deployments`).some((deployment) => asRecord(deployment, "deployment").name === targetContract.deployment), `${item.serviceId} artifact-registry deployments must come from deploy.json target`, applyTarget);
assertCondition(applyTarget.stableImage === targetContract.stableImage, `${item.serviceId} artifact-registry stableImage must come from deploy.json`, applyTarget);
assertCondition(applyTarget.runtimeImage === `${item.expectedRuntimeImage}:${commit}`, `${item.serviceId} artifact-registry runtimeImage must derive from deploy.json stableImage + commit`, applyTarget);
assertCondition(applyTarget.manifestRepoPath === targetContract.manifestRepoPath, `${item.serviceId} artifact-registry manifest path must come from deploy.json`, applyTarget);
assertCondition(applyRuntime.sourceOfTruth === "deploy.json", `${item.serviceId} artifact-registry runtime source must be deploy.json`, applyRuntime);
assertCondition(applyRuntime.containerPort === runtime.containerPort, `${item.serviceId} artifact-registry runtime port must come from deploy.json`, applyRuntime);
assertCondition(applyRuntime.healthPath === runtime.healthPath, `${item.serviceId} artifact-registry runtime healthPath must come from deploy.json`, applyRuntime);
assertCondition(applyRuntimeMemory.request === memory.request && applyRuntimeMemory.limit === memory.limit, `${item.serviceId} artifact-registry memory must come from deploy.json`, applyRuntimeMemory);
assertCondition(applyRuntimeHealth.deployMetadataRequired === health.deployMetadataRequired, `${item.serviceId} artifact-registry deploy metadata requirement must come from deploy.json`, applyRuntimeHealth);
const implicitPlan = asRecord(await runArtifactRegistryCommand([
"deploy-service",
"--env",
"dev",
"--service",
item.serviceId,
"--commit",
commit,
"--dry-run",
]), `${item.serviceId} implicit deploy.json dry-run result`);
assertCondition(asRecord(implicitPlan.registry, `${item.serviceId} implicit registry`).repository === artifact.repository, `${item.serviceId} implicit artifact-registry dry-run must read deploy.json from file`, implicitPlan);
assertCondition(asRecord(implicitPlan.driftCheck, `${item.serviceId} implicit driftCheck`).ok === true, `${item.serviceId} implicit artifact-registry dry-run must drift-check deploy.json`, implicitPlan);
const manifestMirror = k3sManifestExecutorMirror(service);
assertCondition(manifestMirror !== null, `${item.serviceId} deploy.json contract should locate a k8s manifest mirror`);
const cleanDrifts = compareDeployJsonExecutorMirrors(service, "dev", [manifestMirror!]);
assertCondition(cleanDrifts.length === 0, `${item.serviceId} current k8s manifest mirror should match deploy.json contract`, cleanDrifts);
const driftMirror: DeployJsonExecutorMirror = {
...manifestMirror!,
runtime: {
...manifestMirror!.runtime,
containerPort: item.driftPort,
memory: {
...manifestMirror!.runtime?.memory,
limit: "384Mi",
},
health: {
...manifestMirror!.runtime?.health,
deployMetadataRequired: false,
},
},
};
const drift = compareDeployJsonExecutorMirrors(service, "dev", [driftMirror]);
const driftResult = deployJsonDriftResult(service, "dev", drift);
const driftPayload = asRecord(driftResult.drift, `${item.serviceId} drift result payload`);
const driftItems = asArray(driftPayload.items, `${item.serviceId} drift result items`).map((entry) => asRecord(entry, "drift item"));
assertCondition(driftResult.ok === false, `${item.serviceId} drift result must be non-ok`, driftResult);
assertCondition(driftResult.error === "deploy-json-drift", `${item.serviceId} drift result should use structured deploy-json-drift error`, driftResult);
assertCondition(driftItems.some((entry) => entry.field === "runtime.containerPort" && entry.expected === runtime.containerPort && entry.actual === item.driftPort), `${item.serviceId} drift result should report port mismatch`, driftItems);
assertCondition(driftItems.some((entry) => entry.field === "runtime.memory.limit" && entry.expected === "512Mi" && entry.actual === "384Mi"), `${item.serviceId} drift result should report memory mismatch`, driftItems);
assertCondition(driftItems.some((entry) => entry.field === "runtime.health.deployMetadataRequired" && entry.expected === true && entry.actual === false), `${item.serviceId} drift result should report deploy metadata requirement mismatch`, driftItems);
verifiedServices.push(item.serviceId);
}
process.stdout.write(`${JSON.stringify({
ok: true,
checks: [
"deploy executor passes the deploy.json service contract to artifact-registry dry-run",
"artifact-registry dry-run reads mdtodo image, target, port and memory fields from deploy.json",
"k8s manifest target/port/memory are treated as derived mirrors and checked for drift",
"artifact-registry dry-run reads k3s user-service image, target, port, memory and deploy metadata fields from deploy.json",
"k8s manifest target/port/memory/deploy metadata are treated as derived mirrors and checked for drift",
"drift preflight returns structured deploy-json-drift with field-level expected/actual values",
],
serviceId: service.id,
commit,
sourceOfTruth,
services: verifiedServices,
}, null, 2)}\n`);
+4 -1
View File
@@ -1396,7 +1396,7 @@ function deployRefFor(options: ArtifactRegistryOptions, spec: ArtifactConsumerSp
function deployJsonServiceForOptions(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, environment: ArtifactDeployEnvironment): DeployJsonServiceContract | null {
if (!options.dryRun) return null;
if (options.deployJsonService !== null) return options.deployJsonService;
if (environment === "dev" && spec.serviceId === "mdtodo") {
if (environment === "dev" && (spec.serviceId === "decision-center" || spec.serviceId === "mdtodo")) {
return readDeployJsonServiceContractFromFile(environment, spec.serviceId);
}
return null;
@@ -1444,6 +1444,9 @@ function artifactRegistryDeployJsonMirrors(
containerPort: target.k3s.servicePort,
servicePort: target.k3s.servicePort,
healthPath: target.k3s.healthPath,
health: options.deployJsonService?.runtime?.health === undefined ? undefined : {
deployMetadataRequired: true,
},
},
},
];
+14
View File
@@ -78,6 +78,9 @@ export interface DeployJsonExecutorMirror {
request?: string;
limit?: string;
};
health?: {
deployMetadataRequired?: boolean;
};
};
}
@@ -295,6 +298,9 @@ export function compareDeployJsonExecutorMirrors(
pushDrift(items, mirror, "runtime.healthPath", service.runtime.healthPath, mirror.runtime.healthPath);
pushDrift(items, mirror, "runtime.memory.request", service.runtime.memory.request, mirror.runtime.memory?.request);
pushDrift(items, mirror, "runtime.memory.limit", service.runtime.memory.limit, mirror.runtime.memory?.limit);
if (service.runtime.health !== undefined) {
pushDrift(items, mirror, "runtime.health.deployMetadataRequired", service.runtime.health.deployMetadataRequired, mirror.runtime.health?.deployMetadataRequired);
}
}
}
return items;
@@ -366,6 +372,14 @@ export function k3sManifestExecutorMirror(service: DeployJsonServiceContract): D
request: firstRegex(container, /^\s+requests:\s*$[\s\S]*?^\s+memory:\s*([^\s#"]+)\s*$/mu),
limit: firstRegex(container, /^\s+limits:\s*$[\s\S]*?^\s+memory:\s*([^\s#"]+)\s*$/mu),
},
health: {
deployMetadataRequired: [
"UNIDESK_DEPLOY_SERVICE_ID",
"UNIDESK_DEPLOY_REPO",
"UNIDESK_DEPLOY_COMMIT",
"UNIDESK_DEPLOY_REQUESTED_COMMIT",
].every((name) => container.includes(`name: ${name}`)),
},
},
};
}
+3
View File
@@ -1042,6 +1042,9 @@ function deployJsonExecutorMirrors(
containerPort: serviceConfig.backend.nodePort,
servicePort: serviceConfig.backend.nodePort,
healthPath: serviceConfig.backend.healthPath,
health: service.runtime?.health === undefined ? undefined : {
deployMetadataRequired: true,
},
},
});
const manifestMirror = k3sManifestExecutorMirror(service);