import { readFileSync } from "node:fs"; import { rootPath } from "./src/config"; import { runArtifactRegistryCommand } from "./src/artifact-registry"; const serviceId = "claudeqq"; const desiredCommit = "203b1f46684c91340ecbbd8a74502bd55e4f2011"; const sourceRepo = "https://gitee.com/lyon1998/agent_skills"; const dockerfile = "claudeqq/Dockerfile"; const eventPaths = ["/api/events/recent", "/api/events/subscriptions"]; type JsonRecord = Record; function assertCondition(condition: boolean, message: string): void { if (!condition) throw new Error(message); } function asRecord(value: unknown, path: string): JsonRecord { assertCondition(typeof value === "object" && value !== null && !Array.isArray(value), `${path} must be an object`); return value as JsonRecord; } function asArray(value: unknown, path: string): unknown[] { assertCondition(Array.isArray(value), `${path} must be an array`); return value as unknown[]; } function stringField(value: unknown, path: string): string { assertCondition(typeof value === "string" && value.length > 0, `${path} must be a non-empty string`); return value as string; } function serviceById(environment: JsonRecord, id: string, path: string): JsonRecord { const services = asArray(environment.services, `${path}.services`).map((item, index) => asRecord(item, `${path}.services[${index}]`)); const service = services.find((item) => item.id === id); assertCondition(service !== undefined, `${path}.services must include ${id}`); return service!; } function assertDeployJson(): void { const deploy = asRecord(JSON.parse(readFileSync(rootPath("deploy.json"), "utf8")), "deploy.json"); const environments = asRecord(deploy.environments, "deploy.json.environments"); for (const environment of ["dev", "prod"] as const) { const service = serviceById(asRecord(environments[environment], `deploy.json.environments.${environment}`), serviceId, `deploy.json.environments.${environment}`); assertCondition(service.repo === sourceRepo, `${environment} deploy.json claudeqq repo must match source repo`); assertCondition(service.commitId === desiredCommit, `${environment} deploy.json claudeqq commit must match desired commit`); } } function assertArtifactCatalog(): void { const catalog = asRecord(JSON.parse(readFileSync(rootPath("CI.json"), "utf8")), "CI.json"); 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 claudeqq artifact producer"); assertCondition(artifact!.kind === "source-build", "claudeqq artifact must be source-build"); assertCondition(artifact!.status === "supported", "claudeqq artifact must be supported"); assertCondition(artifact!.producer === "ci publish-user-service", "claudeqq artifact producer must be ci publish-user-service"); const source = asRecord(artifact!.source, "CI.json claudeqq source"); assertCondition(source.repo === sourceRepo, "claudeqq artifact source repo must match agent_skills"); assertCondition(source.dockerfile === dockerfile, "claudeqq artifact dockerfile must be claudeqq/Dockerfile"); const image = asRecord(artifact!.image, "CI.json claudeqq image"); assertCondition(image.repository === "unidesk/claudeqq", "claudeqq artifact image repository must be unidesk/claudeqq"); } function assertAdapterSourceContract(): void { const adapter = readFileSync(rootPath("src/components/microservices/claudeqq/adapter.js"), "utf8"); const imageDockerfile = readFileSync(rootPath("src/components/microservices/claudeqq/Dockerfile"), "utf8"); for (const eventPath of eventPaths) { assertCondition(adapter.includes(eventPath), `adapter must expose ${eventPath}`); } assertCondition(adapter.includes("claudeqq-event-api-v1"), "adapter health metadata must name the event API contract"); assertCondition(adapter.includes("adapter-readonly-fallback"), "adapter must include read-only event fallback"); assertCondition(adapter.includes("CLAUDEQQ_HOST: upstreamHost"), "adapter must bind the upstream server to the private upstream host"); assertCondition(adapter.includes("CLAUDEQQ_PORT: String(upstreamPort)"), "adapter must bind the upstream server to the private upstream port"); assertCondition(adapter.includes("POST /api/events/subscriptions"), "adapter health metadata must document event subscription mutation paths"); assertCondition(imageDockerfile.includes('CMD ["node", "unidesk-adapter.cjs"]'), "claudeqq image must start the UniDesk adapter by default"); } async function assertDryRun(environment: "dev" | "prod"): Promise { const plan = asRecord(await runArtifactRegistryCommand([ "deploy-service", "--env", environment, "--service", serviceId, "--commit", desiredCommit, "--dry-run", ]), `artifact dry-run ${environment}`); assertCondition(plan.ok === true, `${environment} dry-run must be ok`); assertCondition(plan.supported === true, `${environment} dry-run must be supported`); assertCondition(plan.dryRun === true, `${environment} dry-run must report dryRun=true`); assertCondition(plan.mutation === false, `${environment} dry-run must not mutate`); assertCondition(plan.serviceId === serviceId, `${environment} dry-run serviceId must be claudeqq`); assertCondition(plan.commit === desiredCommit, `${environment} dry-run commit must match desired commit`); assertCondition(plan.sourceRepo === sourceRepo, `${environment} dry-run source repo must match`); const source = asRecord(plan.source, `${environment}.source`); assertCondition(source.repo === sourceRepo, `${environment} dry-run source.repo must match`); assertCondition(source.commit === desiredCommit, `${environment} dry-run source.commit must match`); assertCondition(source.dockerfile === dockerfile, `${environment} dry-run source.dockerfile must match`); const build = asRecord(plan.build, `${environment}.build`); assertCondition(build.willCompile === false, `${environment} dry-run must not compile on the runtime target`); assertCondition(build.willRunDockerBuild === false, `${environment} dry-run must not docker build on the runtime target`); assertCondition(build.willRunDockerComposeBuild === false, `${environment} dry-run must not docker compose build on the runtime target`); const labels = asRecord(plan.requiredLabels, `${environment}.requiredLabels`); assertCondition(labels["unidesk.ai/service-id"] === serviceId, `${environment} labels must include service id`); assertCondition(labels["unidesk.ai/source-repo"] === sourceRepo, `${environment} labels must include source repo`); assertCondition(labels["unidesk.ai/source-commit"] === desiredCommit, `${environment} labels must include source commit`); assertCondition(labels["unidesk.ai/dockerfile"] === dockerfile, `${environment} labels must include dockerfile`); const target = asRecord(plan.target, `${environment}.target`); assertCondition(target.kind === "d601-k3s", `${environment} target kind must be d601-k3s`); assertCondition(target.namespace === (environment === "dev" ? "unidesk-dev" : "unidesk"), `${environment} namespace must match`); assertCondition(target.deployment === (environment === "dev" ? "claudeqq-dev" : "claudeqq"), `${environment} deployment must match`); assertCondition(target.service === (environment === "dev" ? "claudeqq-dev" : "claudeqq"), `${environment} service must match`); assertCondition(target.runtimeImage === `unidesk-claudeqq:${desiredCommit}`, `${environment} runtime image must be commit-pinned`); const validation = asArray(plan.validation, `${environment}.validation`).map((item, index) => stringField(item, `${environment}.validation[${index}]`)); assertCondition(validation.some((line) => line.includes("service health via Kubernetes API service proxy")), `${environment} dry-run must require API service proxy health`); assertCondition(validation.some((line) => line.includes("source repo")), `${environment} dry-run must require source repo label validation`); } assertDeployJson(); assertArtifactCatalog(); assertAdapterSourceContract(); await assertDryRun("dev"); await assertDryRun("prod"); console.log(JSON.stringify({ ok: true, serviceId, desiredCommit, eventPaths, checks: [ "deploy.json desired dev/prod commit", "CI.json artifact producer contract", "adapter read-only event API fallback and health metadata", "dev/prod artifact-registry deploy-service dry-runs", ], }, null, 2));