import { readFileSync } from "node:fs"; import { rootPath } from "./src/config"; import { runArtifactRegistryCommand } from "./src/artifact-registry"; type JsonRecord = Record; interface ServiceCase { serviceId: string; sourceRepo: string; dockerfile: string; registryRepository: string; composeService: string; containerName: string; targetImage: string; deployEnvPrefix: string; } function asRecord(value: unknown, label: string): JsonRecord { if (typeof value !== "object" || value === null || Array.isArray(value)) { throw new Error(`${label} must be an object: ${JSON.stringify(value)}`); } return value as JsonRecord; } function asArray(value: unknown, label: string): unknown[] { if (!Array.isArray(value)) throw new Error(`${label} must be an array: ${JSON.stringify(value)}`); return value; } function assertCondition(condition: unknown, message: string, detail: unknown = {}): void { if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`); } const commit = "0123456789abcdef0123456789abcdef01234567"; const serviceCases: ServiceCase[] = [ { serviceId: "project-manager", sourceRepo: "https://github.com/pikasTech/unidesk", dockerfile: "src/components/microservices/project-manager/Dockerfile", registryRepository: "unidesk/project-manager", composeService: "project-manager", containerName: "project-manager-backend", targetImage: "project-manager", deployEnvPrefix: "UNIDESK_PROJECT_MANAGER_DEPLOY", }, { serviceId: "oa-event-flow", sourceRepo: "https://github.com/pikasTech/unidesk", dockerfile: "src/components/microservices/oa-event-flow/Dockerfile", registryRepository: "unidesk/oa-event-flow", composeService: "oa-event-flow", containerName: "oa-event-flow-backend", targetImage: "oa-event-flow", deployEnvPrefix: "UNIDESK_OA_EVENT_FLOW_DEPLOY", }, { serviceId: "todo-note", sourceRepo: "https://gitee.com/Lyon1998/todo_note", dockerfile: "Dockerfile", registryRepository: "unidesk/todo-note", composeService: "todo-note", containerName: "todo-note-backend", targetImage: "todo-note", deployEnvPrefix: "UNIDESK_TODO_NOTE_DEPLOY", }, { serviceId: "baidu-netdisk", sourceRepo: "https://github.com/pikasTech/unidesk", dockerfile: "src/components/microservices/baidu-netdisk/Dockerfile", registryRepository: "unidesk/baidu-netdisk", composeService: "baidu-netdisk", containerName: "baidu-netdisk-backend", targetImage: "baidu-netdisk", deployEnvPrefix: "UNIDESK_BAIDU_NETDISK_DEPLOY", }, { serviceId: "frontend", sourceRepo: "https://github.com/pikasTech/unidesk", dockerfile: "src/components/frontend/Dockerfile", registryRepository: "unidesk/frontend", composeService: "frontend", containerName: "unidesk-frontend", targetImage: "unidesk-frontend", deployEnvPrefix: "UNIDESK_FRONTEND_DEPLOY", }, ]; const ciCatalog = asRecord(JSON.parse(readFileSync(rootPath("CI.json"), "utf8")) as unknown, "CI.json"); const artifacts = asArray(ciCatalog.artifacts, "CI.json.artifacts").map((item, index) => asRecord(item, `CI.json.artifacts[${index}]`)); const artifactByService = new Map(artifacts.map((item) => [String(item.serviceId), item])); for (const item of serviceCases) { const artifact = asRecord(artifactByService.get(item.serviceId), `CI.json artifact ${item.serviceId}`); const source = asRecord(artifact.source, `CI.json artifact ${item.serviceId}.source`); const image = asRecord(artifact.image, `CI.json artifact ${item.serviceId}.image`); assertCondition(artifact.kind === "source-build", `${item.serviceId} producer must be source-build`, artifact); assertCondition(artifact.status === "supported", `${item.serviceId} producer must be supported`, artifact); assertCondition(artifact.producer === "ci publish-user-service", `${item.serviceId} producer command must be publish-user-service`, artifact); assertCondition(source.repo === item.sourceRepo, `${item.serviceId} producer source repo mismatch`, source); assertCondition(source.dockerfile === item.dockerfile, `${item.serviceId} producer dockerfile mismatch`, source); assertCondition(image.repository === item.registryRepository, `${item.serviceId} registry repository mismatch`, image); } const plans: JsonRecord[] = []; for (const item of serviceCases) { const plan = asRecord(await runArtifactRegistryCommand([ "deploy-service", "--env", "prod", "--service", item.serviceId, "--commit", commit, "--dry-run", ]), `${item.serviceId} dry-run plan`); const target = asRecord(plan.target, `${item.serviceId} dry-run target`); const labels = asRecord(plan.requiredLabels, `${item.serviceId} dry-run labels`); const registryProbe = asRecord(plan.registryProbe, `${item.serviceId} dry-run registry probe`); const validation = asArray(plan.validation, `${item.serviceId} dry-run validation`).map(String); assertCondition(plan.ok === true, `${item.serviceId} dry-run must be ok`, plan); assertCondition(plan.dryRun === true && plan.mutation === false, `${item.serviceId} dry-run must be non-mutating`, plan); assertCondition(plan.environment === "prod", `${item.serviceId} dry-run must target prod`, plan); assertCondition(plan.serviceId === item.serviceId, `${item.serviceId} dry-run service id mismatch`, plan); assertCondition(plan.sourceImage === `127.0.0.1:5000/${item.registryRepository}:${commit}`, `${item.serviceId} source image mismatch`, plan); assertCondition(labels["unidesk.ai/service-id"] === item.serviceId, `${item.serviceId} required service label missing`, labels); assertCondition(labels["unidesk.ai/source-commit"] === commit, `${item.serviceId} required commit label missing`, labels); assertCondition(labels["unidesk.ai/dockerfile"] === item.dockerfile, `${item.serviceId} required dockerfile label missing`, labels); assertCondition(registryProbe.method === "HEAD", `${item.serviceId} dry-run must only plan registry HEAD probe`, registryProbe); assertCondition(target.kind === "compose", `${item.serviceId} target kind must be main-server Compose`, target); assertCondition(target.runtimeHost === "main-server", `${item.serviceId} runtime host must be main-server`, target); assertCondition(target.composeService === item.composeService, `${item.serviceId} compose service mismatch`, target); assertCondition(target.containerName === item.containerName, `${item.serviceId} container mismatch`, target); assertCondition(target.targetImage === item.targetImage, `${item.serviceId} stable target image mismatch`, target); assertCondition(target.runtimeImage === `${item.targetImage}:${commit}`, `${item.serviceId} runtime image mismatch`, target); assertCondition(target.deployEnvPrefix === item.deployEnvPrefix, `${item.serviceId} deploy env prefix mismatch`, target); assertCondition( target.deployCommandShape === `docker compose up -d --no-build --no-deps --force-recreate ${item.composeService}`, `${item.serviceId} dry-run must narrow Compose recreate command`, target, ); 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(!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); plans.push({ serviceId: item.serviceId, sourceImage: plan.sourceImage, composeService: target.composeService, containerName: target.containerName, deployCommandShape: target.deployCommandShape, }); } process.stdout.write(`${JSON.stringify({ ok: true, checks: [ "CI.json producer catalog covers the main-server direct artifact services", "prod dry-run plans are non-mutating and commit-pinned", "dry-run targets only the matching main-server Compose service with --no-build --no-deps --force-recreate", "dry-run plans image-label and health deploy commit validation", ], plans, }, null, 2)}\n`);