import { readFileSync } from "node:fs"; import { rootPath } from "./src/config"; import { runArtifactRegistryCommand } from "./src/artifact-registry"; type Environment = "dev" | "prod"; type ServiceId = "mdtodo" | "claudeqq" | "todo-note"; type JsonRecord = Record; type ServiceContract = { serviceId: ServiceId; desiredCommit: string; runtimeCommit: string | null; runtimeCommitSource: string; artifactExists: boolean; devStatus: string; prodStatus: string; blockedScopes: string[]; recommendedAction: string; sourceRepo: string; dockerfile: string; registryRepository: string; consumerKind: "d601-k3s" | "compose"; }; const contracts: ServiceContract[] = [ { serviceId: "mdtodo", desiredCommit: "595de3d320b73ec006794440b32db48b3ad14d2b", runtimeCommit: "75fb6757b2504ba86d61f2587fb34a9c9ed4019a", runtimeCommitSource: "prod Deployment annotations; desired artifact target was advanced because 75fb6757 predates mdtodo /health.deploy metadata", artifactExists: false, devStatus: "missing-dev-service", prodStatus: "healthy-prod-annotation-stale-after-health-metadata-repin", blockedScopes: ["registry-artifact", "dev-service", "runtime-health-metadata-proof", "prod-runtime-commit-drift"], recommendedAction: "Publish the desired artifact that includes mdtodo health deploy metadata, create/verify unidesk-dev/mdtodo-dev, then run focused dev smoke before deciding whether prod needs replacement.", sourceRepo: "https://github.com/pikasTech/unidesk", dockerfile: "src/components/microservices/mdtodo/Dockerfile", registryRepository: "unidesk/mdtodo", consumerKind: "d601-k3s", }, { serviceId: "claudeqq", desiredCommit: "203b1f46684c91340ecbbd8a74502bd55e4f2011", runtimeCommit: "203b1f46684c91340ecbbd8a74502bd55e4f2011", runtimeCommitSource: "prod /health deploy.commit and deploy.requestedCommit", artifactExists: false, devStatus: "missing-dev-service", prodStatus: "healthy-prod-health-aligned-event-api-unverified", blockedScopes: ["registry-artifact", "dev-service", "event-api-surface"], recommendedAction: "Publish the desired artifact, create/verify unidesk-dev/claudeqq-dev, then resolve or document the event API paths before prod artifact replacement.", sourceRepo: "https://gitee.com/lyon1998/agent_skills", dockerfile: "claudeqq/Dockerfile", registryRepository: "unidesk/claudeqq", consumerKind: "d601-k3s", }, { serviceId: "todo-note", desiredCommit: "a14ce0eb855a685fa17b47adacd54623e72cd2ff", runtimeCommit: null, runtimeCommitSource: "prod health and container labels do not expose source commit", artifactExists: false, devStatus: "consumer-plan-only-no-live-dev", prodStatus: "healthy-behavior-no-commit-proof", blockedScopes: ["registry-artifact", "runtime-commit-proof", "health-deploy-metadata"], recommendedAction: "Publish the desired artifact, then use the no-build Compose artifact consumer to recreate only todo-note and verify image labels plus health deploy metadata.", sourceRepo: "https://gitee.com/Lyon1998/todo_note", dockerfile: "Dockerfile", registryRepository: "unidesk/todo-note", consumerKind: "compose", }, ]; 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 strings(value: unknown, label: string): string[] { return asArray(value, label).map(String); } function manifestService(manifest: JsonRecord, environment: Environment, serviceId: ServiceId): JsonRecord { const environments = asRecord(manifest.environments, "deploy.json.environments"); const env = asRecord(environments[environment], `deploy.json.environments.${environment}`); const services = asArray(env.services, `deploy.json.environments.${environment}.services`); const service = services.map((item, index) => asRecord(item, `${environment}.services[${index}]`)).find((item) => item.id === serviceId); assertCondition(service !== undefined, `deploy.json ${environment} must include ${serviceId}`, env); return service as JsonRecord; } function artifactCatalogEntry(catalog: JsonRecord, serviceId: ServiceId): JsonRecord { const artifacts = asArray(catalog.artifacts, "CI.json.artifacts").map((item, index) => asRecord(item, `CI.json.artifacts[${index}]`)); const artifact = artifacts.find((item) => item.serviceId === serviceId); assertCondition(artifact !== undefined, `CI.json must include ${serviceId}`, catalog); return artifact as JsonRecord; } function assertDeployJson(): void { const manifest = asRecord(JSON.parse(readFileSync(rootPath("deploy.json"), "utf8")) as unknown, "deploy.json"); assertCondition(manifest.schemaVersion === 2, "deploy.json must use schemaVersion=2", manifest); for (const contract of contracts) { for (const environment of ["dev", "prod"] as const) { const service = manifestService(manifest, environment, contract.serviceId); assertCondition(service.repo === contract.sourceRepo, `${contract.serviceId} ${environment} repo mismatch`, service); assertCondition(service.commitId === contract.desiredCommit, `${contract.serviceId} ${environment} desired commit mismatch`, service); } } } function assertCiCatalog(): void { const catalog = asRecord(JSON.parse(readFileSync(rootPath("CI.json"), "utf8")) as unknown, "CI.json"); for (const contract of contracts) { const artifact = artifactCatalogEntry(catalog, contract.serviceId); const source = asRecord(artifact.source, `${contract.serviceId} CI source`); const image = asRecord(artifact.image, `${contract.serviceId} CI image`); assertCondition(artifact.kind === "source-build", `${contract.serviceId} producer must be source-build`, artifact); assertCondition(artifact.status === "supported", `${contract.serviceId} producer must be supported`, artifact); assertCondition(artifact.producer === "ci publish-user-service", `${contract.serviceId} producer command mismatch`, artifact); assertCondition(source.repo === contract.sourceRepo, `${contract.serviceId} source repo mismatch`, source); assertCondition(source.dockerfile === contract.dockerfile, `${contract.serviceId} Dockerfile mismatch`, source); assertCondition(image.repository === contract.registryRepository, `${contract.serviceId} registry repository mismatch`, image); } } function assertNormalizedStatus(): void { for (const contract of contracts) { assertCondition(/^[0-9a-f]{40}$/u.test(contract.desiredCommit), `${contract.serviceId} desiredCommit must be a full sha`, contract); assertCondition(contract.runtimeCommit === null || /^[0-9a-f]{40}$/u.test(contract.runtimeCommit), `${contract.serviceId} runtimeCommit must be a full sha or null`, contract); assertCondition(contract.artifactExists === false, `${contract.serviceId} desired artifact should remain recorded as missing`, contract); assertCondition(contract.devStatus.length > 0, `${contract.serviceId} devStatus must be structured`, contract); assertCondition(contract.prodStatus.length > 0, `${contract.serviceId} prodStatus must be structured`, contract); assertCondition(contract.blockedScopes.length > 0, `${contract.serviceId} blockedScopes must not be empty`, contract); assertCondition(contract.recommendedAction.length > 0, `${contract.serviceId} recommendedAction must not be empty`, contract); } } function assertCommonDryRun(plan: JsonRecord, contract: ServiceContract, environment: Environment): void { const source = asRecord(plan.source, `${contract.serviceId} ${environment} source`); const registry = asRecord(plan.registry, `${contract.serviceId} ${environment} registry`); const build = asRecord(plan.build, `${contract.serviceId} ${environment} build`); const labels = asRecord(plan.requiredLabels, `${contract.serviceId} ${environment} labels`); const registryProbe = asRecord(plan.registryProbe, `${contract.serviceId} ${environment} registryProbe`); assertCondition(plan.ok === true && plan.supported === true, `${contract.serviceId} ${environment} dry-run must be supported`, plan); assertCondition(plan.dryRun === true && plan.mutation === false, `${contract.serviceId} ${environment} dry-run must be non-mutating`, plan); assertCondition(plan.environment === environment, `${contract.serviceId} ${environment} environment mismatch`, plan); 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); 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); assertCondition(source.dockerfile === contract.dockerfile, `${contract.serviceId} ${environment} source dockerfile mismatch`, source); assertCondition(registry.repository === contract.registryRepository, `${contract.serviceId} ${environment} registry repository mismatch`, registry); assertCondition(registry.imageRef === plan.sourceImage, `${contract.serviceId} ${environment} registry image mismatch`, registry); assertCondition(registry.digest === null, `${contract.serviceId} ${environment} dry-run must not fake a digest`, registry); 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); assertCondition(build.willRunCargoBuild === false, `${contract.serviceId} ${environment} CD must not run cargo build`, build); assertCondition(build.willRunDockerBuild === false, `${contract.serviceId} ${environment} CD must not run docker build`, build); assertCondition(build.willRunDockerComposeBuild === false, `${contract.serviceId} ${environment} CD must not run docker compose build`, build); assertCondition(build.producerBoundary === "ci publish-user-service", `${contract.serviceId} ${environment} producer boundary mismatch`, build); assertCondition(String(plan.boundary ?? "").includes("artifact-consumer only"), `${contract.serviceId} ${environment} boundary must be artifact-only`, plan); assertCondition(String(plan.boundary ?? "").includes("never builds source"), `${contract.serviceId} ${environment} boundary must forbid source builds`, plan); } function assertK3sDryRun(plan: JsonRecord, contract: ServiceContract, environment: Environment): void { const target = asRecord(plan.target, `${contract.serviceId} ${environment} target`); const validation = strings(plan.validation, `${contract.serviceId} ${environment} validation`); const suffix = environment === "dev" ? "-dev" : ""; assertCondition(target.kind === "d601-k3s", `${contract.serviceId} ${environment} target kind mismatch`, target); assertCondition(target.namespace === (environment === "dev" ? "unidesk-dev" : "unidesk"), `${contract.serviceId} ${environment} namespace mismatch`, target); assertCondition(target.deployment === `${contract.serviceId}${suffix}`, `${contract.serviceId} ${environment} deployment mismatch`, target); assertCondition(target.service === `${contract.serviceId}${suffix}`, `${contract.serviceId} ${environment} service mismatch`, target); assertCondition(String(target.deployCommandShape ?? "").includes("kubectl set image"), `${contract.serviceId} ${environment} command must be k3s image update`, target); assertCondition(validation.some((line) => line.includes("Kubernetes API service proxy") || line.includes("service health")), `${contract.serviceId} ${environment} validation should include k3s service health`, validation); } function assertComposeDryRun(plan: JsonRecord, contract: ServiceContract, environment: Environment): void { const target = asRecord(plan.target, `${contract.serviceId} ${environment} target`); const validation = strings(plan.validation, `${contract.serviceId} ${environment} validation`); assertCondition(target.kind === "compose", `${contract.serviceId} ${environment} target kind mismatch`, target); assertCondition(target.runtimeHost === "main-server", `${contract.serviceId} ${environment} runtime host mismatch`, target); 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); 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 { for (const contract of contracts) { for (const environment of ["dev", "prod"] as const) { const plan = asRecord(await runArtifactRegistryCommand([ "deploy-service", "--env", environment, "--service", contract.serviceId, "--commit", contract.desiredCommit, "--dry-run", ]), `${contract.serviceId} ${environment} artifact dry-run`); assertCommonDryRun(plan, contract, environment); if (contract.consumerKind === "d601-k3s") assertK3sDryRun(plan, contract, environment); else assertComposeDryRun(plan, contract, environment); } } } async function main(): Promise { assertDeployJson(); assertCiCatalog(); assertNormalizedStatus(); await assertDryRuns(); process.stdout.write(`${JSON.stringify({ ok: true, checks: [ "deploy.json dev/prod desired commits match the issue #9 service matrix", "CI.json keeps mdtodo, claudeqq and todo-note on supported ci publish-user-service source-build producers", "normalized status records expose desiredCommit, runtimeCommit, artifactExists, devStatus, prodStatus, blockedScopes and recommendedAction", "dev/prod artifact-registry dry-runs are non-mutating, commit-pinned and no-build", "mdtodo and claudeqq dry-runs target D601 k3s consumers", "todo-note dry-runs target the main-server Compose consumer with no-build/no-deps recreate shape", ], services: contracts.map((contract) => ({ serviceId: contract.serviceId, desiredCommit: contract.desiredCommit, runtimeCommit: contract.runtimeCommit, runtimeCommitSource: contract.runtimeCommitSource, artifactExists: contract.artifactExists, devStatus: contract.devStatus, prodStatus: contract.prodStatus, blockedScopes: contract.blockedScopes, recommendedAction: contract.recommendedAction, })), }, null, 2)}\n`); } if (import.meta.main) { await main(); }