From c34e44b3f7ca22fd3cbd1c3e87c27aea748591fd Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 23 May 2026 23:26:58 +0000 Subject: [PATCH] fix: pin code queue dev artifact to skills mount runtime --- deploy.json | 38 ++++++++++++++- docs/reference/cicd-standardization.md | 8 ++-- .../code-queue-runner-skills-contract-test.ts | 47 +++++++++++++++++++ scripts/issue-60-cicd-drift-contract-test.ts | 2 +- 4 files changed, 89 insertions(+), 6 deletions(-) diff --git a/deploy.json b/deploy.json index 383bf83f..b64de6bf 100644 --- a/deploy.json +++ b/deploy.json @@ -237,7 +237,43 @@ { "id": "code-queue", "repo": "https://github.com/pikasTech/unidesk", - "commitId": "0cf73d817f14032ad6038fd47ec402c87bf059bb" + "commitId": "e62f1c21d43a58f73f70516920814ca90f994df8", + "artifact": { + "kind": "source-build", + "repository": "unidesk/code-queue", + "tag": "commitId" + }, + "consumer": { + "kind": "d601-k3s-managed", + "dev": { + "enabled": true + }, + "prod": { + "enabled": false + }, + "supportLevel": "reviewed", + "targetRef": "origin/master:deploy.json#environments.dev.services.code-queue", + "noRuntimeSourceBuild": true, + "target": { + "namespace": "unidesk-dev", + "deployment": "code-queue-scheduler-dev", + "service": "code-queue-scheduler-dev", + "containerName": "code-queue", + "stableImage": "unidesk-code-queue:dev", + "manifestRepoPath": "src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-code-queue.k8s.yaml" + } + }, + "runtime": { + "containerPort": 4222, + "healthPath": "/health", + "memory": { + "request": "384Mi", + "limit": "5Gi" + }, + "health": { + "deployMetadataRequired": true + } + } } ] } diff --git a/docs/reference/cicd-standardization.md b/docs/reference/cicd-standardization.md index ab326b7b..57be1fc0 100644 --- a/docs/reference/cicd-standardization.md +++ b/docs/reference/cicd-standardization.md @@ -40,14 +40,14 @@ Phase one extends the desired-state model in this order: 4. Add deployment-time metadata that must be common across CI, dev CD and prod CD: deploy env prefix, health deploy-metadata requirement, required runtime secret key names, rollback policy, service port, health path, and resource profile identifiers. The planned first keys are `runtime.containerPort`, `runtime.healthPath`, `runtime.memory.request`, `runtime.memory.limit`, `health.deployMetadataRequired` and `runtime.requiredSecretKeys`. Secret values, credentials, volumes and host paths do not move into `deploy.json`. 5. Teach CI producer dry-run, deploy plan and artifact-registry dry-run to render from `deploy.json` first and compare mirrored `CI.json`/`config.json`/manifest values as derived copies. A mismatch is drift, not an alternate source of truth. -Phase two starts with low-risk D601 k3s user-service artifact consumers: `dev/mdtodo` and `dev/decision-center`. Their `deploy.json` entries now own the source-build artifact repository/tag policy, D601 k3s consumer target, stable image, manifest reference, service port, health path, memory request/limit and health deploy-metadata requirement. `deploy plan --env dev --service ` and dry-run artifact consumer plans must read those fields from `deploy.json`; the k8s manifest and artifact-registry executor constants are derived mirrors checked by a structured `deploy-json-drift` preflight. +Phase two starts with low-risk D601 k3s user-service artifact consumers plus the dev-only Code Queue artifact consumer: `dev/mdtodo`, `dev/decision-center` and `dev/code-queue`. Their `deploy.json` entries now own the source-build artifact repository/tag policy, D601 k3s consumer target, stable image, manifest reference, service port, health path, memory request/limit and health deploy-metadata requirement. `deploy plan --env dev --service ` and dry-run artifact consumer plans must read those fields from `deploy.json`; the k8s manifest and artifact-registry executor constants are derived mirrors checked by a structured `deploy-json-drift` preflight. Fields that stay outside `deploy.json` during phase one: - `CI.json`: producer pipeline name, source root, Dockerfile path and success summary shape. Once artifact identity is in `deploy.json`, `CI.json.artifacts[].image.repository` becomes a compatibility mirror checked for drift. -- `config.json`: provider id, proxy route policy, Compose file/service/container names for services not yet migrated, development SSH/worktree details, and secret source locations. For `dev/mdtodo` and `dev/decision-center`, port/health target values in dry-run are deploy-owned and config is no longer an alternate source. -- Compose and Kubernetes manifests: volumes, PVCs, security context and rollout strategy remain concrete manifests. For `dev/mdtodo` and `dev/decision-center`, container port, memory request/limit and deploy metadata env presence are deploy-owned values and manifest copies are drift-checked mirrors until a renderer owns the file. -- Artifact-registry executor code: low-level pull/import/retag/recreate commands, registry probes and platform-specific verification scripts. For `dev/mdtodo` and `dev/decision-center`, executor dry-run consumes the deploy-owned artifact/consumer/runtime contract; any hardcoded mismatch returns `deploy-json-drift`. +- `config.json`: provider id, proxy route policy, Compose file/service/container names for services not yet migrated, development SSH/worktree details, and secret source locations. For `dev/mdtodo`, `dev/decision-center` and `dev/code-queue`, port/health target values in dry-run are deploy-owned and config is no longer an alternate source. +- Compose and Kubernetes manifests: volumes, PVCs, security context and rollout strategy remain concrete manifests. For `dev/mdtodo`, `dev/decision-center` and `dev/code-queue`, container port, memory request/limit and deploy metadata env presence are deploy-owned values and manifest copies are drift-checked mirrors until a renderer owns the file. +- Artifact-registry executor code: low-level pull/import/retag/recreate commands, registry probes and platform-specific verification scripts. For `dev/mdtodo`, `dev/decision-center` and `dev/code-queue`, executor dry-run consumes the deploy-owned artifact/consumer/runtime contract; any hardcoded mismatch returns `deploy-json-drift`. The drift contract is: diff --git a/scripts/code-queue-runner-skills-contract-test.ts b/scripts/code-queue-runner-skills-contract-test.ts index c15735f5..868fa4d8 100644 --- a/scripts/code-queue-runner-skills-contract-test.ts +++ b/scripts/code-queue-runner-skills-contract-test.ts @@ -1,6 +1,7 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { spawnSync } from "node:child_process"; import { collectSkillAvailability, collectSkillSyncPreflight } from "../src/components/microservices/code-queue/src/skill-availability"; import { codexPrPreflightQueryForTest } from "./src/code-queue"; import { summarizeMicroserviceObservation } from "./src/microservices"; @@ -20,6 +21,19 @@ function countOccurrences(haystack: string, needle: string): number { return haystack.split(needle).length - 1; } +function gitShowText(commit: string, path: string): string { + const result = spawnSync("git", ["show", `${commit}:${path}`], { + cwd: process.cwd(), + encoding: "utf8", + maxBuffer: 2 * 1024 * 1024, + }); + assertCondition(result.status === 0, `git show should read ${path} at ${commit}`, { + status: result.status, + stderr: result.stderr.slice(-1000), + }); + return result.stdout; +} + function createSkillSet(root: string, skills: string[]): void { for (const skill of skills) { const dir = join(root, skill); @@ -30,6 +44,13 @@ function createSkillSet(root: string, skills: string[]): void { const productionManifest = readFileSync("src/components/microservices/k3sctl-adapter/k3s/code-queue.k8s.yaml", "utf8"); const devManifest = readFileSync("src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-code-queue.k8s.yaml", "utf8"); +const deployJson = JSON.parse(readFileSync("deploy.json", "utf8")) as { + environments?: { + dev?: { + services?: Array>; + }; + }; +}; const runtimePreflight = readFileSync("src/components/microservices/code-queue/src/runtime-preflight.ts", "utf8"); const indexSource = readFileSync("src/components/microservices/code-queue/src/index.ts", "utf8"); const skillModule = readFileSync("src/components/microservices/code-queue/src/skill-availability.ts", "utf8"); @@ -61,6 +82,31 @@ assertCondition(countOccurrences(productionManifest, "name: skills-dir") >= 6, " }); assertCondition(devManifest.includes("path: /home/ubuntu/.agents/skills"), "dev manifest should keep the same hostPath source of truth"); +const devCodeQueueDeploy = (deployJson.environments?.dev?.services ?? []).find((service) => service.id === "code-queue"); +assertCondition(devCodeQueueDeploy !== undefined, "deploy.json dev environment must include code-queue"); +const devCodeQueueCommit = String(devCodeQueueDeploy?.commitId ?? ""); +assertCondition(/^[0-9a-f]{40}$/u.test(devCodeQueueCommit), "deploy.json dev code-queue commit must be a full SHA", devCodeQueueDeploy); +assertCondition(devCodeQueueCommit !== "0cf73d817f14032ad6038fd47ec402c87bf059bb", "deploy.json dev code-queue must not pin the pre-skills-mount source commit", devCodeQueueDeploy); +assertCondition(asRecord(devCodeQueueDeploy?.artifact, "dev code-queue artifact").repository === "unidesk/code-queue", "deploy.json dev code-queue must own artifact repository", devCodeQueueDeploy); +const devCodeQueueConsumer = asRecord(devCodeQueueDeploy?.consumer, "dev code-queue consumer"); +assertCondition(devCodeQueueConsumer.kind === "d601-k3s-managed", "deploy.json dev code-queue must use the D601 k3s artifact consumer", devCodeQueueConsumer); +const devCodeQueueTarget = asRecord(devCodeQueueConsumer.target, "dev code-queue consumer target"); +assertCondition(devCodeQueueTarget.manifestRepoPath === "src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-code-queue.k8s.yaml", "deploy.json dev code-queue must point at the dev k3s manifest", devCodeQueueTarget); + +const pinnedDevManifest = gitShowText(devCodeQueueCommit, "src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-code-queue.k8s.yaml"); +const pinnedRuntimePreflight = gitShowText(devCodeQueueCommit, "src/components/microservices/code-queue/src/runtime-preflight.ts"); +const pinnedIndexSource = gitShowText(devCodeQueueCommit, "src/components/microservices/code-queue/src/index.ts"); +assertCondition(countOccurrences(pinnedDevManifest, "path: /home/ubuntu/.agents/skills") === 3, "deploy.json dev code-queue commit must include source skills hostPath for scheduler/read/write", { + commit: devCodeQueueCommit, +}); +assertCondition(countOccurrences(pinnedDevManifest, "mountPath: /root/.agents/skills") === 3, "deploy.json dev code-queue commit must mount skills target for scheduler/read/write", { + commit: devCodeQueueCommit, +}); +assertCondition(!pinnedDevManifest.includes(forbiddenPathLiteral), "deploy.json dev code-queue commit must not include the misspelled skills path"); +assertCondition(pinnedRuntimePreflight.includes("skills.contractOk && ports.codex.ok"), "deploy.json dev code-queue commit runtime-preflight must require target projection contract"); +assertCondition(pinnedIndexSource.includes("skills.contractOk === true"), "deploy.json dev code-queue commit dev-ready must require target projection contract"); +assertCondition(pinnedIndexSource.includes("return config.skillsPath"), "deploy.json dev code-queue commit must keep runner UNIDESK_SKILLS_PATH on the configured target"); + const available = collectSkillAvailability({ source: "/home/ubuntu/.agents/skills", target: "/home/ubuntu/.agents/skills", @@ -400,6 +446,7 @@ process.stdout.write(`${JSON.stringify({ ok: true, checks: [ "production Code Queue mounts /home/ubuntu/.agents/skills read-only at /root/.agents/skills", + "deploy.json dev Code Queue pins a commit whose manifest and runtime require the target skills projection", "skill availability report exposes source, target, requiredSkills, missingSkills, version fingerprint/mtime, degraded/blocker and valuesPrinted=false", "skills sync dry-run reports source, target, counts, version fingerprint/mtime, missing skills, permission failures, instructions and no-copy actions", "scheduler blocks runner startup with structured infra-blocked output when required skills are unavailable", diff --git a/scripts/issue-60-cicd-drift-contract-test.ts b/scripts/issue-60-cicd-drift-contract-test.ts index 58b84a2b..19a86cf8 100644 --- a/scripts/issue-60-cicd-drift-contract-test.ts +++ b/scripts/issue-60-cicd-drift-contract-test.ts @@ -59,7 +59,7 @@ const plannedDeployJsonFields = [ "runtime.requiredSecretKeys", ]; -const phaseTwoExecutorContractServices = new Set(["dev/decision-center", "dev/mdtodo"]); +const phaseTwoExecutorContractServices = new Set(["dev/decision-center", "dev/mdtodo", "dev/code-queue"]); function assertCondition(condition: unknown, message: string, detail: unknown = {}): void { if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);