Files
pikasTech-unidesk/scripts/todo-note-artifact-runtime-proof-contract-test.ts
T
2026-05-21 13:32:01 +00:00

141 lines
7.7 KiB
TypeScript

import { readFileSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { rootPath } from "./src/config";
import { runArtifactRegistryCommand } from "./src/artifact-registry";
type JsonRecord = Record<string, unknown>;
const serviceId = "todo-note";
const commit = "a14ce0eb855a685fa17b47adacd54623e72cd2ff";
const sourceRepo = "https://gitee.com/Lyon1998/todo_note";
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 asArray(value: unknown, label: string): unknown[] {
assertCondition(Array.isArray(value), `${label} must be an array`, value);
return value as unknown[];
}
function runDeployApplyDryRun(): JsonRecord {
const result = spawnSync("bun", ["scripts/cli.ts", "deploy", "apply", "--env", "prod", "--service", serviceId, "--dry-run"], {
cwd: process.cwd(),
encoding: "utf8",
maxBuffer: 8 * 1024 * 1024,
});
assertCondition(result.status === 0, "deploy apply dry-run should exit 0", {
status: result.status,
stdout: result.stdout.slice(-2000),
stderr: result.stderr.slice(-2000),
});
const envelope = asRecord(JSON.parse(result.stdout) as unknown, "deploy apply envelope");
assertCondition(envelope.ok === true, "deploy apply dry-run envelope should be ok", envelope);
const data = asRecord(envelope.data, "deploy apply data");
const results = asArray(data.results, "deploy apply results");
assertCondition(results.length === 1, "deploy apply dry-run should include one result", data);
return asRecord(results[0], "todo-note deploy apply result");
}
function assertTodoNoteComposeYaml(): void {
const compose = readFileSync(rootPath("docker-compose.yml"), "utf8");
const todoNoteSection = compose.slice(compose.indexOf(" todo-note:"), compose.indexOf(" oa-event-flow:"));
assertCondition(todoNoteSection.includes("image: todo-note"), "todo-note Compose service must name a stable image", todoNoteSection);
for (const expected of [
"unidesk.ai/deploy-service-id",
"unidesk.ai/deploy-ref",
"unidesk.ai/deploy-repo",
"unidesk.ai/deploy-commit",
"unidesk.ai/deploy-requested-commit",
"UNIDESK_DEPLOY_COMMIT",
"UNIDESK_DEPLOY_REQUESTED_COMMIT",
]) {
assertCondition(todoNoteSection.includes(expected), `todo-note Compose section must include ${expected}`, todoNoteSection);
}
}
function assertPlan(plan: JsonRecord, label: string): void {
const target = asRecord(plan.target, `${label} target`);
const source = asRecord(plan.source, `${label} source`);
const build = asRecord(plan.build, `${label} build`);
const labels = asRecord(plan.requiredLabels, `${label} requiredLabels`);
const validation = asArray(plan.validation, `${label} validation`).map(String);
const runtimeProof = asRecord(plan.runtimeProof, `${label} runtimeProof`);
const sources = asArray(runtimeProof.sources, `${label} runtimeProof.sources`).map(String);
const requiredEnvKeys = asArray(runtimeProof.requiredEnvKeys, `${label} runtimeProof.requiredEnvKeys`).map(String);
assertCondition(plan.ok === true && plan.supported === true, `${label} plan must be supported`, plan);
assertCondition(plan.dryRun === true && plan.mutation === false, `${label} plan must be non-mutating`, plan);
assertCondition(plan.environment === "prod", `${label} environment mismatch`, plan);
assertCondition(plan.serviceId === serviceId, `${label} service id mismatch`, plan);
assertCondition(plan.commit === commit, `${label} commit mismatch`, plan);
assertCondition(plan.sourceRepo === sourceRepo, `${label} source repo mismatch`, plan);
assertCondition(source.repo === sourceRepo && source.commit === commit && source.dockerfile === "Dockerfile", `${label} source mismatch`, source);
assertCondition(build.willCompile === false, `${label} dry-run must not compile`, build);
assertCondition(build.willRunDockerBuild === false, `${label} dry-run must not build Docker images`, build);
assertCondition(build.willRunDockerComposeBuild === false, `${label} dry-run must not run docker compose build`, build);
assertCondition(target.kind === "compose", `${label} target kind mismatch`, target);
assertCondition(target.runtimeHost === "main-server", `${label} runtime host mismatch`, target);
assertCondition(target.composeService === serviceId, `${label} compose service mismatch`, target);
assertCondition(target.containerName === "todo-note-backend", `${label} container mismatch`, target);
assertCondition(target.targetImage === "todo-note", `${label} target image mismatch`, target);
assertCondition(target.runtimeImage === `todo-note:${commit}`, `${label} runtime image mismatch`, target);
assertCondition(target.deployCommandShape === "docker compose up -d --no-build --no-deps --force-recreate todo-note", `${label} command shape mismatch`, target);
assertCondition(labels["unidesk.ai/service-id"] === serviceId, `${label} service label mismatch`, labels);
assertCondition(labels["unidesk.ai/source-repo"] === sourceRepo, `${label} source repo label mismatch`, labels);
assertCondition(labels["unidesk.ai/source-commit"] === commit, `${label} source commit label mismatch`, labels);
assertCondition(labels["unidesk.ai/dockerfile"] === "Dockerfile", `${label} dockerfile label mismatch`, labels);
assertCondition(runtimeProof.kind === "compose-container-runtime-metadata", `${label} runtime proof kind mismatch`, runtimeProof);
assertCondition(runtimeProof.sourceDirectoryUsed === false, `${label} runtime proof must not use source directory guesses`, runtimeProof);
for (const expected of ["service-health", "container-env", "container-labels", "image-labels"]) {
assertCondition(sources.includes(expected), `${label} runtime proof sources should include ${expected}`, runtimeProof);
}
for (const expected of [
"UNIDESK_DEPLOY_COMMIT",
"UNIDESK_DEPLOY_REQUESTED_COMMIT",
"UNIDESK_TODO_NOTE_DEPLOY_COMMIT",
"UNIDESK_TODO_NOTE_DEPLOY_REQUESTED_COMMIT",
]) {
assertCondition(requiredEnvKeys.includes(expected), `${label} runtime proof env keys should include ${expected}`, runtimeProof);
}
assertCondition(validation.some((line) => line.includes("Compose container runtime metadata") && line.includes("not source directory guesses")), `${label} validation must name runtime metadata proof`, validation);
assertCondition(!JSON.stringify(plan).includes("/root/todo_note"), `${label} dry-run must not rely on the todo-note source directory`, plan);
assertCondition(!JSON.stringify(plan).includes("docker compose build"), `${label} dry-run must not mention docker compose build`, plan);
}
async function main(): Promise<void> {
assertTodoNoteComposeYaml();
assertPlan(runDeployApplyDryRun(), "deploy apply");
const artifactPlan = asRecord(await runArtifactRegistryCommand([
"deploy-service",
"--env",
"prod",
"--service",
serviceId,
"--commit",
commit,
"--dry-run",
]), "artifact-registry dry-run");
assertPlan(artifactPlan, "artifact-registry");
process.stdout.write(`${JSON.stringify({
ok: true,
checks: [
"todo-note Compose service names the stable artifact image and deploy labels",
"deploy apply prod dry-run is a no-build/no-deps main-server Compose artifact consumer",
"artifact-registry prod dry-run requires source repo/source commit/Dockerfile image labels",
"runtime proof uses container env, container labels, image labels and health output",
"runtime proof does not infer commit from /root/todo_note or any source directory",
],
}, null, 2)}\n`);
}
if (import.meta.main) {
await main();
}