From 7e171dd904df5223b3d278dcfeb7c30381c2eccc Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 21 May 2026 14:31:00 +0000 Subject: [PATCH] test: guard code queue artifact dry-run readiness --- docs/reference/artifact-registry.md | 11 +- docs/reference/cicd-standardization.md | 2 +- docs/reference/codex-deploy.md | 2 +- docs/reference/deploy.md | 6 +- docs/reference/user-service-delivery.md | 2 +- .../code-queue-cicd-dry-run-contract-test.ts | 39 +++++ ...ue-mgr-artifact-readiness-contract-test.ts | 5 + scripts/src/artifact-registry.ts | 163 ++++++++++++++++-- scripts/src/deploy.ts | 88 +++++++++- scripts/src/help.ts | 4 +- 10 files changed, 291 insertions(+), 31 deletions(-) diff --git a/docs/reference/artifact-registry.md b/docs/reference/artifact-registry.md index e168ee98..6f7eab82 100644 --- a/docs/reference/artifact-registry.md +++ b/docs/reference/artifact-registry.md @@ -76,7 +76,7 @@ bun scripts/cli.ts artifact-registry deploy-service --service mdtodo --env dev - bun scripts/cli.ts artifact-registry deploy-service --service mdtodo --env prod --commit bun scripts/cli.ts artifact-registry deploy-service --service claudeqq --env dev --commit bun scripts/cli.ts artifact-registry deploy-service --service claudeqq --env prod --commit -bun scripts/cli.ts artifact-registry deploy-service --service code-queue --env dev --commit +bun scripts/cli.ts artifact-registry deploy-service --service code-queue --env dev --commit --dry-run ``` `plan` 输出架构边界、依赖项、默认路径和 backend-core artifact CD 流程。`render` 输出 systemd unit、Compose 文件和 registry config 的完整内容与 SHA-256。`install --dry-run` 只列出将要执行的远端动作,不写 D601 文件、不启动容器、不 reload systemd。 @@ -85,7 +85,7 @@ bun scripts/cli.ts artifact-registry deploy-service --service code-queue --env d `deploy-backend-core` 是旧兼容入口,当前作为标准路径已禁用。Backend-core CD 必须使用 `bun scripts/cli.ts deploy apply --env dev --service backend-core --commit ` 或 `bun scripts/cli.ts deploy apply --env prod --service backend-core --commit `,由 deploy reconciler 先执行共同的 artifact-consumer guardrail,再调用通用 `deploy-service` consumer。旧入口只能返回 structured deprecated 结果,不得绕过 `deploy apply --env ...`。 -`deploy-service` 是标准化后的最小通用 artifact consumer。它目前支持 dev/prod `backend-core`、`baidu-netdisk`、prod/dev `frontend`、`decision-center`、`mdtodo`、`claudeqq`、`project-manager`、`oa-event-flow`、`code-queue-mgr`、`todo-note`、`findjob`、`pipeline`、`met-nonlinear` 和 `k3sctl-adapter`,以及 dev-only `code-queue`。所有路径都必须先通过 D601 registry 的 commit-pinned manifest 校验,再执行拉取、导入/retag、部署和健康验证;`code-queue --env prod` 必须返回 structured unsupported,不能回退到生产 artifact deploy、rollout 或 manifest 变更;`code-queue-mgr` 的 prod live apply 仍需 supervisor 单独确认,`met-nonlinear` 与 `k3sctl-adapter` 当前只提供 plan/dry-run: +`deploy-service` 是标准化后的最小通用 artifact consumer。它目前支持 dev/prod `backend-core`、`baidu-netdisk`、prod/dev `frontend`、`decision-center`、`mdtodo`、`claudeqq`、`project-manager`、`oa-event-flow`、`code-queue-mgr`、`todo-note`、`findjob`、`pipeline`、`met-nonlinear` 和 `k3sctl-adapter`,以及 dev-only `code-queue` dry-run。所有路径都必须先通过 D601 registry 的 commit-pinned manifest 校验,再执行拉取、导入/retag、部署和健康验证;`code-queue --env dev --dry-run` 必须显示 `requiresSupervisorApproval=true`、`selfBootstrapGuard`、commit tag/digest provenance、pull-only/no-build build boundary 和不会影响 production scheduler/runner/active tasks 的 `affectedRuntime` / `excludedTargets`;非 dry-run DEV apply 必须返回 supervisor/human authorization gate,不能由正在运行的 Code Queue task 自举执行;`code-queue --env prod` 必须返回 structured unsupported,不能回退到生产 artifact deploy、rollout 或 manifest 变更;`code-queue-mgr` 的 prod live apply 仍需 supervisor 单独确认,`met-nonlinear` 与 `k3sctl-adapter` 当前只提供 plan/dry-run: ```bash bun scripts/cli.ts artifact-registry deploy-service --service baidu-netdisk --commit --run-now @@ -103,12 +103,15 @@ bun scripts/cli.ts artifact-registry deploy-service --env dev --service mdtodo - bun scripts/cli.ts artifact-registry deploy-service --env prod --service mdtodo --commit --run-now bun scripts/cli.ts artifact-registry deploy-service --env dev --service claudeqq --commit --run-now bun scripts/cli.ts artifact-registry deploy-service --env prod --service claudeqq --commit --run-now -bun scripts/cli.ts artifact-registry deploy-service --env dev --service code-queue --commit --run-now ``` `code-queue-mgr` 是主 server Compose sidecar,不是 D601 Code Queue scheduler/runner。`artifact-registry deploy-service --env prod --service code-queue-mgr --commit --dry-run` 必须显示 target 仅为 `composeService=code-queue-mgr` / `containerName=code-queue-mgr-backend`,并在 `excludedTargets` 中说明不会触碰 `code-queue` scheduler、runner、任务、interrupt 或 cancel 状态。真实 prod apply 仍受 supervisor-only gate 保护;未经单服务授权不得执行非 dry-run apply。 -`bun scripts/code-queue-mgr-artifact-readiness-contract-test.ts` 是这个 supervisor gate 的 focused source/contract 检查:它验证 prod `deploy.json` pin 到包含 stats endpoint 的 `code-queue-mgr` commit、`CI.json` 仍使用 `ci publish-user-service` 发布 `unidesk/code-queue-mgr:`、Rust runtime 暴露 `/api/tasks/stats` 且不返回 `skipped` 统计、`artifact-registry deploy-service --env prod --service code-queue-mgr --dry-run` 与 `deploy apply --env prod --service code-queue-mgr --dry-run` 都只计划 `code-queue-mgr-backend` 单个 Compose service,并保持 supervisor-only live apply 与 scheduler/runner/tasks/interrupt/cancel excluded target 合同。 +`bun scripts/code-queue-cicd-dry-run-contract-test.ts` 是 Code Queue 自举边界的 focused contract:它验证 `deploy plan`、`deploy apply --dry-run` 和 `artifact-registry deploy-service --dry-run` 都只给出 DEV 计划证据,显示 `requiresSupervisorApproval` / `selfBootstrapGuard`,保持 pull-only/no-build 和 digest provenance,并证明 PROD unsupported 且不会影响生产 scheduler/runner/active tasks、interrupt 或 cancel。 + +`bun scripts/code-queue-mgr-artifact-readiness-contract-test.ts` 是 Code Queue Manager supervisor gate 的 focused source/contract 检查:它验证 prod `deploy.json` pin 到包含 stats endpoint 的 `code-queue-mgr` commit、`CI.json` 仍使用 `ci publish-user-service` 发布 `unidesk/code-queue-mgr:`、Rust runtime 暴露 `/api/tasks/stats` 且不返回 `skipped` 统计、`artifact-registry deploy-service --env prod --service code-queue-mgr --dry-run` 与 `deploy apply --env prod --service code-queue-mgr --dry-run` 都只计划 `code-queue-mgr-backend` 单个 Compose service,并保持 supervisor-only live apply、`selfBootstrapGuard` 与 scheduler/runner/tasks/interrupt/cancel excluded target 合同。 + +Code Queue DEV live apply is not an automation default. The reviewed path is: publish the commit-pinned artifact, run `deploy plan --env dev --service code-queue` and `deploy apply --env dev --service code-queue --dry-run` or the equivalent artifact-registry dry-run, then have a human operator or supervisor authorize any DEV apply outside the running Code Queue task. PROD has no apply authorization point in this phase. dry-run 输出会暴露 registry probe URL、required labels、目标 image、部署形态、目标 Deployment 列表、回滚信息,以及结构化 `registry`、`source` 和 `build` 字段;`registry.digest` 在 dry-run 中为 `null`,`digestSource` 指明真实 digest 来自 live registry manifest HEAD,`build.willCompile`、`build.willRunCargoBuild`、`build.willRunDockerBuild` 和 `build.willRunDockerComposeBuild` 必须为 false。dry-run 不得打印运行时密钥。`backend-core --env dev` 使用 `ci publish-backend-core` 产出的 `127.0.0.1:5000/unidesk/backend-core:`,导入 D601 native k3s containerd,更新 `unidesk-dev/backend-core-dev` Deployment,设置 image/env/annotations,并通过 Kubernetes API service proxy 验证 `/health.deploy.commit` 和 `deploy.requestedCommit`;CD 阶段不得运行 Rust 编译或 Docker build。`baidu-netdisk` 是 PGDATA 备份链路依赖服务;它的 Compose artifact 路径会通过 provider-gateway Host SSH 把 `unidesk/baidu-netdisk:` 流式拉到 master server,retag 为 `baidu-netdisk` 和 `baidu-netdisk:`,在 canonical UniDesk 根目录使用 `providerGateway.upgrade.composeEnvFile` 指向的受控 env 文件写入 `UNIDESK_BAIDU_NETDISK_DEPLOY_*`,只 recreate `baidu-netdisk` service,并验证容器 image label 与 `/health.deploy.commit`。live apply 在 recreate 前必须确认受控 env 文件中存在 `UNIDESK_BAIDU_NETDISK_CLIENT_ID`、`UNIDESK_BAIDU_NETDISK_CLIENT_SECRET` 和 `UNIDESK_BAIDU_NETDISK_TOKEN_KEY`,输出只能包含 present/length/boolean;dry-run 输出 `runtimeSecrets.secretSource`、`requiredSecretsPresent`、`missingSecretKeys` 和 `recommendedAction`,让 dev secret source blocker 可诊断但不打印值;recreate 后必须验收 `/health.auth.configured`、`clientIdConfigured`、`clientSecretConfigured`、`tokenKeyConfigured` 和 `loggedIn` 全部为 true,否则返回失败或 degraded,并提示先恢复 env、单服务 recreate、再验证 `microservice health baidu-netdisk`。`findjob`、`pipeline` 和 `met-nonlinear` 的 D601 direct Compose 路径在 D601 本机验证 registry manifest、pull image、retag stable image、写入 `UNIDESK_*_DEPLOY_*` labels/env,并用 `docker compose up -d --no-build --no-deps --force-recreate ` 重新拉起对应 compose service;其中 `met-nonlinear` 当前因为 registered Dockerfile 和 long-running service contract 不一致而 live deploy blocked。`k3sctl-adapter` 是基础设施控制桥,只做 plan/dry-run,真实生产部署需要 supervisor 单独确认。`frontend --env prod` 使用同一 Compose artifact consumer,流式拉取 `unidesk/frontend:`,retag 为 `unidesk-frontend` 和 `unidesk-frontend:`,写入 `UNIDESK_FRONTEND_DEPLOY_*`,只 recreate `frontend` service,并验证 image label 与 `/health.deploy.commit`。`frontend --env dev`、`decision-center`、`mdtodo`、`claudeqq` 和 dev `code-queue` 的 k3s 路径会在 D601 上验证 commit image、导入 native k3s containerd、更新 Deployment image/env/annotations,并通过 Kubernetes API service proxy 验证 `/health` 中的 live commit 与 requested commit;dev frontend 还会在 rollout 前把主 server `config.json.auth` 同步到 `unidesk-dev` Secret/ConfigMap。`decision-center --env dev` 落到 `unidesk-dev/decision-center-dev`,prod 落到 `unidesk/decision-center`;`mdtodo` 和 `claudeqq` 使用同样的 dev 后 prod k3s consumer 结构。`code-queue --env dev` 只更新 `unidesk-dev` 中的 scheduler/read/write/provider-egress-proxy dev Deployments,prod 没有 consumer target。D601 direct Compose consumer 与 k3s-managed consumer 的区别是:前者只接触 D601 Docker/Compose 项目和私有 backend health,不创建 Kubernetes 对象;后者只通过 native k3s Deployment/Service、containerd import 和 Kubernetes API service proxy 验证 live commit。回滚信息通过同一 artifact consumer 的 `rollback` 字段暴露,提示操作者重新对一个旧 commit 运行相同命令,而不是切回 legacy maintenance-channel 构建。 diff --git a/docs/reference/cicd-standardization.md b/docs/reference/cicd-standardization.md index 3e4bfb07..8ecd57d5 100644 --- a/docs/reference/cicd-standardization.md +++ b/docs/reference/cicd-standardization.md @@ -272,7 +272,7 @@ 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 | +| CD dry-run | deploy/artifact-registry CLI | Read `origin/master:deploy.json`, show `unidesk-dev` target objects, required registry image tag/digest provenance, pull-only/no-runtime-build validation, `selfBootstrapGuard`, `requiresSupervisorApproval` 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 | diff --git a/docs/reference/codex-deploy.md b/docs/reference/codex-deploy.md index 916e1949..60c659f7 100644 --- a/docs/reference/codex-deploy.md +++ b/docs/reference/codex-deploy.md @@ -2,7 +2,7 @@ `bun scripts/cli.ts codex deploy ` 是旧兼容入口,现已禁用。原因是它会通过 backend-core `host.ssh` 维护通道直连 D601 部署 Code Queue,把维护入口扩张成第二套部署系统。 -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`。 +Code Queue 后续正式生产部署必须走一条受控 CD 路径并单独审查;当前阶段只提供 dev artifact consumer 的 dry-run/source/contract readiness。`deploy apply --env dev --service code-queue --dry-run` 或 `artifact-registry deploy-service --env dev --service code-queue --dry-run` 可以计划消费 D601 registry 中的 `unidesk/code-queue:`,但输出必须显示 `selfBootstrapGuard`、`requiresSupervisorApproval`、pull-only/no-build、image tag/digest provenance,并说明只会在获授权后更新 `unidesk-dev` Code Queue execution slice。非 dry-run DEV apply 必须由 human operator/supervisor 在 Code Queue 任务之外授权;当前 Code Queue runner 不能自己上线 Code Queue。`--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: diff --git a/docs/reference/deploy.md b/docs/reference/deploy.md index 03b892db..d28d5822 100644 --- a/docs/reference/deploy.md +++ b/docs/reference/deploy.md @@ -100,7 +100,7 @@ Phase 5 introduces the dev Code Queue execution manifest at `src/components/micr All dev Code Queue components must use `unidesk-dev-runtime-config` and `unidesk-dev-runtime-secrets`, connect to `postgres-dev.../unidesk_dev`, write logs and state under `/home/ubuntu/unidesk-dev-code-queue-deploy/state`, and expose HTTP on 4222 only as ClusterIP services. The scheduler uses `CODE_QUEUE_MAIN_PROVIDER_ID=D601-dev`, `CODE_QUEUE_WORKDIR=/workspace-dev`, `CODE_QUEUE_REMOTE_WORKDIR=/home/ubuntu/unidesk-dev-workspace`, disables ClaudeQQ notifications by default, and does not use the production `d601-tcp-egress-gateway` or production PostgreSQL route. -Maintenance-channel direct D601 apply must not deploy dev Code Queue and the old `codex deploy` compatibility entry remains disabled. Dev Code Queue deployment is allowed only as the D601 registry artifact consumer for `deploy apply --env dev --service code-queue` or the equivalent `artifact-registry deploy-service --env dev --service code-queue`: it verifies the existing `127.0.0.1:5000/unidesk/code-queue:` artifact, imports it into native k3s containerd, applies only the `unidesk-dev` Code Queue manifest, stamps `code-queue-scheduler-dev`, `code-queue-read-dev`, `code-queue-write-dev` and `d601-dev-provider-egress-proxy`, and verifies the scheduler Service through the Kubernetes API service proxy. `deploy apply --env prod --service code-queue` and `artifact-registry deploy-service --env prod --service code-queue` must return explicit unsupported output and must not mutate production Code Queue manifests, Deployments or rollouts. The scheduler has an explicit 5Gi memory limit and must use `Recreate` rollout strategy so an update does not temporarily require two scheduler replicas under the namespace quota. All dev Code Queue containers must set CPU limits so the namespace `LimitRange` does not inject a quota-breaking default CPU limit. Live health verification uses the Kubernetes API service proxy for the dev ClusterIP Service, not `kubectl exec` or debug binaries inside the application image. This dev execution slice proves artifact deployability, health and dev database isolation; wiring the dev frontend stable `code-queue` route through a dev `code-queue-mgr` is a separate later phase. +Maintenance-channel direct D601 apply must not deploy dev Code Queue and the old `codex deploy` compatibility entry remains disabled. Dev Code Queue deployment is allowed only as the D601 registry artifact consumer dry-run for `deploy apply --env dev --service code-queue --dry-run` or the equivalent `artifact-registry deploy-service --env dev --service code-queue --dry-run`: it verifies the planned `127.0.0.1:5000/unidesk/code-queue:` artifact, target image tag, pull-only/no-build boundary, `selfBootstrapGuard`, `requiresSupervisorApproval`, and the `unidesk-dev` target list. A non-dry-run DEV apply may import the artifact into native k3s containerd, apply only the `unidesk-dev` Code Queue manifest, stamp `code-queue-scheduler-dev`, `code-queue-read-dev`, `code-queue-write-dev` and `d601-dev-provider-egress-proxy`, and verify the scheduler Service only after a human operator or supervisor authorizes the action outside the running Code Queue task. `deploy apply --env prod --service code-queue` and `artifact-registry deploy-service --env prod --service code-queue` must return explicit unsupported output and must not mutate production Code Queue manifests, Deployments or rollouts. The scheduler has an explicit 5Gi memory limit and must use `Recreate` rollout strategy so an update does not temporarily require two scheduler replicas under the namespace quota. All dev Code Queue containers must set CPU limits so the namespace `LimitRange` does not inject a quota-breaking default CPU limit. Live health verification uses the Kubernetes API service proxy for the dev ClusterIP Service, not `kubectl exec` or debug binaries inside the application image. This dev execution slice proves artifact deployability, health and dev database isolation; wiring the dev frontend stable `code-queue` route through a dev `code-queue-mgr` is a separate later phase. Production `code-queue-mgr` is a separate main-server Compose sidecar artifact consumer. `deploy apply --env prod --service code-queue-mgr --dry-run` may plan only the `code-queue-mgr` Compose service/container and must surface that D601 Code Queue scheduler/runner, queued tasks, interrupts and cancellations are excluded targets. Non-dry-run production apply for this sidecar remains supervisor-gated even when the artifact exists. @@ -114,9 +114,9 @@ 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.registry`, `artifactConsumer.source`, `artifactConsumer.build`, `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 retained only as a legacy classification and should not appear for backend-core. Artifact consumer plan items must explicitly report `noRuntimeSourceBuild=true`, expose registry/source/build boundaries including digest provenance, and list forbidden build/public exposure actions. Services with runtime secret gates, currently `baidu-netdisk`, must also expose a redacted `artifactConsumer.runtimeSecrets` contract with `secretSource`, `requiredSecretsPresent`, `missingSecretKeys` and `recommendedAction`; this contract may report key names, booleans and lengths only, never secret values. 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. +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; the plan must set `artifactConsumer.dryRunOnly=true`, `liveApply.allowed=false`, `requiresSupervisorApproval=true`, and expose a `selfBootstrapGuard` so a running Code Queue task cannot authorize its own replacement. 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` redeploys 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 `backend-core`/`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 backend-core --commit ` or `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. +`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` redeploys 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 `backend-core`/`frontend`/`baidu-netdisk`/`decision-center`/`mdtodo`/`claudeqq`/dev-only `code-queue` dry-run/authorized apply, `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 backend-core --commit ` or `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/docs/reference/user-service-delivery.md b/docs/reference/user-service-delivery.md index f8db13e6..ef0f0e2a 100644 --- a/docs/reference/user-service-delivery.md +++ b/docs/reference/user-service-delivery.md @@ -182,5 +182,5 @@ Code Queue is dev-only for this artifact-consumer policy. - The minimal standard artifact command is `bun scripts/cli.ts ci publish-user-service --service code-queue --commit --wait-ms 1200000`. - The expected artifact is `127.0.0.1:5000/unidesk/code-queue:` plus its registry digest from the CI output. -- Dev CD may consume that artifact only with `deploy apply --env dev --service code-queue`, updating `unidesk-dev` scheduler/read/write/provider-egress-proxy objects and verifying health through the Kubernetes API service proxy. +- Dev CD may be planned only with `deploy apply --env dev --service code-queue --dry-run` or the equivalent artifact-registry dry-run from a Code Queue task. The dry-run must expose `selfBootstrapGuard`, `requiresSupervisorApproval`, commit tag/digest provenance and pull-only/no-build behavior; any real DEV apply that updates `unidesk-dev` scheduler/read/write/provider-egress-proxy objects requires human operator or supervisor authorization outside the running Code Queue task. - Production artifact deploy, production rollout and production manifest mutation for `code-queue` are unsupported and must fail visibly. diff --git a/scripts/code-queue-cicd-dry-run-contract-test.ts b/scripts/code-queue-cicd-dry-run-contract-test.ts index e00a67fb..0b8360e9 100644 --- a/scripts/code-queue-cicd-dry-run-contract-test.ts +++ b/scripts/code-queue-cicd-dry-run-contract-test.ts @@ -48,12 +48,28 @@ 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"); +const devPlanLiveApply = asRecord(devPlan.liveApply, "dev plan liveApply"); +const devPlanGuard = asRecord(devBoundary.selfBootstrapGuard, "dev boundary selfBootstrapGuard"); +const devArtifactGuard = asRecord(devArtifact.selfBootstrapGuard, "dev artifact selfBootstrapGuard"); +const devBuild = asRecord(devArtifact.build, "dev artifact build"); +const devRegistry = asRecord(devArtifact.registry, "dev artifact registry"); 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(devArtifact.dryRunOnly === true, "dev Code Queue artifact consumer must be dry-run-only until human authorization", devArtifact); +assertCondition(String(devArtifact.blockedReason ?? "").includes("self-bootstrap"), "dev Code Queue blocked reason should name self-bootstrap", devArtifact); +assertCondition(devArtifact.requiresSupervisorApproval === true, "dev Code Queue artifact consumer should require supervisor approval", devArtifact); +assertCondition(devBuild.willRunDockerBuild === false && devBuild.willRunDockerComposeBuild === false, "dev Code Queue CD must be pull-only/no-build", devBuild); +assertCondition(devRegistry.tag === devPlan.commitId && String(devRegistry.imageRef ?? "").endsWith(`:${devPlan.commitId}`), "dev Code Queue registry plan must expose commit tag/image", devRegistry); 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(devPlanLiveApply.allowed === false, "dev Code Queue live apply must be blocked for Code Queue automation", devPlanLiveApply); +assertCondition(devPlanLiveApply.requiresSupervisorApproval === true, "dev Code Queue live apply must require supervisor approval", devPlanLiveApply); +assertCondition(devPlanGuard.selfBootstrapBlocked === true && devArtifactGuard.selfBootstrapBlocked === true, "dev Code Queue must expose self-bootstrap guards", { devPlanGuard, devArtifactGuard }); assertCondition(devCd.prodMutationAllowed === false, "dev Code Queue boundary must prohibit production mutation", devCd); +assertCondition(devCd.liveApplyAllowed === false, "dev Code Queue boundary must not advertise direct live apply", devCd); +assertCondition(devCd.liveApplyCommandShape === null, "dev Code Queue boundary must not advertise a run-now command", devCd); +assertCondition(devCd.requiresSupervisorApproval === true, "dev Code Queue boundary should require supervisor approval", 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); @@ -64,6 +80,7 @@ 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 prodGuard = asRecord(prodBoundary.selfBootstrapGuard, "prod boundary selfBootstrapGuard"); const prodLiveApply = asRecord(prodPlan.liveApply, "prod liveApply"); const prodUnsupported = asRecord(prodPlan.unsupported, "prod unsupported"); const prodForbidden = asStringArray(prodTarget.forbiddenActions, "prod forbiddenActions"); @@ -72,6 +89,7 @@ assertCondition(prodPlan.deploymentPath === "unsupported", "prod Code Queue depl 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(prodArtifact.requiresSupervisorApproval === true, "prod Code Queue artifact consumer should require supervisor approval", 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); @@ -79,6 +97,8 @@ assertCondition(String(prodLiveApply.reason ?? "").includes("production CD is in 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(prodCd.requiresSupervisorApproval === true, "prod Code Queue boundary should require supervisor approval", prodCd); +assertCondition(prodGuard.selfBootstrapBlocked === true, "prod Code Queue boundary should expose self-bootstrap guard", prodGuard); 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); @@ -97,7 +117,23 @@ const devArtifactDryRun = asRecord(runCli([ "--dry-run", ], 0).data, "dev artifact-registry dry-run"); const artifactTarget = asRecord(devArtifactDryRun.target, "artifact dry-run target"); +const artifactRegistry = asRecord(devArtifactDryRun.registry, "artifact dry-run registry"); +const artifactBuild = asRecord(devArtifactDryRun.build, "artifact dry-run build"); +const artifactLiveApply = asRecord(devArtifactDryRun.liveApply, "artifact dry-run liveApply"); +const artifactGuard = asRecord(devArtifactDryRun.selfBootstrapGuard, "artifact dry-run selfBootstrapGuard"); +const artifactAffectedRuntime = asRecord(devArtifactDryRun.affectedRuntime, "artifact dry-run affectedRuntime"); +const artifactExcludedTargets = JSON.stringify(devArtifactDryRun.excludedTargets); assertCondition(devArtifactDryRun.mutation === false, "artifact-registry dev dry-run must be non-mutating", devArtifactDryRun); +assertCondition(devArtifactDryRun.requiresSupervisorApproval === true, "artifact-registry dev dry-run must require supervisor approval", devArtifactDryRun); +assertCondition(artifactRegistry.imageRef === `127.0.0.1:5000/unidesk/code-queue:${commit}`, "artifact-registry dev dry-run should expose commit-pinned image", artifactRegistry); +assertCondition(artifactRegistry.digest === null && String(artifactRegistry.digestSource ?? "").includes("manifest HEAD"), "artifact-registry dev dry-run should expose digest provenance", artifactRegistry); +assertCondition(artifactBuild.willCompile === false && artifactBuild.willRunDockerBuild === false && artifactBuild.willRunDockerComposeBuild === false, "artifact-registry dev dry-run must be pull-only/no-build", artifactBuild); +assertCondition(artifactLiveApply.allowed === false && artifactLiveApply.policy === "supervisor-only", "artifact-registry dev dry-run must block self-bootstrap live apply", artifactLiveApply); +assertCondition(artifactLiveApply.requiresSupervisorApproval === true, "artifact-registry dev liveApply should require supervisor approval", artifactLiveApply); +assertCondition(artifactGuard.selfBootstrapBlocked === true, "artifact-registry dev dry-run must expose self-bootstrap guard", artifactGuard); +assertCondition(artifactAffectedRuntime.productionNamespaceAffected === false, "artifact-registry dev dry-run must not affect production namespace", artifactAffectedRuntime); +assertCondition(artifactAffectedRuntime.activeTaskInterruptCancelAffected === false, "artifact-registry dev dry-run must not affect active task control", artifactAffectedRuntime); +assertCondition(artifactExcludedTargets.includes("active tasks") && artifactExcludedTargets.includes("cancel"), "artifact-registry dev dry-run should exclude active task interrupt/cancel", devArtifactDryRun); assertCondition(artifactTarget.namespace === "unidesk-dev", "artifact-registry dev dry-run must target unidesk-dev", artifactTarget); const prodArtifactDryRun = asRecord(runCli([ @@ -113,6 +149,9 @@ const prodArtifactDryRun = asRecord(runCli([ ], 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); +assertCondition(prodArtifactDryRun.requiresSupervisorApproval === true, "artifact-registry prod code-queue should require supervisor approval before any future design", prodArtifactDryRun); +assertCondition(asRecord(prodArtifactDryRun.selfBootstrapGuard, "prod artifact selfBootstrapGuard").selfBootstrapBlocked === true, "artifact-registry prod code-queue should expose self-bootstrap guard", prodArtifactDryRun); +assertCondition(JSON.stringify(prodArtifactDryRun).includes("production artifact deploy") && JSON.stringify(prodArtifactDryRun).includes("active task"), "artifact-registry prod code-queue should explain prod deploy and active-task boundaries", prodArtifactDryRun); process.stdout.write(`${JSON.stringify({ ok: true, diff --git a/scripts/code-queue-mgr-artifact-readiness-contract-test.ts b/scripts/code-queue-mgr-artifact-readiness-contract-test.ts index dc4171bb..f70627e8 100644 --- a/scripts/code-queue-mgr-artifact-readiness-contract-test.ts +++ b/scripts/code-queue-mgr-artifact-readiness-contract-test.ts @@ -88,6 +88,7 @@ function assertCommonDryRun(plan: JsonRecord, deployRef: string): void { const probe = asRecord(plan.registryProbe, "dry-run registryProbe"); const target = asRecord(plan.target, "dry-run target"); const liveApply = asRecord(plan.liveApply, "dry-run liveApply"); + const guard = asRecord(plan.selfBootstrapGuard, "dry-run selfBootstrapGuard"); const validation = strings(plan.validation, "dry-run validation"); const excludedTargets = asArray(plan.excludedTargets, "dry-run excludedTargets").map((item, index) => asRecord(item, `excludedTargets[${index}]`)); const excludedText = JSON.stringify(excludedTargets); @@ -130,7 +131,11 @@ function assertCommonDryRun(plan: JsonRecord, deployRef: string): void { assertCondition(liveApply.policy === "supervisor-only", "code-queue-mgr prod live apply must be supervisor-only", liveApply); assertCondition(liveApply.allowed === false, "code-queue-mgr prod live apply must be blocked in automation", liveApply); + assertCondition(liveApply.requiresSupervisorApproval === true, "code-queue-mgr prod live apply must require supervisor approval", liveApply); assertCondition(String(liveApply.reason ?? "").includes("explicit supervisor confirmation"), "code-queue-mgr live apply reason must name supervisor confirmation", liveApply); + assertCondition(plan.requiresSupervisorApproval === true, "code-queue-mgr prod dry-run must expose top-level supervisor approval requirement", plan); + assertCondition(guard.selfBootstrapBlocked === true, "code-queue-mgr prod dry-run must expose self-bootstrap guard", guard); + assertCondition(String(guard.targetScope ?? "").includes("code-queue-mgr-backend"), "code-queue-mgr guard must name the control-plane sidecar target", guard); assertCondition(validation.some((line) => line.includes("deploy.commit/deploy.requestedCommit")), "code-queue-mgr validation must require health deploy commit metadata", validation); assertCondition(excludedText.includes("code-queue"), "code-queue-mgr excluded targets must include code-queue", excludedTargets); assertCondition(excludedText.includes("scheduler"), "code-queue-mgr excluded targets must mention scheduler", excludedTargets); diff --git a/scripts/src/artifact-registry.ts b/scripts/src/artifact-registry.ts index d5693f25..9b03881a 100644 --- a/scripts/src/artifact-registry.ts +++ b/scripts/src/artifact-registry.ts @@ -140,6 +140,8 @@ interface ArtifactConsumerSpec { targets: Partial>; prodLiveApply: "enabled" | "supervisor-only" | "unsupported"; prodLiveBlockReason?: string; + devLiveApply?: "enabled" | "supervisor-only"; + devLiveBlockReason?: string; runtimeVerification?: "strict" | "blocked"; runtimeVerificationBlockReason?: string; } @@ -503,7 +505,9 @@ const artifactConsumerSpecs: Record = { registryRepository: "unidesk/code-queue", dockerfile: "src/components/microservices/code-queue/Dockerfile", prodLiveApply: "unsupported", - prodLiveBlockReason: "code-queue is dev-only for artifact consumer validation and has no prod artifact deploy, rollout, or manifest mutation target.", + prodLiveBlockReason: "code-queue is dev-only for artifact consumer validation and has no production artifact deploy, production rollout, or production manifest mutation target.", + devLiveApply: "supervisor-only", + devLiveBlockReason: "Code Queue DEV live apply is self-bootstrap sensitive: a running Code Queue task may plan only dry-run evidence. A human operator or supervisor must explicitly authorize DEV apply outside Code Queue after reviewing the CI artifact digest and dry-run target list.", targets: { dev: { targetImage: "unidesk-code-queue:dev", @@ -1315,6 +1319,10 @@ function unsupportedService(serviceId: string, options: ArtifactRegistryOptions) function unsupportedEnvironment(spec: ArtifactConsumerSpec, options: ArtifactRegistryOptions): Record { const environment = options.environment ?? "prod"; + const codeQueueGuard = spec.serviceId === "code-queue" ? codeQueueSelfBootstrapGuard(environment) : undefined; + const reason = spec.serviceId === "code-queue" && environment === "prod" + ? spec.prodLiveBlockReason ?? "Code Queue production artifact deploy, rollout and manifest mutation are intentionally unsupported in this phase." + : `No standardized ${environment} registry artifact consumer is implemented for ${spec.serviceId}.`; return { ok: false, supported: false, @@ -1322,14 +1330,125 @@ function unsupportedEnvironment(spec: ArtifactConsumerSpec, options: ArtifactReg serviceId: spec.serviceId, environment, providerId: options.providerId, - reason: `No standardized ${environment} registry artifact consumer is implemented for ${spec.serviceId}.`, + reason, supportedEnvironments: Object.keys(spec.targets), + requiresSupervisorApproval: spec.serviceId === "code-queue", + selfBootstrapGuard: codeQueueGuard, + affectedRuntime: spec.serviceId === "code-queue" ? codeQueueAffectedRuntime(environment, false) : undefined, + excludedTargets: spec.serviceId === "code-queue" ? codeQueueExcludedTargets(environment) : undefined, policy: "artifact CD must not silently fall back to maintenance-channel source builds or legacy direct deployment", }; } +function artifactConsumerLivePolicy(spec: ArtifactConsumerSpec, environment: ArtifactDeployEnvironment): "enabled" | "supervisor-only" | "unsupported" { + return environment === "prod" ? spec.prodLiveApply : spec.devLiveApply ?? "enabled"; +} + +function artifactConsumerLiveBlockReason(spec: ArtifactConsumerSpec, environment: ArtifactDeployEnvironment): string | null { + if (spec.runtimeVerificationBlockReason !== undefined) return spec.runtimeVerificationBlockReason; + return environment === "prod" ? spec.prodLiveBlockReason ?? null : spec.devLiveBlockReason ?? null; +} + +function codeQueueExcludedTargets(environment: ArtifactDeployEnvironment): Record[] { + const excluded: Record[] = [ + { + namespace: "unidesk", + deployments: ["code-queue", "code-queue-read", "code-queue-write", "d601-provider-egress-proxy", "d601-tcp-egress-gateway"], + reason: "production Code Queue execution-plane Deployments are outside this artifact consumer and must not be mutated by dry-run or self-bootstrap automation.", + }, + { + operations: ["restart scheduler", "restart runner", "interrupt active tasks", "cancel active tasks"], + reason: "artifact CD planning must not alter scheduler/runner lifecycle or active task control state.", + }, + ]; + if (environment === "dev") { + excluded.push({ + namespace: "unidesk-dev", + nonDryRunMutation: "requires explicit supervisor or human authorization outside the running Code Queue task", + reason: "DEV is the only Code Queue artifact consumer target, but self-bootstrap automation may produce dry-run evidence only.", + }); + } + return excluded; +} + +function codeQueueAffectedRuntime(environment: ArtifactDeployEnvironment, targetPlanned: boolean): Record { + return { + dryRunMutation: false, + plannedTarget: targetPlanned && environment === "dev" + ? { + namespace: "unidesk-dev", + deployments: ["code-queue-scheduler-dev", "code-queue-read-dev", "code-queue-write-dev", "d601-dev-provider-egress-proxy"], + } + : null, + productionNamespaceAffected: false, + productionSchedulerRunnerAffected: false, + activeTaskControlAffected: false, + activeTaskInterruptCancelAffected: false, + }; +} + +function codeQueueSelfBootstrapGuard(environment: ArtifactDeployEnvironment): Record { + return { + check: "code-queue-self-bootstrap-guard", + serviceId: "code-queue", + selfBootstrapBlocked: true, + requiresSupervisorApproval: true, + actorBoundary: "a running Code Queue task may publish contract evidence and dry-run plans only; it must not deploy Code Queue itself", + devApply: "requires explicit supervisor or human authorization outside the running Code Queue task", + prodApply: "unsupported until a separate supervisor-approved production Code Queue CD design exists", + allowedWithoutApproval: [ + "CI artifact publication", + "deploy plan", + "deploy apply --dry-run", + "artifact-registry deploy-service --dry-run", + ], + forbiddenActions: [ + "self-deploy Code Queue from Code Queue", + "run a non-dry-run DEV apply from Code Queue", + "production namespace mutation", + "production manifest mutation", + "scheduler or runner restart", + "active task interrupt", + "active task cancel", + ], + environment, + }; +} + +function codeQueueMgrSelfBootstrapGuard(environment: ArtifactDeployEnvironment, requiresSupervisorApproval: boolean): Record { + return { + check: "code-queue-mgr-control-plane-guard", + serviceId: "code-queue-mgr", + selfBootstrapBlocked: true, + requiresSupervisorApproval, + actorBoundary: "dry-run is allowed, but a running Code Queue task must not replace the Code Queue control-plane sidecar as part of delivering Code Queue itself", + targetScope: "main-server Compose service code-queue-mgr / container code-queue-mgr-backend only", + prodApply: "requires explicit supervisor confirmation", + environment, + forbiddenActions: [ + "mutate D601 Code Queue scheduler", + "mutate D601 Code Queue runner", + "restart active tasks", + "interrupt active tasks", + "cancel active tasks", + ], + }; +} + +function artifactConsumerSelfBootstrapGuard( + spec: ArtifactConsumerSpec, + environment: ArtifactDeployEnvironment, + requiresSupervisorApproval: boolean, +): Record | undefined { + if (spec.serviceId === "code-queue") return codeQueueSelfBootstrapGuard(environment); + if (spec.serviceId === "code-queue-mgr") return codeQueueMgrSelfBootstrapGuard(environment, requiresSupervisorApproval); + return undefined; +} + function artifactConsumerLiveBlock(spec: ArtifactConsumerSpec, options: ArtifactRegistryOptions): Record | null { const environment = options.environment ?? "prod"; + const livePolicy = artifactConsumerLivePolicy(spec, environment); + const liveReason = artifactConsumerLiveBlockReason(spec, environment); if (spec.runtimeVerification === "blocked") { return { ok: false, @@ -1347,8 +1466,8 @@ function artifactConsumerLiveBlock(spec: ArtifactConsumerSpec, options: Artifact policy: "artifact CD must not accept a healthy old service or silently fall back to legacy rebuild paths", }; } - if (environment !== "prod" || spec.prodLiveApply === "enabled") return null; - if (spec.prodLiveApply === "supervisor-only") { + if (livePolicy === "enabled") return null; + if (livePolicy === "supervisor-only") { return { ok: false, supported: true, @@ -1357,9 +1476,13 @@ function artifactConsumerLiveBlock(spec: ArtifactConsumerSpec, options: Artifact serviceId: spec.serviceId, environment, providerId: options.providerId, - reason: spec.prodLiveBlockReason ?? `${spec.serviceId} production artifact apply requires supervisor confirmation.`, - dryRunCommandShape: `bun scripts/cli.ts artifact-registry deploy-service --env prod --service ${spec.serviceId} --commit --dry-run`, - policy: "worker automation must not perform live production apply for this infrastructure control-plane service", + reason: liveReason ?? `${spec.serviceId} ${environment} artifact apply requires supervisor confirmation.`, + requiresSupervisorApproval: true, + selfBootstrapGuard: artifactConsumerSelfBootstrapGuard(spec, environment, true), + affectedRuntime: spec.serviceId === "code-queue" ? codeQueueAffectedRuntime(environment, true) : undefined, + excludedTargets: spec.serviceId === "code-queue" ? codeQueueExcludedTargets(environment) : undefined, + dryRunCommandShape: `bun scripts/cli.ts artifact-registry deploy-service --env ${environment} --service ${spec.serviceId} --commit --dry-run`, + policy: "worker automation must not perform live apply for this supervisor-gated artifact consumer", }; } return { @@ -1370,7 +1493,11 @@ function artifactConsumerLiveBlock(spec: ArtifactConsumerSpec, options: Artifact serviceId: spec.serviceId, environment, providerId: options.providerId, - reason: spec.prodLiveBlockReason ?? `${spec.serviceId} does not yet satisfy the artifact consumer runtime verification contract.`, + reason: liveReason ?? `${spec.serviceId} does not yet satisfy the artifact consumer runtime verification contract.`, + requiresSupervisorApproval: spec.serviceId === "code-queue", + selfBootstrapGuard: spec.serviceId === "code-queue" ? codeQueueSelfBootstrapGuard(environment) : undefined, + affectedRuntime: spec.serviceId === "code-queue" ? codeQueueAffectedRuntime(environment, false) : undefined, + excludedTargets: spec.serviceId === "code-queue" ? codeQueueExcludedTargets(environment) : undefined, requiredBeforeLiveApply: [ "CI can publish a commit-pinned image with matching service id, source repo, source commit, and Dockerfile labels", "runtime Compose env injects deploy commit/requestedCommit metadata", @@ -1380,6 +1507,7 @@ function artifactConsumerLiveBlock(spec: ArtifactConsumerSpec, options: Artifact }; } + function artifactImageRef(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, commit: string): string { return `127.0.0.1:${options.port}/${(options.dryRun ? options.deployJsonService?.artifact?.repository : undefined) ?? spec.registryRepository}:${commit}`; } @@ -2627,7 +2755,9 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti : []; if (drifts.length > 0) return deployJsonDriftResult(deployJsonService, environment, drifts); const verificationBlocked = spec.runtimeVerification === "blocked"; - const livePolicy = environment === "prod" ? spec.prodLiveApply : "enabled"; + const livePolicy = artifactConsumerLivePolicy(spec, environment); + const liveReason = artifactConsumerLiveBlockReason(spec, environment); + const requiresSupervisorApproval = livePolicy === "supervisor-only" || spec.serviceId === "code-queue"; const sourceImage = artifactImageRef(effectiveOptions, spec, commit); const registryEndpoint = `http://127.0.0.1:${effectiveOptions.port}`; const config = readConfig(); @@ -2689,9 +2819,13 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti boundary: `${environment} CD is artifact-consumer only: verify commit-pinned registry image, pull/import, deploy, then verify live commit/image/health; it never builds source on the runtime target`, liveApply: { policy: livePolicy, - allowed: !verificationBlocked && (environment !== "prod" || spec.prodLiveApply === "enabled"), - reason: spec.runtimeVerificationBlockReason ?? (environment === "prod" ? spec.prodLiveBlockReason ?? null : null), + allowed: !verificationBlocked && livePolicy === "enabled", + requiresSupervisorApproval, + reason: liveReason, }, + requiresSupervisorApproval, + selfBootstrapGuard: artifactConsumerSelfBootstrapGuard(spec, environment, requiresSupervisorApproval), + affectedRuntime: spec.serviceId === "code-queue" ? codeQueueAffectedRuntime(environment, true) : undefined, sourceOfTruth: deployJsonService !== null && hasDeployJsonExecutorContract(deployJsonService) ? deployJsonSourceOfTruth(deployJsonService, environment) : undefined, driftCheck: deployJsonService !== null && hasDeployJsonExecutorContract(deployJsonService) ? { ok: true, @@ -2727,6 +2861,8 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti reason: "dry-run does not mutate D601 Code Queue scheduler, runner, tasks, interrupts, or cancellations.", }, ] + : spec.serviceId === "code-queue" + ? codeQueueExcludedTargets(environment) : undefined, validation: [ "D601 registry /v2 manifest exists for the commit tag", @@ -2800,6 +2936,7 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti memory: contractRuntime.memory, health: contractRuntime.health, }, + excludedTargets: spec.serviceId === "code-queue" ? codeQueueExcludedTargets(environment) : undefined, validation: [ "D601 registry /v2 manifest exists for the commit tag before mutation", "D601 Docker-pulled image labels match service id, source repo, source commit, and Dockerfile", @@ -3183,7 +3320,7 @@ function localHelp(): Record { "bun scripts/cli.ts artifact-registry deploy-service --env prod --service mdtodo --commit [--dry-run] [--run-now] [--provider-id D601]", "bun scripts/cli.ts artifact-registry deploy-service --env dev --service claudeqq --commit [--dry-run] [--run-now] [--provider-id D601]", "bun scripts/cli.ts artifact-registry deploy-service --env prod --service claudeqq --commit [--dry-run] [--run-now] [--provider-id D601]", - "bun scripts/cli.ts artifact-registry deploy-service --env dev --service code-queue --commit [--dry-run] [--run-now] [--provider-id D601]", + "bun scripts/cli.ts artifact-registry deploy-service --env dev --service code-queue --commit --dry-run [--provider-id D601]", ], firstStage: "install now writes the rendered systemd/Compose/config files and starts the registry", artifactConsumers: { @@ -3218,7 +3355,7 @@ function localHelp(): Record { "bun scripts/cli.ts deploy apply --env dev --service met-nonlinear --dry-run", "bun scripts/cli.ts deploy apply --env dev --service mdtodo", "bun scripts/cli.ts deploy apply --env dev --service claudeqq", - "bun scripts/cli.ts deploy apply --env dev --service code-queue", + "bun scripts/cli.ts deploy apply --env dev --service code-queue --dry-run", ], devOnlyConsumers: ["code-queue"], rollbackShape: "rerun the same artifact consumer with a previous commit-pinned image", diff --git a/scripts/src/deploy.ts b/scripts/src/deploy.ts index db66a5f6..bba25667 100644 --- a/scripts/src/deploy.ts +++ b/scripts/src/deploy.ts @@ -158,6 +158,9 @@ const prodArtifactLiveApplyBlockedServiceIds = new Map([ ["met-nonlinear", "met-nonlinear is blocked for live artifact deploy because config.json points at docker/unidesk/Dockerfile.ml while the compose service is met-nonlinear-ts."], ["k3sctl-adapter", "k3sctl-adapter is an infrastructure control bridge; this executor exposes artifact consumer plan/dry-run only. Real production deployment requires supervisor confirmation outside this task."], ]); +const devArtifactLiveApplyBlockedServiceIds = new Map([ + ["code-queue", "Code Queue DEV live apply is self-bootstrap sensitive: a running Code Queue task may produce dry-run evidence only. A human operator or supervisor must explicitly authorize DEV apply outside Code Queue after reviewing the CI artifact digest and dry-run target list."], +]); const artifactConsumerDryRunBlockedServiceIds = new Map([ ["met-nonlinear", "runtime-verification-blocked: CI publishes the ML image Dockerfile contract, but the long-running Compose service is met-nonlinear-ts; CD cannot yet prove the running container image label matches the requested commit."], ]); @@ -1141,11 +1144,41 @@ function unsupportedPlanTarget(serviceId: string, environment: DeployEnvironment }; } +function codeQueueSelfBootstrapGuard(environment: DeployEnvironment): Record | undefined { + if (environment !== "dev" && environment !== "prod") return undefined; + return { + check: "code-queue-self-bootstrap-guard", + selfBootstrapBlocked: true, + requiresSupervisorApproval: true, + actorBoundary: "a running Code Queue task may publish contract evidence and dry-run plans only; it must not deploy Code Queue itself", + devApply: "requires explicit supervisor or human authorization outside the running Code Queue task", + prodApply: "unsupported until a separate supervisor-approved production Code Queue CD design exists", + allowedWithoutApproval: [ + "CI artifact publication", + "deploy plan", + "deploy apply --dry-run", + "artifact-registry deploy-service --dry-run", + ], + forbiddenActions: [ + "self-deploy Code Queue from Code Queue", + "run a non-dry-run DEV apply from Code Queue", + "production namespace mutation", + "production manifest mutation", + "scheduler or runner restart", + "active task interrupt", + "active task cancel", + ], + environment, + }; +} + function codeQueueCiCdBoundary(serviceId: string, environment: DeployEnvironment): Record | undefined { if (serviceId !== "code-queue") return undefined; return { serviceId, selfDeployBlocked: true, + requiresSupervisorApproval: true, + selfBootstrapGuard: codeQueueSelfBootstrapGuard(environment), ciProducer: { command: "bun scripts/cli.ts ci publish-user-service --service code-queue --commit ", allowed: true, @@ -1157,7 +1190,9 @@ function codeQueueCiCdBoundary(serviceId: string, environment: DeployEnvironment ? { 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", + liveApplyCommandShape: null, + liveApplyAllowed: false, + requiresSupervisorApproval: true, 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, @@ -1166,6 +1201,8 @@ function codeQueueCiCdBoundary(serviceId: string, environment: DeployEnvironment environment: "prod", dryRunCommand: "bun scripts/cli.ts deploy plan --env prod --service code-queue", liveApplyCommandShape: null, + liveApplyAllowed: false, + requiresSupervisorApproval: true, allowedTarget: null, manualAuthorizationPoint: "PROD requires a future supervisor-approved Code Queue CD design; current runner output is plan/unsupported only", prodMutationAllowed: false, @@ -3072,6 +3109,10 @@ function environmentDryRunPlan( : planTarget; const registryRepository = service.artifact?.repository ?? `unidesk/${service.id}`; const stableImage = service.consumer?.target.stableImage; + const liveApplyBlockedReason = environment === "prod" + ? prodArtifactLiveApplyBlockedServiceIds.get(service.id) ?? null + : devArtifactLiveApplyBlockedServiceIds.get(service.id) ?? null; + const dryRunOnly = unsupported || liveApplyBlockedReason !== null || dryRunBlockedReason !== null; return { id: service.id, repo: service.repo, @@ -3119,8 +3160,10 @@ function environmentDryRunPlan( "unidesk.ai/dockerfile": serviceConfig.repository.dockerfile, }, noRuntimeSourceBuild: unsupported || (service.consumer?.noRuntimeSourceBuild ?? effectivePlanKind !== "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), + dryRunOnly, + blockedReason: unsupportedReason ?? dryRunBlockedReason ?? liveApplyBlockedReason, + requiresSupervisorApproval: service.id === "code-queue" || liveApplyBlockedReason !== null, + selfBootstrapGuard: service.id === "code-queue" ? codeQueueSelfBootstrapGuard(environment) : undefined, runtimeSecrets, sourceOfTruth: deployJsonContract ? deployJsonSourceOfTruth(service, environment) : undefined, driftCheck: deployJsonContract ? { @@ -3162,11 +3205,12 @@ function environmentDryRunPlan( reason: dryRunBlockedReason, dryRunOnly: true, } - : environment === "prod" && prodArtifactLiveApplyBlockedServiceIds.has(service.id) + : liveApplyBlockedReason !== null ? { allowed: false, - reason: prodArtifactLiveApplyBlockedServiceIds.get(service.id), + reason: liveApplyBlockedReason, dryRunOnly: true, + requiresSupervisorApproval: true, } : environment === "prod" ? { allowed: prodArtifactConsumerServiceIds.has(service.id) } @@ -3240,7 +3284,17 @@ function prodArtifactUnsupportedResult(services: DeployManifestService[]): Recor repo: service.repo, commitId: service.commitId, supported: false, - reason: "No standardized prod D601 registry artifact consumer is implemented for this service.", + reason: unsupportedEnvironmentPlanReason(service.id, "prod"), + requiresSupervisorApproval: service.id === "code-queue", + selfBootstrapGuard: service.id === "code-queue" ? codeQueueSelfBootstrapGuard("prod") : undefined, + affectedRuntime: service.id === "code-queue" ? { + dryRunMutation: false, + plannedTarget: null, + productionNamespaceAffected: false, + productionSchedulerRunnerAffected: false, + activeTaskControlAffected: false, + activeTaskInterruptCancelAffected: false, + } : undefined, })), policy: "prod deploy must not silently fall back to legacy D601 maintenance-channel source builds", supportedServices: Array.from(prodArtifactConsumerServiceIds), @@ -3294,6 +3348,26 @@ function prodArtifactLiveApplyBlockedResult(services: DeployManifestService[]): }; } +function devArtifactLiveApplyBlockedResult(services: DeployManifestService[]): Record { + return { + ok: false, + supported: true, + error: "live-dev-apply-blocked", + services: services.map((service) => ({ + id: service.id, + repo: service.repo, + commitId: service.commitId, + supported: true, + liveApplyAllowed: false, + requiresSupervisorApproval: true, + reason: devArtifactLiveApplyBlockedServiceIds.get(service.id) ?? "live DEV artifact apply is blocked by policy", + selfBootstrapGuard: service.id === "code-queue" ? codeQueueSelfBootstrapGuard("dev") : undefined, + })), + policy: "dev dry-run/plan is available, but a running Code Queue task must not perform live DEV Code Queue apply without explicit supervisor or human authorization outside Code Queue", + dryRunCommandShape: "bun scripts/cli.ts deploy apply --env dev --service --dry-run", + }; +} + async function runArtifactConsumerApplyNow( manifest: DeployManifest, options: DeployOptions, @@ -3472,6 +3546,8 @@ export async function runDeployCommand(config: UniDeskConfig | null, args: strin throw new Error("deploy apply --env dev cannot mix artifact consumer services with target-side rollout services in one invocation; pass --service"); } if (devArtifactServices.length > 0) { + const blocked = devArtifactServices.filter((service) => devArtifactLiveApplyBlockedServiceIds.has(service.id)); + if (!options.dryRun && blocked.length > 0) return devArtifactLiveApplyBlockedResult(blocked); if (options.dryRun || options.runNow) return await runArtifactConsumerApplyNow(manifest, options, "dev", devArtifactServices); return devArtifactApplyJob(args, options); } diff --git a/scripts/src/help.ts b/scripts/src/help.ts index 72a7c519..8cc5d140 100644 --- a/scripts/src/help.ts +++ b/scripts/src/help.ts @@ -349,7 +349,7 @@ function artifactRegistryHelp(): unknown { "bun scripts/cli.ts artifact-registry deploy-service --env prod --service mdtodo --commit [--dry-run] [--run-now] [--provider-id D601]", "bun scripts/cli.ts artifact-registry deploy-service --env dev --service claudeqq --commit [--dry-run] [--run-now] [--provider-id D601]", "bun scripts/cli.ts artifact-registry deploy-service --env prod --service claudeqq --commit [--dry-run] [--run-now] [--provider-id D601]", - "bun scripts/cli.ts artifact-registry deploy-service --env dev --service code-queue --commit [--dry-run] [--run-now] [--provider-id D601]", + "bun scripts/cli.ts artifact-registry deploy-service --env dev --service code-queue --commit --dry-run [--provider-id D601]", ], description: "Manage the declaration, rendered files and readonly checks for the D601 host-managed CNCF Distribution artifact registry.", boundary: [ @@ -359,7 +359,7 @@ function artifactRegistryHelp(): unknown { "deploy-backend-core only pulls commit-pinned backend-core artifacts and does not build backend-core on the master server", "deploy-service currently supports backend-core, baidu-netdisk, prod/dev frontend, decision-center, mdtodo, claudeqq, project-manager, oa-event-flow, code-queue-mgr, todo-note, findjob, pipeline, met-nonlinear, k3sctl-adapter, and dev-only code-queue as standardized consumers", "findjob and pipeline have D601 direct dev/prod Compose artifact consumers; met-nonlinear is runtime-verification blocked; k3sctl-adapter is supervisor-only", - "code-queue has no prod artifact deploy target and prod requests return structured unsupported", + "code-queue has no prod artifact deploy target; dev requests are dry-run evidence only unless a human operator or supervisor authorizes DEV apply outside Code Queue", "status and health use provider-gateway Host SSH readonly checks", ], legacyEntrypoints: {