test: add todo-note runtime artifact proof

This commit is contained in:
Codex
2026-05-21 13:32:01 +00:00
parent fc87e680e4
commit 5cb5cc1b43
7 changed files with 292 additions and 7 deletions
+7
View File
@@ -166,10 +166,17 @@ services:
retries: 20
todo-note:
image: todo-note
build:
context: /root/todo_note
dockerfile: Dockerfile
container_name: todo-note-backend
labels:
unidesk.ai/deploy-service-id: "${UNIDESK_TODO_NOTE_DEPLOY_SERVICE_ID:-todo-note}"
unidesk.ai/deploy-ref: "${UNIDESK_TODO_NOTE_DEPLOY_REF:-deploy.json#environments.prod.services.todo-note}"
unidesk.ai/deploy-repo: "${UNIDESK_TODO_NOTE_DEPLOY_REPO:-}"
unidesk.ai/deploy-commit: "${UNIDESK_TODO_NOTE_DEPLOY_COMMIT:-}"
unidesk.ai/deploy-requested-commit: "${UNIDESK_TODO_NOTE_DEPLOY_REQUESTED_COMMIT:-}"
restart: unless-stopped
depends_on:
- database
@@ -167,8 +167,18 @@ for (const item of serviceCases) {
}
assertCondition(validation.some((line) => line.includes("registry /v2 manifest")), `${item.serviceId} must plan registry manifest validation`, validation);
assertCondition(validation.some((line) => line.includes("image labels match service id, source commit, and Dockerfile")), `${item.serviceId} must plan image label validation`, validation);
assertCondition(validation.some((line) => line.includes("health probe succeeds") && line.includes("deploy.commit/deploy.requestedCommit")), `${item.serviceId} must plan health commit validation`, validation);
assertCondition(validation.some((line) => line.includes("image labels match service id, source repo, source commit, and Dockerfile")), `${item.serviceId} must plan image label validation`, validation);
if (item.serviceId === "todo-note") {
const runtimeProof = asRecord(plan.runtimeProof, "todo-note runtimeProof");
const requiredEnvKeys = asArray(runtimeProof.requiredEnvKeys, "todo-note runtime proof env keys").map(String);
assertCondition(runtimeProof.kind === "compose-container-runtime-metadata", "todo-note runtime proof kind mismatch", runtimeProof);
assertCondition(runtimeProof.sourceDirectoryUsed === false, "todo-note runtime proof must not use source directory guesses", runtimeProof);
assertCondition(requiredEnvKeys.includes("UNIDESK_DEPLOY_COMMIT"), "todo-note runtime proof should use generic deploy commit env", runtimeProof);
assertCondition(requiredEnvKeys.includes("UNIDESK_TODO_NOTE_DEPLOY_COMMIT"), "todo-note runtime proof should use service deploy commit env", runtimeProof);
assertCondition(validation.some((line) => line.includes("Compose container runtime metadata") && line.includes("not source directory guesses")), "todo-note must plan synthetic runtime proof", validation);
} else {
assertCondition(validation.some((line) => line.includes("health probe succeeds") && line.includes("deploy.commit/deploy.requestedCommit")), `${item.serviceId} must plan health commit validation`, validation);
}
assertCondition(!JSON.stringify(plan).includes("docker compose build"), `${item.serviceId} dry-run must not include compose build`, plan);
assertCondition(!JSON.stringify(plan).includes("server rebuild"), `${item.serviceId} dry-run must not include server rebuild`, plan);
@@ -156,7 +156,9 @@ function assertCommonDryRun(plan: JsonRecord, contract: ServiceContract, environ
assertCondition(plan.providerId === "D601", `${contract.serviceId} ${environment} provider mismatch`, plan);
assertCondition(plan.serviceId === contract.serviceId, `${contract.serviceId} ${environment} service id mismatch`, plan);
assertCondition(plan.commit === contract.desiredCommit, `${contract.serviceId} ${environment} commit mismatch`, plan);
assertCondition(plan.deployRef === `deploy.json#environments.${environment}.services.${contract.serviceId}`, `${contract.serviceId} ${environment} deployRef mismatch`, plan);
const deployRef = `deploy.json#environments.${environment}.services.${contract.serviceId}`;
const allowedDeployRefs = [deployRef, `origin/master:${deployRef}`];
assertCondition(allowedDeployRefs.includes(String(plan.deployRef)), `${contract.serviceId} ${environment} deployRef mismatch`, plan);
assertCondition(plan.sourceImage === `127.0.0.1:5000/${contract.registryRepository}:${contract.desiredCommit}`, `${contract.serviceId} ${environment} source image mismatch`, plan);
assertCondition(source.repo === contract.sourceRepo, `${contract.serviceId} ${environment} source repo mismatch`, source);
assertCondition(source.commit === contract.desiredCommit, `${contract.serviceId} ${environment} source commit mismatch`, source);
@@ -167,6 +169,7 @@ function assertCommonDryRun(plan: JsonRecord, contract: ServiceContract, environ
assertCondition(String(registry.digestSource ?? "").includes("live apply must read this digest"), `${contract.serviceId} ${environment} digest source mismatch`, registry);
assertCondition(registryProbe.method === "HEAD", `${contract.serviceId} ${environment} registry probe must be HEAD`, registryProbe);
assertCondition(labels["unidesk.ai/service-id"] === contract.serviceId, `${contract.serviceId} ${environment} service label mismatch`, labels);
assertCondition(labels["unidesk.ai/source-repo"] === contract.sourceRepo, `${contract.serviceId} ${environment} source repo label mismatch`, labels);
assertCondition(labels["unidesk.ai/source-commit"] === contract.desiredCommit, `${contract.serviceId} ${environment} commit label mismatch`, labels);
assertCondition(labels["unidesk.ai/dockerfile"] === contract.dockerfile, `${contract.serviceId} ${environment} Dockerfile label mismatch`, labels);
assertCondition(build.willCompile === false, `${contract.serviceId} ${environment} CD must not compile`, build);
@@ -198,7 +201,13 @@ function assertComposeDryRun(plan: JsonRecord, contract: ServiceContract, enviro
assertCondition(target.composeService === "todo-note", `${contract.serviceId} ${environment} compose service mismatch`, target);
assertCondition(target.containerName === "todo-note-backend", `${contract.serviceId} ${environment} container mismatch`, target);
assertCondition(target.deployCommandShape === "docker compose up -d --no-build --no-deps --force-recreate todo-note", `${contract.serviceId} ${environment} command shape mismatch`, target);
assertCondition(validation.some((line) => line.includes("deploy.commit/deploy.requestedCommit")), `${contract.serviceId} ${environment} validation must require health deploy metadata`, validation);
const runtimeProof = asRecord(plan.runtimeProof, `${contract.serviceId} ${environment} runtimeProof`);
const requiredEnvKeys = asArray(runtimeProof.requiredEnvKeys, `${contract.serviceId} ${environment} runtime proof env keys`).map(String);
assertCondition(runtimeProof.kind === "compose-container-runtime-metadata", `${contract.serviceId} ${environment} runtime proof kind mismatch`, runtimeProof);
assertCondition(runtimeProof.sourceDirectoryUsed === false, `${contract.serviceId} ${environment} runtime proof must not use source directory guesses`, runtimeProof);
assertCondition(requiredEnvKeys.includes("UNIDESK_DEPLOY_COMMIT"), `${contract.serviceId} ${environment} runtime proof must use generic commit env`, runtimeProof);
assertCondition(requiredEnvKeys.includes("UNIDESK_TODO_NOTE_DEPLOY_COMMIT"), `${contract.serviceId} ${environment} runtime proof must use service commit env`, runtimeProof);
assertCondition(validation.some((line) => line.includes("deploy.commit/deploy.requestedCommit") && line.includes("Compose container runtime metadata")), `${contract.serviceId} ${environment} validation must require synthetic health deploy metadata`, validation);
}
async function assertDryRuns(): Promise<void> {
@@ -146,6 +146,7 @@ function assertCommonContract(result: JsonRecord, item: ServiceCase): void {
assertCondition(build.willRunDockerComposeBuild === false, `${item.serviceId} dry-run must not run docker compose build`, build);
assertCondition(build.producerBoundary === "ci publish-user-service", `${item.serviceId} producer boundary mismatch`, build);
assertCondition(labels["unidesk.ai/service-id"] === item.serviceId, `${item.serviceId} service label mismatch`, labels);
assertCondition(labels["unidesk.ai/source-repo"] === item.sourceRepo, `${item.serviceId} source repo label mismatch`, labels);
assertCondition(labels["unidesk.ai/source-commit"] === item.commit, `${item.serviceId} commit label mismatch`, labels);
assertCondition(labels["unidesk.ai/dockerfile"] === item.dockerfile, `${item.serviceId} dockerfile label mismatch`, labels);
assertCondition(registryProbe.method === "HEAD", `${item.serviceId} registry probe must be HEAD`, registryProbe);
@@ -160,6 +161,14 @@ function assertCommonContract(result: JsonRecord, item: ServiceCase): void {
for (const snippet of item.expectedValidationSnippets) {
assertCondition(validation.some((line) => line.includes(snippet)), `${item.serviceId} validation should include ${snippet}`, validation);
}
if (item.serviceId === "todo-note") {
const runtimeProof = asRecord(result.runtimeProof, "todo-note runtimeProof");
const requiredEnvKeys = asArray(runtimeProof.requiredEnvKeys, "todo-note runtime proof env keys").map(String);
assertCondition(runtimeProof.kind === "compose-container-runtime-metadata", "todo-note runtime proof kind mismatch", runtimeProof);
assertCondition(runtimeProof.sourceDirectoryUsed === false, "todo-note runtime proof must not use source directory guesses", runtimeProof);
assertCondition(requiredEnvKeys.includes("UNIDESK_DEPLOY_REQUESTED_COMMIT"), "todo-note proof should use generic requested commit env", runtimeProof);
assertCondition(requiredEnvKeys.includes("UNIDESK_TODO_NOTE_DEPLOY_REQUESTED_COMMIT"), "todo-note proof should use service requested commit env", runtimeProof);
}
}
const cases: ServiceCase[] = [
@@ -214,7 +223,8 @@ const cases: ServiceCase[] = [
expectedValidationSnippets: [
"D601 registry /v2 manifest exists for the commit tag",
"running Compose container image label matches the requested commit",
"todo-note runtime health probe succeeds and reports deploy.commit/deploy.requestedCommit matching the artifact commit",
"todo-note runtime health proof synthesizes deploy.commit/deploy.requestedCommit from Compose container runtime metadata",
"not source directory guesses",
],
rollbackType: "compose-retag-recreate",
},
+108 -2
View File
@@ -159,6 +159,7 @@ interface ArtifactConsumerTarget {
projectHint?: string;
requiredRuntimeSecrets?: RuntimeSecretRequirement[];
authHealthGate?: AuthHealthGate;
syntheticHealthDeployProof?: SyntheticHealthDeployProof;
};
k3s?: {
namespace: string;
@@ -184,6 +185,11 @@ interface AuthHealthGate {
recoveryHint: string;
}
interface SyntheticHealthDeployProof {
kind: "compose-container-runtime-metadata";
description: string;
}
interface ComposeArtifactRuntime {
workDir: string;
composeFile: string;
@@ -230,8 +236,13 @@ export interface RuntimeSecretContract {
dryRunDisposition: "not-required" | "ready-for-live-apply" | "secret-source-blocked";
}
const todoNoteSyntheticHealthDeployProof: SyntheticHealthDeployProof = {
kind: "compose-container-runtime-metadata",
description: "todo-note /api/health does not natively expose deploy metadata, so the artifact consumer synthesizes health.deploy from the recreated container's UNIDESK_DEPLOY_* env and verifies it against image/container labels.",
};
function todoNoteHealthProbeCommand(): string {
return "bun -e \"fetch('http://127.0.0.1:4211/api/health').then(async r=>{const text=await r.text(); let body; try{body=JSON.parse(text)}catch{body={ok:r.ok,raw:text}}; const deploy=body.deploy&&typeof body.deploy==='object'&&!Array.isArray(body.deploy)?body.deploy:{}; body.deploy={...deploy,serviceId:deploy.serviceId||process.env.UNIDESK_DEPLOY_SERVICE_ID||'todo-note',ref:deploy.ref||process.env.UNIDESK_DEPLOY_REF||'',repo:deploy.repo||process.env.UNIDESK_DEPLOY_REPO||'',commit:deploy.commit||process.env.UNIDESK_DEPLOY_COMMIT||'',requestedCommit:deploy.requestedCommit||process.env.UNIDESK_DEPLOY_REQUESTED_COMMIT||''}; console.log(JSON.stringify(body)); process.exit(r.ok?0:1)}).catch(e=>{console.error(e); process.exit(1)})\"";
return "bun -e \"fetch('http://127.0.0.1:4211/api/health').then(async r=>{const text=await r.text(); let body; try{body=JSON.parse(text)}catch{body={ok:r.ok,raw:text}}; console.log(JSON.stringify(body)); process.exit(r.ok?0:1)}).catch(e=>{console.error(e); process.exit(1)})\"";
}
export const baiduNetdiskRuntimeSecretRequirements: RuntimeSecretRequirement[] = [
@@ -777,6 +788,7 @@ const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
deployEnvPrefix: "UNIDESK_TODO_NOTE_DEPLOY",
healthProbeCommand: todoNoteHealthProbeCommand(),
requireHealthCommit: true,
syntheticHealthDeployProof: todoNoteSyntheticHealthDeployProof,
},
},
prod: {
@@ -789,6 +801,7 @@ const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
deployEnvPrefix: "UNIDESK_TODO_NOTE_DEPLOY",
healthProbeCommand: todoNoteHealthProbeCommand(),
requireHealthCommit: true,
syntheticHealthDeployProof: todoNoteSyntheticHealthDeployProof,
},
},
},
@@ -2166,6 +2179,70 @@ function composeArtifactEnvValues(spec: ArtifactConsumerSpec, target: ArtifactCo
};
}
function syntheticComposeHealthDeployScript(compose: NonNullable<ArtifactConsumerTarget["compose"]>, commit: string, sourceRepo: string): string[] {
if (compose.syntheticHealthDeployProof === undefined) return [];
const prefix = compose.deployEnvPrefix;
const envKeys = [
"UNIDESK_DEPLOY_SERVICE_ID",
"UNIDESK_DEPLOY_REF",
"UNIDESK_DEPLOY_REPO",
"UNIDESK_DEPLOY_COMMIT",
"UNIDESK_DEPLOY_REQUESTED_COMMIT",
`${prefix}_SERVICE_ID`,
`${prefix}_REF`,
`${prefix}_REPO`,
`${prefix}_COMMIT`,
`${prefix}_REQUESTED_COMMIT`,
];
const envKeysJson = JSON.stringify(envKeys);
return [
"runtime_metadata_json=$(docker inspect \"$cid\" --format '{{json .Config.Env}}')",
"container_label_commit=$(docker inspect -f '{{ index .Config.Labels \"unidesk.ai/deploy-commit\" }}' \"$cid\")",
"container_label_requested_commit=$(docker inspect -f '{{ index .Config.Labels \"unidesk.ai/deploy-requested-commit\" }}' \"$cid\")",
"container_label_repo=$(docker inspect -f '{{ index .Config.Labels \"unidesk.ai/deploy-repo\" }}' \"$cid\")",
"container_label_ref=$(docker inspect -f '{{ index .Config.Labels \"unidesk.ai/deploy-ref\" }}' \"$cid\")",
`python3 - "$health_json" "$runtime_metadata_json" ${shellQuote(envKeysJson)} ${shellQuote(prefix)} ${shellQuote(commit)} ${shellQuote(sourceRepo)} "$actual_commit" "$actual_service" "$actual_repo" "$container_label_commit" "$container_label_requested_commit" "$container_label_repo" "$container_label_ref" <<'PY'`,
"import json, sys",
"path, env_json, keys_json, prefix, expected_commit, expected_repo, image_commit, image_service, image_repo, label_commit, label_requested_commit, label_repo, label_ref = sys.argv[1:]",
"body = json.load(open(path, encoding='utf-8'))",
"env_items = json.loads(env_json)",
"allow = set(json.loads(keys_json))",
"env = {}",
"for item in env_items:",
" if not isinstance(item, str) or '=' not in item:",
" continue",
" key, value = item.split('=', 1)",
" if key in allow:",
" env[key] = value",
"deploy = body.get('deploy') if isinstance(body, dict) and isinstance(body.get('deploy'), dict) else {}",
"synthetic = {",
" 'serviceId': deploy.get('serviceId') or env.get('UNIDESK_DEPLOY_SERVICE_ID') or env.get(prefix + '_SERVICE_ID') or image_service,",
" 'ref': deploy.get('ref') or env.get('UNIDESK_DEPLOY_REF') or env.get(prefix + '_REF') or label_ref,",
" 'repo': deploy.get('repo') or env.get('UNIDESK_DEPLOY_REPO') or env.get(prefix + '_REPO') or label_repo or image_repo,",
" 'commit': deploy.get('commit') or env.get('UNIDESK_DEPLOY_COMMIT') or env.get(prefix + '_COMMIT') or label_commit or image_commit,",
" 'requestedCommit': deploy.get('requestedCommit') or env.get('UNIDESK_DEPLOY_REQUESTED_COMMIT') or env.get(prefix + '_REQUESTED_COMMIT') or label_requested_commit,",
"}",
"body['deploy'] = synthetic",
"body['deployProof'] = {",
" 'kind': 'compose-container-runtime-metadata',",
" 'sources': ['service-health', 'container-env', 'container-labels', 'image-labels'],",
" 'sourceDirectoryUsed': False,",
" 'envKeysUsed': sorted([key for key in env if key.startswith('UNIDESK_DEPLOY_') or key.startswith(prefix + '_')]),",
"}",
"if synthetic.get('commit') != expected_commit or synthetic.get('requestedCommit') != expected_commit:",
" raise SystemExit('synthetic deploy commit mismatch: ' + json.dumps(synthetic, sort_keys=True))",
"if synthetic.get('repo') != expected_repo:",
" raise SystemExit('synthetic deploy repo mismatch: ' + json.dumps(synthetic, sort_keys=True))",
"if synthetic.get('serviceId') != image_service:",
" raise SystemExit('synthetic deploy service mismatch: ' + json.dumps(synthetic, sort_keys=True))",
"with open(path, 'w', encoding='utf-8') as handle:",
" json.dump(body, handle, separators=(',', ':'))",
" handle.write('\\n')",
"print('synthetic_health_deploy_proof=compose-container-runtime-metadata sourceDirectoryUsed=false')",
"PY",
];
}
async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, target: ArtifactConsumerTarget): Promise<Record<string, unknown>> {
const commit = options.commit;
if (commit === null) throw new Error("artifact-registry deploy-service requires --commit <full-sha>");
@@ -2202,6 +2279,7 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
return { ok: false, serviceId: spec.serviceId, error: "D601 artifact registry is not healthy", health };
}
const sourceImage = artifactImageRef(options, spec, commit);
const sourceRepo = sourceRepoFor(options, spec);
const composeImage = options.targetImage ?? target.targetImage;
const commitImage = `${composeImage}:${commit}`;
const registryProbe = runRemoteScript(options, registryArtifactProbeScript(options, spec, commit), Math.max(options.timeoutMs, 120_000));
@@ -2227,7 +2305,7 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
};
}
const localLoadedImage = sourceImage;
const labelFailure = verifyLocalArtifactLabels(localLoadedImage, spec, commit, sourceRepoFor(options, spec));
const labelFailure = verifyLocalArtifactLabels(localLoadedImage, spec, commit, sourceRepo);
if (labelFailure !== null) return { ...labelFailure, registryProbe: commandTail(registryProbe) };
const tag = runCommand(["docker", "tag", localLoadedImage, composeImage], repoRoot);
if (tag.exitCode !== 0 || tag.timedOut) {
@@ -2265,9 +2343,14 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
`cid=$(docker ps -q --filter label=com.docker.compose.project=${shellQuote(composeProject)} --filter label=com.docker.compose.service=${shellQuote(serviceName)} --filter label=com.docker.compose.oneoff=False | head -1)`,
"image_id=$(docker inspect -f '{{.Image}}' \"$cid\")",
"actual_commit=$(docker image inspect -f '{{ index .Config.Labels \"unidesk.ai/source-commit\" }}' \"$image_id\")",
"actual_service=$(docker image inspect -f '{{ index .Config.Labels \"unidesk.ai/service-id\" }}' \"$image_id\")",
"actual_repo=$(docker image inspect -f '{{ index .Config.Labels \"unidesk.ai/source-repo\" }}' \"$image_id\")",
`test "$actual_commit" = ${shellQuote(commit)}`,
`test "$actual_service" = ${shellQuote(spec.serviceId)}`,
`test "$actual_repo" = ${shellQuote(sourceRepo)}`,
`health_json=/tmp/unidesk-${safeName(spec.serviceId)}-health.json`,
`docker exec "$cid" sh -lc ${shellQuote(target.compose.healthProbeCommand)} > "$health_json"`,
...syntheticComposeHealthDeployScript(target.compose, commit, sourceRepo),
"cat \"$health_json\"",
...authHealthGateScript(target.compose.authHealthGate, "\"$health_json\""),
...(target.compose.requireHealthCommit ? [
@@ -2312,6 +2395,7 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
imageLabelCommit: commit,
serviceHealthCommit: target.compose.requireHealthCommit ? commit : "not-required",
serviceHealthRequestedCommit: target.compose.requireHealthCommit ? commit : "not-required",
runtimeProof: target.compose.syntheticHealthDeployProof === undefined ? "native-health" : target.compose.syntheticHealthDeployProof,
requiredRuntimeSecrets: secretPreflight.requirements,
runtimeSecrets,
authHealthGate: target.compose.authHealthGate === undefined ? "not-required" : {
@@ -2652,6 +2736,8 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
...(spec.kind === "d601-compose" ? ["running Compose container image label matches the requested commit"] : []),
verificationBlocked
? `blocked: ${spec.runtimeVerificationBlockReason}`
: target.compose.requireHealthCommit && target.compose.syntheticHealthDeployProof !== undefined
? `${spec.serviceId} runtime health proof synthesizes deploy.commit/deploy.requestedCommit from Compose container runtime metadata and verifies it against image/container labels, not source directory guesses`
: target.compose.requireHealthCommit
? `${spec.serviceId} runtime health probe succeeds and reports deploy.commit/deploy.requestedCommit matching the artifact commit`
: `${spec.serviceId} /health succeeds for the recreated container`,
@@ -2668,6 +2754,26 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
requiredAuthFields: target.compose.authHealthGate.requiredFields,
dryRunDisposition: "pending-live-check",
},
runtimeProof: target.compose.syntheticHealthDeployProof === undefined ? {
kind: "native-health-deploy-metadata",
sourceDirectoryUsed: false,
} : {
...target.compose.syntheticHealthDeployProof,
sourceDirectoryUsed: false,
sources: ["service-health", "container-env", "container-labels", "image-labels"],
requiredEnvKeys: [
"UNIDESK_DEPLOY_SERVICE_ID",
"UNIDESK_DEPLOY_REF",
"UNIDESK_DEPLOY_REPO",
"UNIDESK_DEPLOY_COMMIT",
"UNIDESK_DEPLOY_REQUESTED_COMMIT",
`${target.compose.deployEnvPrefix}_SERVICE_ID`,
`${target.compose.deployEnvPrefix}_REF`,
`${target.compose.deployEnvPrefix}_REPO`,
`${target.compose.deployEnvPrefix}_COMMIT`,
`${target.compose.deployEnvPrefix}_REQUESTED_COMMIT`,
],
},
rollback: rollbackInfo(spec, target, environment, commit),
};
}
+3
View File
@@ -1076,6 +1076,9 @@ function artifactConsumerPlanValidation(service: UniDeskMicroserviceConfig, envi
...base,
"recreates only the selected Compose service with --no-build --no-deps --force-recreate",
"verifies running image labels and private service health",
...(service.id === "todo-note" ? [
"todo-note runtime proof synthesizes health.deploy.commit/requestedCommit from Compose container env, container labels, and image labels; it does not infer commit from /root/todo_note or any source directory",
] : []),
];
}
return ["unsupported service remains blocked before source materialization or runtime mutation"];
@@ -0,0 +1,140 @@
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();
}