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; 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 { 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(); }