From 62a2eaad689668c49338f9f8aaea57cc201e7990 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 21 May 2026 05:13:29 +0000 Subject: [PATCH] test: add code queue cicd dry-run contract --- TEST.md | 4 + docs/reference/cicd-standardization.md | 21 +++ docs/reference/codex-deploy.md | 9 ++ docs/reference/deploy.md | 2 + .../code-queue-cicd-dry-run-contract-test.ts | 126 ++++++++++++++++ scripts/src/deploy.ts | 135 ++++++++++++++++-- 6 files changed, 286 insertions(+), 11 deletions(-) create mode 100644 scripts/code-queue-cicd-dry-run-contract-test.ts diff --git a/TEST.md b/TEST.md index e36dc87c..d478bfee 100644 --- a/TEST.md +++ b/TEST.md @@ -111,6 +111,10 @@ 随后登录公网 frontend `http://74.48.78.17:18081/`,进入 `用户服务 / Code Queue`,确认页面显示默认模型 `gpt-5.5`、默认执行 Provider `D601`、默认工作目录 `/workspace`、模型下拉菜单包含 `gpt-5.4-mini`/`gpt-5.4`/`gpt-5.5`、入队份数、队列指标、任务 ID、复制任务 ID、引用按钮、任务耗时、引用任务 ID、清空输入、创建成功提示、任务提交表单、Trace 输出、attempt 表、MiniMax/fallback judge 状态、追加 prompt、打断和重试控件;通过页面提交一个小任务,确认任务进入 queued/running/succeeded 或可解释的 failed 状态,并且输出区能看到运行中的 Codex 消息。需要验证运行中纠偏时,提交一个明确会保持 running 的长任务,使用 `printf '<纠偏提示>' | bun scripts/cli.ts codex steer --prompt-stdin` 注入 prompt,确认命令返回有界 task/queue 摘要和 `codex task`/`codex output` 后续命令,且 `bun scripts/cli.ts codex task --trace --tail --limit 80` 能看到 `Steer prompt` 或 `turn/steer`;正式验收不得再使用底层 `microservice proxy code-queue /api/tasks//steer` 作为主入口。批量验收时设置 `入队份数=5` 或用 `---` 分隔 5 段 prompt,一次性入队 5 条任务,确认 5 条任务按顺序运行并全部进入 succeeded 或可解释的非成功终态,不能只运行第一条后停止;其中任一任务被 judge 判定 `fail` 时只能把当前任务标为 failed,后续 queued 任务仍必须继续推进。测试异常中断时可以提交长任务后点击 `打断`,确认任务变为 canceled 或被 judge 标记为非成功终态;自动重试只应在服务端/传输异常、任务正常结束但 execution record 显示未完成、或 judge 判定 retry 时发生;retry 必须复用已有 Codex thread 并 append 继续执行 prompt,只有当前任务 complete 后才推进队列中的下一个任务。MiniMax judge 必须能处理 Markdown fence/夹杂文本等 JSON 去噪;若去噪后仍失败,必须把解析错误和上一轮去噪前原始回答反馈给 MiniMax 修复后重试,日志中应出现 `judge_json_parse_retry`,且 repair 成功时仍以 `source=minimax` 返回。Codex provider key 只能通过 `OPENAI_API_KEY`、`CRS_OAI_KEY` 这类运行时环境透传,MiniMax API key 只能通过 D601 env-file 运行时环境传入,禁止写入 `config.json`、Dockerfile、源码或测试文档。 +## T23D Code Queue CI/CD Dry-Run Contract + +阅读 `AGENTS.md`(本项目 `AGENTS.md` 同时承担 `SKILL.md` 对 `scripts/cli.ts` 的解释职责),然后用 cli 手动测试以下内容:运行 `bun scripts/code-queue-cicd-dry-run-contract-test.ts`,确认 Code Queue 标准镜像化 CI/CD dry-run 合同输出 `ok=true`。该测试必须证明 CI 只负责发布 commit-pinned `unidesk/code-queue:` artifact 与 digest/label summary,DEV CD dry-run 只指向 `unidesk-dev` Code Queue scheduler/read/write/provider-egress-proxy,PROD Code Queue 是 structured unsupported 且不暴露 production k3s runtime deploy target,并且输出显式禁止 `codex deploy`、`server rebuild code-queue`、production namespace/manifest mutation、scheduler/runner restart、任务 interrupt 和 cancel。该测试不得执行真实 deploy/rebuild/restart,不得 interrupt/cancel 运行中任务,也不得执行 prod apply。 + ## T23A D601 k3s CI Gate 阅读 `AGENTS.md` 和 `docs/reference/ci.md`,运行 `bun scripts/cli.ts ci install`,确认 Tekton Pipelines `v1.12.0`、Tekton Triggers `v0.34.0` 和 `unidesk-ci` Pipeline/Task/EventListener 已部署到 D601 原生 k3s;随后运行 `bun scripts/cli.ts ci run --revision <已push的commitId> --wait-ms 1200000`,确认 PipelineRun 只执行 clone/check/performance,不调用 `deploy apply` 或 `codex deploy`,并确认临时 `code-queue-ci-read` 使用主 PostgreSQL 只读查询 Code Queue 首屏、TraceView summary、TraceView steps 和 step detail 的性能指标。若失败,使用 `bun scripts/cli.ts ci logs ` 查看 TaskRun 和 Pod 日志;交付说明必须记录性能预算是否通过。 diff --git a/docs/reference/cicd-standardization.md b/docs/reference/cicd-standardization.md index 2e5923e4..58afcf5d 100644 --- a/docs/reference/cicd-standardization.md +++ b/docs/reference/cicd-standardization.md @@ -184,6 +184,27 @@ CI may build images, push to the D601 loopback registry and report immutable dig backend-core and D601 `code-queue` remain restricted to dev image validation in this phase. Any future production rollout for them must be implemented as an explicit CD consumer change, not as a CI producer side effect. +### Code Queue Dry-Run Delivery Boundary + +Code Queue follows the standard artifact split only up to a dev-only consumer: + +| Stage | Owner | Allowed output | Explicitly forbidden | +| --- | --- | --- | --- | +| CI producer | Tekton / CI runner outside Code Queue self-deploy | Build from pushed Git, publish `127.0.0.1:5000/unidesk/code-queue:`, report service id, source repo, source commit, Dockerfile, tag, digest and digest ref | `deploy apply`, `kubectl apply`, scheduler/read/write rollout, task `interrupt` or `cancel` | +| CD dry-run | deploy/artifact-registry CLI | Read `origin/master:deploy.json`, show `unidesk-dev` target objects, required registry image, no-runtime-build validation and forbidden actions | Mutating runtime objects, using dirty worktrees, using mutable tags, falling back to `codex deploy` or D601 maintenance-channel source deploy | +| DEV live apply | Human operator after dry-run evidence | Pull/import the existing commit image, update only `unidesk-dev` Code Queue scheduler/read/write/provider-egress-proxy objects, verify health through the Kubernetes API service proxy | Touching production `unidesk` namespace, production PostgreSQL, production scheduler/runner, running task state or Code Queue Manager prod sidecar | +| PROD | Not implemented | Structured unsupported / dry-run evidence only | Any production Code Queue artifact deploy, manifest mutation, rollout restart, scheduler/runner rebuild, task interrupt/cancel, or self-deploy by the running Code Queue task | + +The operator review points are fixed. DEV requires an operator to compare the CI artifact summary with `deploy plan --env dev --service code-queue` or `deploy apply --env dev --service code-queue --dry-run`, then explicitly authorize a DEV apply outside the Code Queue task. PROD has no apply authorization point in this phase; even a manual request must first land a separate reviewed Code Queue production CD design. The current runner may produce plans, preflight output, docs and contract tests only. + +Lightweight contract evidence for this boundary is: + +```bash +bun scripts/code-queue-cicd-dry-run-contract-test.ts +``` + +The test checks that dev targets only `unidesk-dev`, prod exposes no runtime deploy target, production mutation is unsupported, and dry-run output forbids Code Queue self-deploy, scheduler/runner mutation, interrupt and cancel actions. + ## Validation Boundary This precheck uses lightweight parsing and dry-run evidence only. It intentionally does not run full `check`, e2e, Playwright, or other broad browser/runtime test suites on the master server because those are outside the precheck scope and may exceed master-server resource limits. `backend-core` and D601 `code-queue` production validation are also out of scope; backend-core dev rollout can be attempted only through the existing D601 dev path, and a provider-offline result is an infrastructure blocker rather than permission to validate production. diff --git a/docs/reference/codex-deploy.md b/docs/reference/codex-deploy.md index 53fd8047..71f7ac9d 100644 --- a/docs/reference/codex-deploy.md +++ b/docs/reference/codex-deploy.md @@ -4,6 +4,15 @@ Code Queue 后续正式生产部署必须走一条受控 CD 路径并单独审查;当前阶段只提供 dev artifact consumer。`deploy apply --env dev --service code-queue` 或 `artifact-registry deploy-service --env dev --service code-queue` 可以消费 D601 registry 中的 `unidesk/code-queue:`,只更新 `unidesk-dev` Code Queue execution slice,并通过 Kubernetes API service proxy 验证健康。`--env prod --service code-queue` 必须明确 unsupported,不能执行生产 artifact deploy、rollout 或 manifest 变更。persistent dev apply 的完整服务范围见 `docs/reference/dev-environment.md`,Code Queue temporary smoke 仍通过 `ci run-dev-e2e`,规则见 `docs/reference/dev-ci-runner.md`。 +The reproducible dry-run delivery path is: + +1. CI publishes `127.0.0.1:5000/unidesk/code-queue:` from pushed Git and reports the immutable digest/label summary. +2. A runner or operator runs `bun scripts/cli.ts deploy plan --env dev --service code-queue` or `bun scripts/cli.ts deploy apply --env dev --service code-queue --commit --dry-run`; this is non-mutating evidence only. +3. A human operator reviews the dry-run and, if accepted, separately authorizes DEV apply. That apply may touch only `unidesk-dev` Code Queue execution objects. +4. PROD remains unsupported. A dry-run or plan may document the gap, but no Code Queue runner may perform prod apply, rollout restart, scheduler/runner rebuild, interrupt or cancel as part of delivering Code Queue itself. + +This contract is checked by `bun scripts/code-queue-cicd-dry-run-contract-test.ts`. The test is intentionally lightweight and does not run full e2e, Playwright, Code Queue deployment, or task interruption flows. + ## Command ```bash diff --git a/docs/reference/deploy.md b/docs/reference/deploy.md index e92106cb..465be47c 100644 --- a/docs/reference/deploy.md +++ b/docs/reference/deploy.md @@ -114,6 +114,8 @@ Production `code-queue-mgr` is a separate main-server Compose sidecar artifact c Environment plan output must be sufficient to review the artifact matrix without running a live apply. Each service item includes `deploymentPath`, `artifactConsumer.consumerKind`, `artifactConsumer.registryImage`, `artifactConsumer.noRuntimeSourceBuild`, `artifactConsumer.dryRunOnly`, `target`, `validation` and `liveApply` where relevant. `consumerKind=d601-direct-compose` means the reviewed consumer touches only the D601 Docker/Compose service and private health path; `consumerKind=d601-k3s-managed` means the reviewed consumer imports the artifact into native k3s/containerd and verifies through the Kubernetes API service proxy; `consumerKind=main-server-compose` means the reviewed consumer streams or loads the D601 artifact into the main-server Compose service; `consumerKind=d601-dev-target-side-build` is reserved for the controlled dev backend-core source-build exception. Artifact consumer plan items must explicitly report `noRuntimeSourceBuild=true` and list forbidden build/public exposure actions. Blocked or gated services must keep structured `dryRunOnly` / `blockedReason` output, for example `met-nonlinear` `runtime-verification-blocked` and `k3sctl-adapter` supervisor-only production apply. +For `--env dev --service code-queue`, the environment plan must also expose a `boundary` block that separates the CI producer from the dev CD consumer. CI is allowed to publish only `127.0.0.1:5000/unidesk/code-queue:` plus digest/label evidence. DEV CD may consume that artifact only for `unidesk-dev` Code Queue scheduler/read/write/provider-egress-proxy objects after an operator reviews the dry-run. For `--env prod --service code-queue`, the service item must remain `deploymentPath=unsupported`, `artifactConsumer.consumerKind=unsupported`, `target.deployCommandShape=none` and `liveApply.allowed=false`; it must not expose production k3s as an executable target. The prod boundary must state that production Code Queue CD needs a future supervisor-approved design and that this runner cannot self-deploy, mutate the production namespace, restart scheduler/runner, or interrupt/cancel tasks. + `bun scripts/cli.ts deploy apply [--file deploy.json | --env dev|prod] [--service ] [--commit ] [--dry-run] [--force]` starts an asynchronous job only for supported targets. Use `bun scripts/cli.ts job status --tail-bytes 30000` to observe progress. `--dry-run` resolves the same plan but does not build or replace runtime objects. `--force` rebuilds even when the live commit matches. Environment apply is not the dev e2e trigger; use `bun scripts/cli.ts ci run-dev-e2e` for the Git-controlled temporary namespace smoke flow. `--env dev` apply is enabled for persistent D601 `backend-core` target-side rollout and for `frontend`/`baidu-netdisk`/`decision-center`/`mdtodo`/`claudeqq`/dev-only `code-queue`/`project-manager`/`oa-event-flow`/`code-queue-mgr`/`todo-note`/`findjob`/`pipeline`/`met-nonlinear` artifact consumers. `--env prod` apply exposes the D601 registry artifact consumer for `backend-core`, `frontend`, `baidu-netdisk`, `decision-center`, `mdtodo`, `claudeqq`, `project-manager`, `oa-event-flow`, `todo-note`, `findjob`, `pipeline` and `met-nonlinear`; `code-queue-mgr` prod live apply is supervisor-gated and `k3sctl-adapter` is plan/dry-run only. `--commit` may override one selected reviewed artifact consumer in either dev or prod, for example `deploy apply --env dev --service frontend --commit `, and the image must already exist as `127.0.0.1:5000/unidesk/:`. Unsupported prod services, especially `code-queue`, return a structured `unsupported` payload instead of silently falling back to a maintenance-channel source build. All deploy commands output JSON. Long operations must use `.state/jobs/` and bounded log tails; no deploy path may succeed with missing progress output. diff --git a/scripts/code-queue-cicd-dry-run-contract-test.ts b/scripts/code-queue-cicd-dry-run-contract-test.ts new file mode 100644 index 00000000..e00a67fb --- /dev/null +++ b/scripts/code-queue-cicd-dry-run-contract-test.ts @@ -0,0 +1,126 @@ +import { spawnSync } from "node:child_process"; + +type JsonRecord = Record; + +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 asStringArray(value: unknown, label: string): string[] { + assertCondition(Array.isArray(value) && value.every((item) => typeof item === "string"), `${label} must be a string array`, value); + return value as string[]; +} + +function runCli(args: string[], expectStatus: number): JsonRecord { + const result = spawnSync("bun", ["scripts/cli.ts", ...args], { + cwd: process.cwd(), + encoding: "utf8", + maxBuffer: 8 * 1024 * 1024, + }); + assertCondition(result.status === expectStatus, `cli status mismatch for ${args.join(" ")}`, { + status: result.status, + stdout: result.stdout.slice(-3000), + stderr: result.stderr.slice(-3000), + }); + return asRecord(JSON.parse(result.stdout) as unknown, "cli envelope"); +} + +function firstService(envelope: JsonRecord, label: string): JsonRecord { + const data = asRecord(envelope.data, `${label} data`); + const services = Array.isArray(data.services) ? data.services : []; + assertCondition(services.length === 1, `${label} should return exactly one service`, data); + return asRecord(services[0], `${label} service`); +} + +function includes(value: unknown, expected: string): boolean { + return Array.isArray(value) && value.some((item) => item === expected); +} + +const commit = "0123456789abcdef0123456789abcdef01234567"; + +const devPlan = firstService(runCli(["deploy", "plan", "--env", "dev", "--service", "code-queue"], 0), "dev plan"); +const devArtifact = asRecord(devPlan.artifactConsumer, "dev artifactConsumer"); +const devTarget = asRecord(devPlan.target, "dev target"); +const devBoundary = asRecord(devPlan.boundary, "dev boundary"); +const devCd = asRecord(devBoundary.cdConsumer, "dev cdConsumer"); + +assertCondition(devArtifact.consumerKind === "d601-k3s-managed", "dev Code Queue must be a k3s-managed artifact consumer", devArtifact); +assertCondition(devArtifact.noRuntimeSourceBuild === true, "dev Code Queue must not build source on the runtime target", devArtifact); +assertCondition(devTarget.namespace === "unidesk-dev", "dev Code Queue target namespace must be unidesk-dev", devTarget); +assertCondition(devTarget.deployment === "code-queue-scheduler-dev", "dev Code Queue should target the dev scheduler deployment", devTarget); +assertCondition(devCd.prodMutationAllowed === false, "dev Code Queue boundary must prohibit production mutation", devCd); +assertCondition(String(devCd.manualAuthorizationPoint ?? "").includes("DEV apply"), "dev Code Queue boundary should expose the DEV manual authorization point", devCd); +assertCondition(asRecord(devBoundary.ciProducer, "dev ciProducer").allowed === true, "Code Queue CI producer should be allowed for image publication", devBoundary); +assertCondition(includes(devTarget.forbiddenActions, "docker build"), "dev target must forbid runtime docker build", devTarget); +assertCondition(includes(devTarget.forbiddenActions, "NodePort"), "dev target must forbid NodePort", devTarget); + +const prodPlan = firstService(runCli(["deploy", "plan", "--env", "prod", "--service", "code-queue"], 1), "prod plan"); +const prodArtifact = asRecord(prodPlan.artifactConsumer, "prod artifactConsumer"); +const prodTarget = asRecord(prodPlan.target, "prod target"); +const prodBoundary = asRecord(prodPlan.boundary, "prod boundary"); +const prodCd = asRecord(prodBoundary.cdConsumer, "prod cdConsumer"); +const prodLiveApply = asRecord(prodPlan.liveApply, "prod liveApply"); +const prodUnsupported = asRecord(prodPlan.unsupported, "prod unsupported"); +const prodForbidden = asStringArray(prodTarget.forbiddenActions, "prod forbiddenActions"); + +assertCondition(prodPlan.deploymentPath === "unsupported", "prod Code Queue deployment path must be unsupported", prodPlan); +assertCondition(prodArtifact.consumerKind === "unsupported", "prod Code Queue artifact consumer must be unsupported", prodArtifact); +assertCondition(prodArtifact.registryImage === null, "prod Code Queue plan must not advertise a production registry image target", prodArtifact); +assertCondition(prodArtifact.noRuntimeSourceBuild === true, "prod Code Queue plan must still block runtime source builds", prodArtifact); +assertCondition(prodTarget.runtimeHost === null, "prod Code Queue plan must not expose a runtime host target", prodTarget); +assertCondition(prodTarget.deployCommandShape === "none", "prod Code Queue plan must not expose a deploy command shape", prodTarget); +assertCondition(prodLiveApply.allowed === false, "prod Code Queue live apply must be blocked", prodLiveApply); +assertCondition(String(prodLiveApply.reason ?? "").includes("production CD is intentionally unsupported"), "prod blocked reason should name the intentional CD gap", prodLiveApply); +assertCondition(String(prodUnsupported.reason ?? "").includes("prod artifact deploy"), "prod unsupported reason should mention prod artifact deploy", prodUnsupported); +assertCondition(prodCd.prodMutationAllowed === false, "prod Code Queue boundary must prohibit production mutation", prodCd); +assertCondition(prodCd.liveApplyCommandShape === null, "prod Code Queue boundary must not advertise a live apply command", prodCd); +assertCondition(String(prodCd.manualAuthorizationPoint ?? "").includes("future supervisor-approved"), "prod boundary should require a future supervisor-approved design", prodCd); +assertCondition(prodForbidden.includes("production namespace mutation"), "prod forbidden actions must include production namespace mutation", prodTarget); +assertCondition(prodForbidden.includes("interrupt running Code Queue tasks"), "prod forbidden actions must include task interrupt", prodTarget); +assertCondition(prodForbidden.includes("cancel running Code Queue tasks"), "prod forbidden actions must include task cancel", prodTarget); +assertCondition(JSON.stringify(prodTarget).includes("code-queue-read"), "prod excluded targets should name production Code Queue deployments", prodTarget); + +const devArtifactDryRun = asRecord(runCli([ + "artifact-registry", + "deploy-service", + "--env", + "dev", + "--service", + "code-queue", + "--commit", + commit, + "--dry-run", +], 0).data, "dev artifact-registry dry-run"); +const artifactTarget = asRecord(devArtifactDryRun.target, "artifact dry-run target"); +assertCondition(devArtifactDryRun.mutation === false, "artifact-registry dev dry-run must be non-mutating", devArtifactDryRun); +assertCondition(artifactTarget.namespace === "unidesk-dev", "artifact-registry dev dry-run must target unidesk-dev", artifactTarget); + +const prodArtifactDryRun = asRecord(runCli([ + "artifact-registry", + "deploy-service", + "--env", + "prod", + "--service", + "code-queue", + "--commit", + commit, + "--dry-run", +], 1).data, "prod artifact-registry dry-run"); +assertCondition(prodArtifactDryRun.error === "unsupported-environment", "artifact-registry prod code-queue should be unsupported", prodArtifactDryRun); +assertCondition(Array.isArray(prodArtifactDryRun.supportedEnvironments) && prodArtifactDryRun.supportedEnvironments.length === 1 && prodArtifactDryRun.supportedEnvironments[0] === "dev", "artifact-registry prod code-queue should expose only dev as supported", prodArtifactDryRun); + +process.stdout.write(`${JSON.stringify({ + ok: true, + checks: [ + "deploy plan exposes Code Queue CI producer and dev CD consumer boundary", + "dev Code Queue dry-run targets only unidesk-dev and forbids runtime builds/public ports", + "prod Code Queue plan is unsupported and exposes no runtime deploy target", + "prod Code Queue boundary forbids self-deploy, prod mutation, interrupt and cancel actions", + "artifact-registry dev dry-run is non-mutating while prod remains unsupported", + ], +}, null, 2)}\n`); diff --git a/scripts/src/deploy.ts b/scripts/src/deploy.ts index dc4355b4..b7715234 100644 --- a/scripts/src/deploy.ts +++ b/scripts/src/deploy.ts @@ -1002,6 +1002,105 @@ function artifactConsumerPlanValidation(service: UniDeskMicroserviceConfig, envi return ["unsupported service remains blocked before source materialization or runtime mutation"]; } +function unsupportedEnvironmentPlanReason(serviceId: string, environment: DeployEnvironment): string { + if (environment === "prod" && serviceId === "code-queue") { + return "Code Queue production CD is intentionally unsupported in this phase: only CI image publication and dev artifact-consumer validation are implemented; prod artifact deploy, rollout and manifest mutation are blocked."; + } + return environment === "prod" + ? "No standardized prod D601 registry artifact consumer is implemented for this service; legacy maintenance-channel deployment is not allowed." + : "No standardized dev deployment or artifact validation path is implemented for this service."; +} + +function unsupportedPlanTarget(serviceId: string, environment: DeployEnvironment, reason: string): Record { + const forbiddenActions = [ + "docker build", + "docker compose build", + "docker compose up --build", + "maintenance-channel source deploy", + "dirty worktree source deploy", + ]; + if (serviceId === "code-queue") { + forbiddenActions.push( + "codex deploy", + "server rebuild code-queue", + "production namespace mutation", + "production manifest mutation", + "interrupt running Code Queue tasks", + "cancel running Code Queue tasks", + ); + } + return { + consumerKind: "unsupported", + runtimeHost: null, + deploymentMode: "none", + noRuntimeSourceBuild: true, + dryRunOnly: true, + blockedReason: reason, + deployCommandShape: "none", + forbiddenActions, + ...(serviceId === "code-queue" + ? { + excludedTargets: [ + { + namespace: "unidesk", + deployments: ["code-queue", "code-queue-read", "code-queue-write", "d601-provider-egress-proxy", "d601-tcp-egress-gateway"], + reason: `${environment} dry-run must not mutate production Code Queue execution-plane objects.`, + }, + { + operations: ["interrupt", "cancel", "restart scheduler", "restart runner"], + reason: "Code Queue cannot self-deploy or alter active task control state from this runner path.", + }, + ], + } + : {}), + }; +} + +function codeQueueCiCdBoundary(serviceId: string, environment: DeployEnvironment): Record | undefined { + if (serviceId !== "code-queue") return undefined; + return { + serviceId, + selfDeployBlocked: true, + ciProducer: { + command: "bun scripts/cli.ts ci publish-user-service --service code-queue --commit ", + allowed: true, + outputImage: "127.0.0.1:5000/unidesk/code-queue:", + responsibility: "build a commit-pinned Code Queue image from pushed Git source, push it to the D601 registry, and report digest/label evidence", + forbiddenActions: ["deploy apply", "kubectl apply", "rollout restart", "interrupt", "cancel"], + }, + cdConsumer: environment === "dev" + ? { + environment: "dev", + dryRunCommand: "bun scripts/cli.ts deploy apply --env dev --service code-queue --commit --dry-run", + liveApplyCommandShape: "bun scripts/cli.ts deploy apply --env dev --service code-queue --commit --run-now", + allowedTarget: "unidesk-dev Code Queue scheduler/read/write/provider-egress-proxy only", + manualAuthorizationPoint: "operator reviews CI artifact summary and dev dry-run plan, then explicitly authorizes DEV apply outside this Code Queue task", + prodMutationAllowed: false, + } + : { + environment: "prod", + dryRunCommand: "bun scripts/cli.ts deploy plan --env prod --service code-queue", + liveApplyCommandShape: null, + allowedTarget: null, + manualAuthorizationPoint: "PROD requires a future supervisor-approved Code Queue CD design; current runner output is plan/unsupported only", + prodMutationAllowed: false, + }, + manualAuthorization: { + dev: "DEV live apply is a human operator decision after dry-run evidence; this task may not execute it.", + prod: "PROD live apply is not implemented and must remain unsupported, even with manual intent, until a separate reviewed CD consumer exists.", + }, + forbiddenActions: [ + "codex deploy", + "server rebuild code-queue", + "deploy apply --env prod --service code-queue", + "artifact-registry deploy-service --env prod --service code-queue", + "kubectl apply to namespace unidesk", + "rollout restart production Code Queue scheduler/read/write", + "interrupt or cancel active Code Queue tasks", + ], + }; +} + function environmentPlanServiceConfig(config: UniDeskConfig | null, service: DeployManifestService, environment: DeployEnvironment): UniDeskMicroserviceConfig | null { if (environment === "dev") { const devService = devK3sDeployService(service.id); @@ -2845,7 +2944,12 @@ function environmentDryRunPlan( const unsupported = (environment === "prod" && !prodArtifactConsumerServiceIds.has(service.id)) || (environment === "dev" && !devApplySupportedServiceIds.has(service.id) && !devArtifactConsumerServiceIds.has(service.id)); const dryRunBlockedReason = artifactConsumerDryRunBlockedServiceIds.get(service.id) ?? null; + const planKind = serviceConfig === null ? "unsupported" : artifactConsumerPlanKind(serviceConfig, environment); const planTarget = serviceConfig === null ? null : artifactConsumerPlanTarget(serviceConfig, environment); + const unsupportedReason = unsupported ? unsupportedEnvironmentPlanReason(service.id, environment) : null; + const effectiveTarget = unsupported + ? unsupportedPlanTarget(service.id, environment, unsupportedReason ?? "unsupported") + : planTarget; return { id: service.id, repo: service.repo, @@ -2863,24 +2967,33 @@ function environmentDryRunPlan( dryRunOnly: true, blockedReason: "service is missing from config.json and no synthetic deploy service is available for this environment", } : { - consumerKind: artifactConsumerPlanKind(serviceConfig, environment), - registryImage: `127.0.0.1:5000/unidesk/${service.id}:${service.commitId}`, - noRuntimeSourceBuild: artifactConsumerPlanKind(serviceConfig, environment) !== "d601-dev-target-side-build", - dryRunOnly: (environment === "prod" && prodArtifactLiveApplyBlockedServiceIds.has(service.id)) || dryRunBlockedReason !== null, - blockedReason: dryRunBlockedReason ?? (environment === "prod" ? prodArtifactLiveApplyBlockedServiceIds.get(service.id) ?? null : null), + consumerKind: unsupported ? "unsupported" : planKind, + registryImage: unsupported ? null : `127.0.0.1:5000/unidesk/${service.id}:${service.commitId}`, + noRuntimeSourceBuild: unsupported || planKind !== "d601-dev-target-side-build", + dryRunOnly: unsupported || (environment === "prod" && prodArtifactLiveApplyBlockedServiceIds.has(service.id)) || dryRunBlockedReason !== null, + blockedReason: unsupportedReason ?? dryRunBlockedReason ?? (environment === "prod" ? prodArtifactLiveApplyBlockedServiceIds.get(service.id) ?? null : null), }, - target: planTarget, - validation: serviceConfig === null ? undefined : artifactConsumerPlanValidation(serviceConfig, environment), + target: effectiveTarget, + validation: unsupported + ? ["unsupported service remains blocked before source materialization, registry pull, manifest update, rollout, or runtime mutation"] + : serviceConfig === null + ? undefined + : artifactConsumerPlanValidation(serviceConfig, environment), + boundary: codeQueueCiCdBoundary(service.id, environment), unsupported: unsupported ? { ok: false, supported: false, - reason: environment === "prod" - ? "No standardized prod D601 registry artifact consumer is implemented for this service; legacy maintenance-channel deployment is not allowed." - : "No standardized dev deployment or artifact validation path is implemented for this service.", + reason: unsupportedReason, } : undefined, - liveApply: dryRunBlockedReason !== null + liveApply: unsupported + ? { + allowed: false, + reason: unsupportedReason, + dryRunOnly: true, + } + : dryRunBlockedReason !== null ? { allowed: false, reason: dryRunBlockedReason,