Add decision-center k3s artifact consumer paths

This commit is contained in:
Codex
2026-05-19 16:22:03 +00:00
parent 2a21f6cda3
commit b2f16f235b
13 changed files with 543 additions and 161 deletions
+5
View File
@@ -102,6 +102,11 @@
"repo": "https://github.com/pikasTech/unidesk",
"commitId": "c8561d392be857e8a0b79d6dc1b27dee5ece6b88"
},
{
"id": "decision-center",
"repo": "https://github.com/pikasTech/unidesk",
"commitId": "54c1f8e165f90fa8509fda1f0c01f8c3fa82cbee"
},
{
"id": "code-queue",
"repo": "https://github.com/pikasTech/unidesk",
+3 -2
View File
@@ -76,10 +76,11 @@ bun scripts/cli.ts artifact-registry deploy-service --service decision-center --
bun scripts/cli.ts artifact-registry deploy-service --service baidu-netdisk --commit <full-sha> --run-now
bun scripts/cli.ts artifact-registry deploy-service --service frontend --env prod --commit <full-sha> --run-now
bun scripts/cli.ts artifact-registry deploy-service --service frontend --env dev --commit <full-sha> --run-now
bun scripts/cli.ts artifact-registry deploy-service --env dev --service decision-center --commit <full-sha> --run-now
bun scripts/cli.ts artifact-registry deploy-service --service decision-center --commit <full-sha> --run-now
```
dry-run 输出会暴露 registry probe URL、required labels、目标 image、部署形态和回滚信息。`baidu-netdisk` 的 Compose 路径会通过 provider-gateway Host SSH 把 `unidesk/baidu-netdisk:<commit>` 流式拉到 master serverretag 为 `baidu-netdisk``baidu-netdisk:<commit>`,写入 `UNIDESK_BAIDU_NETDISK_DEPLOY_*`,只 recreate `baidu-netdisk` service,并验证容器 image label 与 `/health.deploy.commit``frontend --env prod` 使用同一 Compose artifact consumer,流式拉取 `unidesk/frontend:<commit>`retag 为 `unidesk-frontend``unidesk-frontend:<commit>`,写入 `UNIDESK_FRONTEND_DEPLOY_*`,只 recreate `frontend` service,并验证 image label 与 `/health.deploy.commit``frontend --env dev``decision-center` 的 k3s 路径会在 D601 上验证 commit image、导入 native k3s containerd、更新 Deployment image/env/annotations,并通过 Kubernetes API service proxy 验证 `/health` 中的 `deploy.commit`dev frontend 还会在 rollout 前把主 server `config.json.auth` 同步到 `unidesk-dev` Secret/ConfigMap。回滚信息通过同一 artifact consumer 的 `rollback` 字段暴露,提示操作者重新对一个旧 commit 运行相同命令,而不是切回 legacy maintenance-channel 构建。
dry-run 输出会暴露 registry probe URL、required labels、目标 image、部署形态和回滚信息。`baidu-netdisk` 的 Compose 路径会通过 provider-gateway Host SSH 把 `unidesk/baidu-netdisk:<commit>` 流式拉到 master serverretag 为 `baidu-netdisk``baidu-netdisk:<commit>`,写入 `UNIDESK_BAIDU_NETDISK_DEPLOY_*`,只 recreate `baidu-netdisk` service,并验证容器 image label 与 `/health.deploy.commit``frontend --env prod` 使用同一 Compose artifact consumer,流式拉取 `unidesk/frontend:<commit>`retag 为 `unidesk-frontend``unidesk-frontend:<commit>`,写入 `UNIDESK_FRONTEND_DEPLOY_*`,只 recreate `frontend` service,并验证 image label 与 `/health.deploy.commit``frontend --env dev``decision-center` 的 k3s 路径会在 D601 上验证 commit image、导入 native k3s containerd、更新 Deployment image/env/annotations,并通过 Kubernetes API service proxy 验证 `/health` 中的 `deploy.commit``deploy.requestedCommit`dev frontend 还会在 rollout 前把主 server `config.json.auth` 同步到 `unidesk-dev` Secret/ConfigMap。`decision-center --env dev` 落到 `unidesk-dev/decision-center-dev`prod 落到 `unidesk/decision-center`回滚信息通过同一 artifact consumer 的 `rollback` 字段暴露,提示操作者重新对一个旧 commit 运行相同命令,而不是切回 legacy maintenance-channel 构建。
`status``health` 通过:
@@ -123,7 +124,7 @@ docker compose -p unidesk-artifact-registry -f /home/ubuntu/.unidesk/artifact-re
6. Compose runtime retag 为 Compose 使用的镜像名,并执行 `docker compose up -d --no-build --no-deps --force-recreate <service>`k3s runtime 设置 Deployment image/env/annotations 并等待 rollout。
7. 部署后通过 image label、runtime env、health payload 验证 live commit。
Baidu Netdisk is the first main-server direct user-service sample in this flow. Its dev validation command and prod CD command both consume `127.0.0.1:5000/unidesk/baidu-netdisk:<commit>` and must not build source on the master server. Frontend is the standard UniDesk UI artifact sample: CI publishes `127.0.0.1:5000/unidesk/frontend:<commit>`, production CD consumes that artifact into the master-server Compose `frontend` service, and dev CD consumes the same artifact into D601 native k3s `frontend-dev`. Neither path may use `server rebuild frontend`, dirty source, mutable `latest`, or target-side frontend source builds as release truth. Decision Center follows the same artifact-consumer pattern, except the runtime target is native k3s on D601 instead of the master-server Compose stack. The consumer must check the registry manifest, pull the commit-pinned image, import it into `/run/k3s/containerd/containerd.sock`, set the Deployment image to the commit tag, stamp `UNIDESK_DEPLOY_*` env/annotations, and reject an old healthy revision if the live commit does not match.
Baidu Netdisk is the first main-server direct user-service sample in this flow. Its dev validation command and prod CD command both consume `127.0.0.1:5000/unidesk/baidu-netdisk:<commit>` and must not build source on the master server. Frontend is the standard UniDesk UI artifact sample: CI publishes `127.0.0.1:5000/unidesk/frontend:<commit>`, production CD consumes that artifact into the master-server Compose `frontend` service, and dev CD consumes the same artifact into D601 native k3s `frontend-dev`. Neither path may use `server rebuild frontend`, dirty source, mutable `latest`, or target-side frontend source builds as release truth. Decision Center follows the same artifact-consumer pattern in both dev and prod, except the runtime target is native k3s on D601 instead of the master-server Compose stack. The consumer must check the registry manifest, pull the commit-pinned image, import it into `/run/k3s/containerd/containerd.sock`, set the Deployment image to the commit tag, stamp `UNIDESK_DEPLOY_*` env/annotations, and reject an old healthy revision if the live commit or requested commit does not match.
这个 CD 路径必须满足:
+3 -3
View File
@@ -25,7 +25,7 @@ CLI 可以从 `master` 快速演进,但必须兼容 `deploy.json` 固定的 CI
- `microservice list/status/health/diagnostics/tunnel-self-test/proxy` 通过 backend-core 内网 API 管理挂载在计算节点 Docker 或 k3s 控制面中的用户服务(底层命令名仍为 microservice);`health``diagnostics``tunnel-self-test``proxy` 会走真实 backend-core -> provider-gateway 或 k3sctl-adapter -> 节点服务链路,`proxy` 支持受控 JSON 请求体并对超大响应 body 默认输出有界预览,规则见 `docs/reference/microservices.md`
- `decision upload/list/show/health` 通过 backend-core 用户服务代理访问 D601 k3s Decision Center,用于上传会议记录/决议 Markdown、列出权威记录、查看详情和健康检查;`decision requirement list/upsert` 在同一 records 模型上管理 `goal|decision|blocker|debt|experiment` 需求记录。它们不得直连 D601 Service、NodePort 或 provider-gateway 业务 HTTP。
- `decision diary import <markdown-file>` 将带 `# YYYY年M月D日``# YYYY-MM-DD``# YYYY/M/D` 标题的工作日志拆成每天一篇 Markdown 日记,按 `YYYY-MM/YYYY-MM-DD.md` 虚拟路径写入 Decision Center PostgreSQL`decision diary list/months/show` 分别用于按月/日期查询、列出月份和查看单日正文;`decision diary edit|upsert <YYYY-MM-DD|id> --body-file <path> [--title text] [--source-file path] [--tag tag]` 通过 `PUT /api/diary/entries/:idOrDate` 创建当天或历史条目并编辑既有条目。
- `deploy check/plan/apply` 默认从根目录 `deploy.json` 读取服务 repo 与 commit 期望状态,join `config.json` 和现有 manifest 后使用 target-side build 单一路径校验或更新已支持目标;`deploy plan --env dev|prod` 只从 `origin/master:deploy.json#environments.<env>` 读取 manifest 并输出 dry-run 环境计划,不使用本地 dirty worktree;当前 `deploy apply --env dev` 支持 D601 `backend-core` target-side rollout,以及 `frontend`/`baidu-netdisk` artifact consumersdev desired-state smoke 使用 `ci run-dev-e2e`;规则见 `docs/reference/deploy.md``docs/reference/dev-environment.md``docs/reference/dev-ci-runner.md`
- `deploy check/plan/apply` 默认从根目录 `deploy.json` 读取服务 repo 与 commit 期望状态,join `config.json` 和现有 manifest 后使用 target-side build 单一路径校验或更新已支持目标;`deploy plan --env dev|prod` 只从 `origin/master:deploy.json#environments.<env>` 读取 manifest 并输出 dry-run 环境计划,不使用本地 dirty worktree;当前 `deploy apply --env dev` 支持 D601 `backend-core` target-side rollout,以及 `frontend`/`baidu-netdisk`/`decision-center` artifact consumersdev desired-state smoke 使用 `ci run-dev-e2e`;规则见 `docs/reference/deploy.md``docs/reference/dev-environment.md``docs/reference/dev-ci-runner.md`
- `dev-env validate [--manifest path] [--kubectl-dry-run]` 离线校验 D601 `unidesk-dev` namespace、dev PostgreSQL 底座和 dev workload manifest。默认检查 `src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-foundation.k8s.yaml`;也可显式校验 `src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-core.k8s.yaml``src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-code-queue.k8s.yaml`。所有 namespaced 对象必须只落到 `unidesk-dev`foundation manifest 必须包含 `postgres-dev` StatefulSet/Service、dev secret/config、迁移 Job 和 DB URL guardcore manifest 必须包含 `backend-core-dev`/`frontend-dev` Deployment/ServiceCode Queue dev manifest 必须包含 `code-queue-scheduler-dev``code-queue-read-dev``code-queue-write-dev` 和 dev provider egress proxy。加 `--kubectl-dry-run` 时额外执行 `kubectl apply --dry-run=client --validate=false -f <manifest>`,仍不 apply 资源。
- `dev-env prewarm-images [--image image] [--provider-id D601] [--no-pull] [--proxy-url URL] [--pull-timeout-ms N] [--dry-run]` 创建异步 job,通过 UniDesk SSH 维护桥在 D601 上把开发底座依赖镜像从 Docker 缓存导入原生 k3s containerd。默认镜像是 `postgres:16-alpine``rancher/mirrored-library-busybox:1.36.1`,用于避免 `postgres-dev` 与 local-path helper pod 卡在外部 registry 拉取。该命令固定验证 `/etc/rancher/k3s/k3s.yaml` 指向的 native k3s 上下文,并输出 `dev_env_containerd_image_ready=...` 作为成功判据;它不 apply manifest、不修改生产 `unidesk` namespace。
- `artifact-registry plan|render|status|health|install|deploy-backend-core|deploy-service` 管理 D601 host-managed CNCF Distribution registry 的声明、安装、只读检查和 pull-only artifact CD。该 registry 固定为 D601 loopback `127.0.0.1:5000`,由 systemd + Docker Compose 管理,位于 native k3s 故障域外;`deploy-backend-core``deploy-service` 只拉取 CI 已发布的 commit-pinned 镜像、retag/recreate 或导入 native k3s,并做 live commit 验证,不构建 runtime source。长期规则见 `docs/reference/artifact-registry.md`
@@ -48,9 +48,9 @@ CLI 可以从 `master` 快速演进,但必须兼容 `deploy.json` 固定的 CI
长时操作采用 Fire-and-Forget 模式:CLI 创建 `.state/jobs/{jobId}.json`,后台进程执行真实命令,并将 stdout、stderr 分别写入 `.state/jobs/{jobId}.stdout.log``.state/jobs/{jobId}.stderr.log`。调用者通过 `bun scripts/cli.ts job status <jobId>` 查询进度和尾部输出。
`server rebuild``server start``server stop` 一样必须通过返回的 job id 确认结果;不要把连续 `server rebuild` 命令理解成“前一个重建已完成”,因为两个命令只是在快速创建异步 job。重建 frontend 只保留为维护/非标准路径;标准 frontend 发布必须先运行 `ci publish-user-service --service frontend --commit <full-sha>`,再运行 `deploy apply --env dev --service frontend``deploy apply --env prod --service frontend`,并验证 `/health.deploy.commit`。重建 dev 入口薄代理使用 `bun scripts/cli.ts server rebuild dev-frontend-proxy`,随后验证 `server status``urls.devFrontend``http://127.0.0.1:18083/health`;重建 Todo Note 后端使用 `bun scripts/cli.ts server rebuild todo-note`,随后用 `microservice health todo-note``microservice proxy todo-note /api/instances` 验证;重建 Code Queue Manager 使用 `bun scripts/cli.ts server rebuild code-queue-mgr`,随后用 `microservice health code-queue-mgr``microservice health code-queue``codex submit --dry-run` 验证主 server 控制面路径;重建 Project Manager 后端使用 `bun scripts/cli.ts server rebuild project-manager`,随后用 `microservice health project-manager``microservice proxy project-manager /api/projects` 验证;重建 Baidu Netdisk 后端使用 `bun scripts/cli.ts server rebuild baidu-netdisk`,随后用 `microservice health baidu-netdisk``microservice proxy baidu-netdisk /api/transfers` 验证,但该命令只保留为维护/非标准路径;重建 OA Event Flow 后端使用 `bun scripts/cli.ts server rebuild oa-event-flow`,随后用 `microservice health oa-event-flow``microservice proxy oa-event-flow /api/diagnostics` 验证。D601 Code Queue 执行面和 Decision Center 后端由 D601 k3s/k8s 控制面代管;persistent dev backend-core 走 target-side dev rolloutpersistent dev frontend 走 artifact consumer,当前 Code Queue/Decision Center 仍不得通过维护通道直连 D601 做部署。不得把 `docker rm` 手工兜底当成正式交付步骤。
`server rebuild``server start``server stop` 一样必须通过返回的 job id 确认结果;不要把连续 `server rebuild` 命令理解成“前一个重建已完成”,因为两个命令只是在快速创建异步 job。重建 frontend 只保留为维护/非标准路径;标准 frontend 发布必须先运行 `ci publish-user-service --service frontend --commit <full-sha>`,再运行 `deploy apply --env dev --service frontend``deploy apply --env prod --service frontend`,并验证 `/health.deploy.commit`。重建 dev 入口薄代理使用 `bun scripts/cli.ts server rebuild dev-frontend-proxy`,随后验证 `server status``urls.devFrontend``http://127.0.0.1:18083/health`;重建 Todo Note 后端使用 `bun scripts/cli.ts server rebuild todo-note`,随后用 `microservice health todo-note``microservice proxy todo-note /api/instances` 验证;重建 Code Queue Manager 使用 `bun scripts/cli.ts server rebuild code-queue-mgr`,随后用 `microservice health code-queue-mgr``microservice health code-queue``codex submit --dry-run` 验证主 server 控制面路径;重建 Project Manager 后端使用 `bun scripts/cli.ts server rebuild project-manager`,随后用 `microservice health project-manager``microservice proxy project-manager /api/projects` 验证;重建 Baidu Netdisk 后端使用 `bun scripts/cli.ts server rebuild baidu-netdisk`,随后用 `microservice health baidu-netdisk``microservice proxy baidu-netdisk /api/transfers` 验证,但该命令只保留为维护/非标准路径;重建 OA Event Flow 后端使用 `bun scripts/cli.ts server rebuild oa-event-flow`,随后用 `microservice health oa-event-flow``microservice proxy oa-event-flow /api/diagnostics` 验证。D601 Code Queue 执行面和 Decision Center 后端由 D601 k3s/k8s 控制面代管;persistent dev backend-core 走 target-side dev rolloutpersistent dev frontend 和 Decision Center 走 artifact consumer,当前 Code Queue 仍不得通过维护通道直连 D601 做部署。不得把 `docker rm` 手工兜底当成正式交付步骤。
新部署入口优先使用 `deploy apply``deploy apply --env dev --service backend-core` 是 D601 target-side dev 路径;`deploy apply --env dev|prod --service frontend``deploy apply --env dev|prod --service baidu-netdisk` 是 artifact-consumer 样板;旧的 `codex deploy` 已禁用;后续 Code Queue、Decision Center 等 D601 服务部署应另行收敛到同一类受控 CD 路径,部署后用 live commit 校验证明不是旧服务。
新部署入口优先使用 `deploy apply``deploy apply --env dev --service backend-core` 是 D601 target-side dev 路径;`deploy apply --env dev|prod --service frontend``deploy apply --env dev|prod --service baidu-netdisk``deploy apply --env dev|prod --service decision-center` 是 artifact-consumer 样板;旧的 `codex deploy` 已禁用;后续 Code Queue 等 D601 服务部署应另行收敛到同一类受控 CD 路径,部署后用 live commit 校验证明不是旧服务。
## Output Contract
+5 -5
View File
@@ -40,7 +40,7 @@ The root `deploy.json` is the single desired-state source for both prod and dev.
The optional non-service execution declaration under `environments.dev` is intentionally not specified here. The only currently allowed declaration is `ci`, and its authoritative `repo`, `scriptPath`, `timeoutMs`, short launcher, host fetch boundary and no-CD rules are defined only in `docs/reference/dev-ci-runner.md`.
Environment mode never reads the local dirty working tree manifest. `deploy check --env ...`, `deploy plan --env ...` and `deploy apply --env ...` fetch `origin/master`, read `origin/master:deploy.json`, select `environments.<env>`, and report the manifest commit/blob, service commit IDs, target namespace, database fingerprint and Provider identity. `deploy apply --env dev` is currently enabled for persistent D601 dev `backend-core` target-side rollout and for `frontend`/`baidu-netdisk` artifact-consumer validation; all other D601 services remain rejected before runtime mutation. `deploy apply --env prod` exposes only reviewed D601 registry artifact consumers (`backend-core`, `frontend`, `baidu-netdisk`, `decision-center`). The default user-service delivery policy, including CI build, registry publication, dev validation, production CD and manual acceptance, is documented in `docs/reference/user-service-delivery.md`.
Environment mode never reads the local dirty working tree manifest. `deploy check --env ...`, `deploy plan --env ...` and `deploy apply --env ...` fetch `origin/master`, read `origin/master:deploy.json`, select `environments.<env>`, and report the manifest commit/blob, service commit IDs, target namespace, database fingerprint and Provider identity. `deploy apply --env dev` is currently enabled for persistent D601 dev `backend-core` target-side rollout and for `frontend`/`baidu-netdisk`/`decision-center` artifact consumers; all other D601 services remain rejected before runtime mutation. `deploy apply --env prod` exposes only reviewed D601 registry artifact consumers (`backend-core`, `frontend`, `baidu-netdisk`, `decision-center`). Production backend-core artifact CD is a separate executor because its build target is D601 CI while its runtime target is the master server. The default user-service delivery policy, including CI build, registry publication, dev validation, production CD and manual acceptance, is documented in `docs/reference/user-service-delivery.md`.
The current implementation has not yet enabled separate stable and integration dev lanes. Future lane names such as `dev-v1` and `dev-master`, or an equivalent nested schema, must be added as explicit `deploy.json` and CLI semantics before use. A deploy command must print the manifest ref it used and must not infer `release/v1` from a local branch, a dirty file, or an undocumented environment alias.
@@ -83,7 +83,7 @@ Phase 3 introduces the dev backend/frontend manifest at `src/components/microser
`backend-core-dev` must use `unidesk-dev-runtime-config` and `unidesk-dev-runtime-secrets`, connect to `postgres-dev.../unidesk_dev`, expose HTTP on 8080 and provider ingress on 8081, and write logs under `/var/log/unidesk-dev`. `frontend-dev` must set `CORE_INTERNAL_URL=http://backend-core-dev.unidesk-dev.svc.cluster.local:8080` and must not proxy to production backend-core.
The manifest keeps placeholder image tags and deploy commit values in source control. The controlled `deploy apply --env dev --service backend-core` path fetches `origin/master:deploy.json`, materializes the requested source commit on D601, narrows the dev core control manifest to the selected Service/Deployment pair, replaces placeholders with the requested commit and dev image tag, builds on D601, imports the image into native k3s containerd, applies only the `unidesk-dev` objects and stamps the Deployment. `deploy apply --env dev --service frontend` uses the same selected dev manifest objects but consumes the existing D601 registry artifact `127.0.0.1:5000/unidesk/frontend:<commit>` instead of building frontend source on the target. Client dry-run and static validation remain useful checks before controlled apply:
The manifest keeps placeholder image tags and deploy commit values in source control. The controlled `deploy apply --env dev --service backend-core` path fetches `origin/master:deploy.json`, materializes the requested source commit on D601, narrows the dev core control manifest to the selected Service/Deployment pair, replaces placeholders with the requested commit and dev image tag, builds on D601, imports the image into native k3s containerd, applies only the `unidesk-dev` objects and stamps the Deployment. `deploy apply --env dev --service frontend` uses the same selected dev manifest objects but consumes the existing D601 registry artifact `127.0.0.1:5000/unidesk/frontend:<commit>` instead of building frontend source on the target. Decision Center uses the same dev namespace but follows the D601 registry artifact consumer path instead of a source build: it verifies the commit-pinned image in D601 registry, imports it into native k3s containerd, applies the dev Decision Center manifest, stamps the Deployment and verifies live commit/requestedCommit. Client dry-run and static validation remain useful checks before controlled apply:
- `bun scripts/cli.ts dev-env validate --manifest src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-core.k8s.yaml`
- `KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl apply --dry-run=client --validate=false -f src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-core.k8s.yaml`
@@ -106,7 +106,7 @@ Maintenance-channel direct D601 apply must not deploy dev Code Queue; the CLI re
`bun scripts/cli.ts deploy plan --env dev [--service <id>]` reads `origin/master:deploy.json#environments.dev` and prints a dry-run environment plan without checking or mutating live runtime resources. `deploy check --env dev` uses the same dry-run environment plan. `--env prod` is available for parity as a dry-run planning path; it reads `origin/master:deploy.json#environments.prod` and must not use a dirty local `deploy.json`.
`bun scripts/cli.ts deploy apply [--file deploy.json | --env dev|prod] [--service <id>] [--commit <full-sha>] [--dry-run] [--force]` starts an asynchronous job only for supported targets. Use `bun scripts/cli.ts job status <jobId> --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` artifact consumers. `--env prod` apply exposes the D601 registry artifact consumer for `backend-core`, `frontend`, `baidu-netdisk`, and `decision-center`. Unsupported prod services 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 <id>] [--commit <full-sha>] [--dry-run] [--force]` starts an asynchronous job only for supported targets. Use `bun scripts/cli.ts job status <jobId> --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` artifact consumers. `--env prod` apply exposes the D601 registry artifact consumer for `backend-core`, `frontend`, `baidu-netdisk`, and `decision-center`. Unsupported prod services 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.
@@ -163,7 +163,7 @@ The reconciler selects the executor from `config.json`:
- `deployment.mode=internal-sidecar` on `main-server`: use the same main-server target-side source export, Docker build, image label stamping, fixed Compose project replacement and live commit verification as direct Compose services. This class is for private sidecars such as `code-queue-mgr`; it is still versioned by `deploy.json.commitId`, not by the operator's current worktree.
- `deployment.mode=unidesk-direct` on a provider: this executor is disabled for D601 service deployment. The historical behavior dispatched `host.ssh` to the provider, built on the provider, then used the service's provider-local compose file and project; that shape must not remain a second deployment control plane.
- Control bridges that UniDesk needs in order to inspect or repair an orchestrator must stay in this direct class. In particular, `k3sctl-adapter` is a UniDesk-managed bridge to native k3s and must remain outside k3s; Docker packaging on Docker Desktop/WSL must create an explicit host-local bridge, currently an adapter-container SSH local tunnel, to reach `/etc/rancher/k3s/k3s.yaml` and WSL `127.0.0.1:6443`.
- `deployment.mode=k3sctl-managed`: the target behavior is to build on the active control target unless the service has a reviewed artifact-consumer exception, verify native k3s on the host OS/WSL distro, import the image into native k3s/containerd, apply the existing Kubernetes manifest, stamp the Deployment and wait for rollout. On D601, persistent dev apply is currently allowed for `backend-core` target-side build and `frontend` artifact consumption in `unidesk-dev`; normal production services still cannot use a maintenance-channel direct rollout. The executor must use the native kubeconfig and containerd socket, for example `/etc/rancher/k3s/k3s.yaml` and `/run/k3s/containerd/containerd.sock`; running k3s itself in Docker is forbidden for both control-plane and worker nodes. A `rancher/k3s` image or legacy container may only be used as a temporary artifact source during migration, and any active containerized k3s control plane must be stopped before verification succeeds. The executor must preload a valid `rancher/mirrored-pause:3.6` sandbox image into native k3s containerd through the provider-gateway one-shot egress path, verify its entrypoint is `/pause`, and reject fake or sleep-based replacement images. Code Queue's k3s migration executor must also stop/remove the legacy direct Docker `code-queue-backend` after k3s rollout, so there is never a second scheduler running beside the native k3s scheduler.
- `deployment.mode=k3sctl-managed`: the target behavior is to build on the active control target unless the service has a reviewed artifact-consumer exception, verify native k3s on the host OS/WSL distro, import the image into native k3s/containerd, apply the existing Kubernetes manifest, stamp the Deployment and wait for rollout. On D601, persistent dev apply is currently allowed for `backend-core` target-side build plus `frontend` and `decision-center` artifact consumption in `unidesk-dev`; normal production services still cannot use a maintenance-channel direct rollout. The executor must use the native kubeconfig and containerd socket, for example `/etc/rancher/k3s/k3s.yaml` and `/run/k3s/containerd/containerd.sock`; running k3s itself in Docker is forbidden for both control-plane and worker nodes. A `rancher/k3s` image or legacy container may only be used as a temporary artifact source during migration, and any active containerized k3s control plane must be stopped before verification succeeds. The executor must preload a valid `rancher/mirrored-pause:3.6` sandbox image into native k3s containerd through the provider-gateway one-shot egress path, verify its entrypoint is `/pause`, and reject fake or sleep-based replacement images. Code Queue's k3s migration executor must also stop/remove the legacy direct Docker `code-queue-backend` after k3s rollout, so there is never a second scheduler running beside the native k3s scheduler.
D601 Docker local images are not the source of truth for k3s runtime availability. For Code Queue, the deploy gate must verify `unidesk-code-queue:d601` exists in native k3s containerd after import with `ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images ls`, and it must fail before rollout if the tag is missing. The same gate must verify every production Code Queue Deployment that uses the image (`code-queue`, `code-queue-read`, `code-queue-write`, `d601-provider-egress-proxy`, `d601-tcp-egress-gateway`) still references exactly `unidesk-code-queue:d601`; otherwise kubelet may attempt an external registry pull and leave base gateways in `ImagePullBackOff`.
@@ -173,7 +173,7 @@ Existing service-specific commands such as Code Queue deploy are disabled as dir
Baidu Netdisk is the main-server `unidesk-direct` sample for artifact CD. Controlled dev validation and prod CD use the D601 registry artifact consumer: it verifies `unidesk/baidu-netdisk:<commit>` exists in the registry, streams the image to the main server through provider-gateway Host SSH, retags `baidu-netdisk` and `baidu-netdisk:<commit>`, stamps `UNIDESK_BAIDU_NETDISK_DEPLOY_*`, recreates only Compose service `baidu-netdisk`, and verifies container health, image labels, service id, and `/health.deploy.commit`. It must not use `server rebuild baidu-netdisk`, mutable tags, dirty worktrees, hand-built images, or public `4244` exposure as deployment truth.
Decision Center is a standard `k3sctl-managed` service in this model, but D601 maintenance-channel direct apply must not deploy it. Controlled CD for Decision Center uses the D601 registry artifact consumer: it verifies `unidesk/decision-center:<commit>` exists in the registry, imports `unidesk-decision-center:<commit>` into native k3s containerd, applies `src/components/microservices/k3sctl-adapter/k3s/decision-center.k8s.yaml`, stamps the Deployment, and verifies health through `/api/microservices/decision-center/health`. It must not add a main-server Compose service, NodePort, hostPort, or provider-gateway direct HTTP backend for Decision Center.
Decision Center is a standard `k3sctl-managed` service in this model, but D601 maintenance-channel direct apply must not deploy it. Controlled CD for Decision Center uses the D601 registry artifact consumer in both dev and prod: it verifies `unidesk/decision-center:<commit>` exists in the registry, imports `unidesk-decision-center:<commit>` into native k3s containerd, applies the appropriate Decision Center manifest, stamps the Deployment, and verifies health through `/api/microservices/decision-center/health` while proving the live and requested commit match. It must not add a main-server Compose service, NodePort, hostPort, or provider-gateway direct HTTP backend for Decision Center.
## CI Separation
+2 -1
View File
@@ -234,7 +234,7 @@ D601 上必须显式使用原生 k3s kubeconfig`KUBECONFIG=/etc/rancher/k3s/k
当前 Decision Center 作为 `id=decision-center``k3sctl-managed` 用户服务登记在 `config.json`,用于沉淀 Codex/人工会议后的会议记录、决议、目标、问题分级、停放事项、证据和按天工作日记,同时也作为外部需求向内部目标分解的工作台。它只负责权威记录、Markdown 日记、需求管理和展示,不承载通用聊天、LLM 会话窗口或自动参谋对话。
- Orchestrator`deployment.mode=k3sctl-managed``deployment.adapterServiceId=k3sctl-adapter``deployment.k3sServiceId=decision-center``backend.proxyMode=k3sctl-adapter-http``backend.nodeBaseUrl=k3s://decision-center`;正式链路只能是 `frontend/CLI -> backend-core -> k3sctl-adapter -> Kubernetes API service proxy -> Kubernetes Service decision-center:4277`
- 部署引用:后端源码位于 UniDesk 仓库 `src/components/microservices/decision-center`Dockerfile 为 `src/components/microservices/decision-center/Dockerfile`k3s manifest 为 `src/components/microservices/k3sctl-adapter/k3s/decision-center.k3s.json`Kubernetes 运行清单为 `src/components/microservices/k3sctl-adapter/k3s/decision-center.k8s.yaml`,镜像名固定为 `unidesk-decision-center:d601`。主 server `docker-compose.yml` 不得加入该服务,也不得公开 `4277`
- 部署引用:后端源码位于 UniDesk 仓库 `src/components/microservices/decision-center`Dockerfile 为 `src/components/microservices/decision-center/Dockerfile`k3s manifest 为 `src/components/microservices/k3sctl-adapter/k3s/decision-center.k3s.json`Kubernetes 运行清单为 `src/components/microservices/k3sctl-adapter/k3s/decision-center.k8s.yaml`,镜像名固定为 `unidesk-decision-center:d601`dev 环境使用 `src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-decision-center.k8s.yaml``unidesk-dev-decision-center.k3s.json`,服务名为 `decision-center-dev`主 server `docker-compose.yml` 不得加入该服务,也不得公开 `4277`
- 状态权威:Decision Center 必须写入主 PostgreSQL,权威记录表为 `decision_center_records`,日记表为 `decision_center_diary_entries`;不得使用浏览器 `localStorage`、IndexedDB、容器 writable layer 或本地 JSON 文件作为会议、决议、目标、问题或日记状态权威。D601 Pod 通过集群内 `d601-tcp-egress-gateway.unidesk.svc.cluster.local:15432` 访问主 PostgreSQL。
- 记录数据模型:记录类型为 `meeting|decision|goal|blocker|debt|experiment`,等级为 `G0|G1|G2|G3|P0|P1|P2|P3|none`,状态为 `active|blocked|parked|done`,字段包含 `title`、Markdown `summary/body``linkedGoalId``tags``evidenceLinks``sourceSession``taskId``commitId``createdAt``updatedAt`
- 需求管理:Decision Center 里的 `goal` 记录应承接外部需求或长期目标,`decision` 记录应承接需求分解后的取舍,`blocker` 记录应承接当前阻塞,`experiment` 记录应承接验证性工作,`debt` 记录应承接必须偿还的技术/流程债。任何新需求都应先写成可验证的外部收益,再分解为这些内部记录,而不是先发散成内部审美或架构偏好。需求管理 API 复用 `decision_center_records``/api/requirements` 只是在同一模型上排除 `meeting` 并提供需求语义的 list/upsert 别名,不引入第二套需求表。
@@ -242,6 +242,7 @@ D601 上必须显式使用原生 k3s kubeconfig`KUBECONFIG=/etc/rancher/k3s/k
- 日记编辑:工作日记必须支持按真实日期创建当天条目,并支持按日期回看和编辑历史条目;`PUT /api/diary/entries/:idOrDate` 允许安全更新 `body`/`markdown``title``tags``sourceFile`,按 `YYYY-MM-DD` key 且不存在时创建当天或历史条目,按非日期 id 时只编辑既有条目。数据库仍是唯一权威,前端只是编辑入口和展示入口。
- API:只允许 `/health``/live``/logs``/api/` 前缀;允许 `GET``HEAD``POST``PUT``DELETE`。业务 API 包含 `GET /api/records``POST /api/records``GET|PUT|DELETE /api/records/:id``GET|POST|PUT /api/requirements``POST /api/meetings/import``POST /api/diary/import``GET /api/diary/entries``GET|PUT /api/diary/entries/:idOrDate``GET /api/diary/months`,错误必须返回结构化 JSON,便于 CLI 与 frontend 诊断。
- CLI`bun scripts/cli.ts decision upload <markdown-file>``decision list``decision show <id>``decision requirement list/upsert``decision diary import/list/months/show/edit/upsert``decision health` 只能通过 backend-core 用户服务代理访问 Decision Center,不得直连 D601 Service、NodePort 或 provider-gateway `microservice.http`。日记编辑验收应使用 `decision diary upsert <YYYY-MM-DD> --body-file <file>` 创建或更新,再用 `decision diary show <YYYY-MM-DD>` 读取确认。
- Dev/prod CDDecision Center 的 dev/prod rollout 都必须走 D601 registry artifact consumer,验证同一个 commit-pinned artifact contract,证明 live `deploy.commit``deploy.requestedCommit` 一致,再通过 Kubernetes API service proxy 验证健康;不得回退到维护通道直连或 NodePort/hostPort。
- UniDesk 前端:`用户服务 / Decision Center` React 页面展示权威记录筛选、当前 G0/G1 目标、P0/P1 blocker、停放事项、最近会议/决议和工作日记;它还应成为需求管理入口,让外部目标、内部拆解和每日工作记录在同一页面中可追溯。日记视图按月份筛选并展示每天 Markdown 正文,未来应支持当天自动创建与历史编辑。默认不得展示裸 JSON,完整原始数据只能通过 `查看原始JSON` 打开。
### MDTODO k3s-Managed
+1
View File
@@ -70,6 +70,7 @@ Decision Center is the canonical example of a user service that doubles as a pro
- The minimal standard artifact command is `bun scripts/cli.ts ci publish-user-service --service decision-center --commit <full-sha> --wait-ms 1200000`.
- The expected artifact is `127.0.0.1:5000/unidesk/decision-center:<commit>` plus its registry digest from the CI output.
- Dev and prod CD both consume that same commit-pinned artifact contract through the D601 registry artifact consumer; dev lands in `unidesk-dev`, prod lands in the production k3s namespace, and both must prove `deploy.commit` and `deploy.requestedCommit`.
- Requirements and follow-up items should be represented with structured records such as `goal`, `decision`, `blocker`, `debt`, and `experiment`, with linked evidence and goal references.
- The service should act as a demand-management surface for external goals that need to be decomposed into internal tasks, blockers, and decisions.
- The work-diary surface should support creating today's diary entry automatically from the real current date.
+218 -121
View File
@@ -78,6 +78,10 @@ interface ArtifactConsumerSpec {
kind: "compose" | "d601-k3s";
registryRepository: string;
dockerfile: string;
targets: Partial<Record<ArtifactDeployEnvironment, ArtifactConsumerTarget>>;
}
interface ArtifactConsumerTarget {
targetImage: string;
targetCommitImage: (commit: string) => string;
deployRef: string;
@@ -90,7 +94,7 @@ interface ArtifactConsumerSpec {
};
k3s?: {
namespace: string;
manifestPath: string;
manifestRepoPath: string;
deploymentName: string;
serviceName: string;
servicePort: number;
@@ -108,15 +112,19 @@ const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
kind: "compose",
registryRepository: "unidesk/backend-core",
dockerfile: "src/components/backend-core/Dockerfile",
targetImage: "unidesk-backend-core",
targetCommitImage: (commit: string) => `unidesk-backend-core:${commit}`,
deployRef: "deploy.json#environments.prod.services.backend-core",
compose: {
serviceName: "backend-core",
containerName: "unidesk-backend-core",
deployEnvPrefix: "UNIDESK_DEPLOY",
healthProbeCommand: "if command -v backend-core >/dev/null 2>&1; then backend-core --fetch-json http://127.0.0.1:8080/health --require-ok; elif command -v bun >/dev/null 2>&1; then bun -e \"fetch('http://127.0.0.1:8080/health').then(async r=>{const text=await r.text(); console.log(text); process.exit(r.ok?0:1)}).catch(e=>{console.error(e); process.exit(1)})\"; else exit 1; fi",
requireHealthCommit: false,
targets: {
prod: {
targetImage: "unidesk-backend-core",
targetCommitImage: (commit: string) => `unidesk-backend-core:${commit}`,
deployRef: "deploy.json#environments.prod.services.backend-core",
compose: {
serviceName: "backend-core",
containerName: "unidesk-backend-core",
deployEnvPrefix: "UNIDESK_DEPLOY",
healthProbeCommand: "if command -v backend-core >/dev/null 2>&1; then backend-core --fetch-json http://127.0.0.1:8080/health --require-ok; elif command -v bun >/dev/null 2>&1; then bun -e \"fetch('http://127.0.0.1:8080/health').then(async r=>{const text=await r.text(); console.log(text); process.exit(r.ok?0:1)}).catch(e=>{console.error(e); process.exit(1)})\"; else exit 1; fi",
requireHealthCommit: false,
},
},
},
},
"baidu-netdisk": {
@@ -125,15 +133,19 @@ const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
kind: "compose",
registryRepository: "unidesk/baidu-netdisk",
dockerfile: "src/components/microservices/baidu-netdisk/Dockerfile",
targetImage: "baidu-netdisk",
targetCommitImage: (commit: string) => `baidu-netdisk:${commit}`,
deployRef: "deploy.json#environments.prod.services.baidu-netdisk",
compose: {
serviceName: "baidu-netdisk",
containerName: "baidu-netdisk-backend",
deployEnvPrefix: "UNIDESK_BAIDU_NETDISK_DEPLOY",
healthProbeCommand: "bun -e \"fetch('http://127.0.0.1:4244/health').then(async r=>{const text=await r.text(); console.log(text); process.exit(r.ok?0:1)}).catch(e=>{console.error(e); process.exit(1)})\"",
requireHealthCommit: true,
targets: {
prod: {
targetImage: "baidu-netdisk",
targetCommitImage: (commit: string) => `baidu-netdisk:${commit}`,
deployRef: "deploy.json#environments.prod.services.baidu-netdisk",
compose: {
serviceName: "baidu-netdisk",
containerName: "baidu-netdisk-backend",
deployEnvPrefix: "UNIDESK_BAIDU_NETDISK_DEPLOY",
healthProbeCommand: "bun -e \"fetch('http://127.0.0.1:4244/health').then(async r=>{const text=await r.text(); console.log(text); process.exit(r.ok?0:1)}).catch(e=>{console.error(e); process.exit(1)})\"",
requireHealthCommit: true,
},
},
},
},
"decision-center": {
@@ -142,17 +154,36 @@ const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
kind: "d601-k3s",
registryRepository: "unidesk/decision-center",
dockerfile: "src/components/microservices/decision-center/Dockerfile",
targetImage: "unidesk-decision-center:d601",
targetCommitImage: (commit: string) => `unidesk-decision-center:${commit}`,
deployRef: "deploy.json#environments.prod.services.decision-center",
k3s: {
namespace: "unidesk",
manifestPath: "/home/ubuntu/cq-deploy/src/components/microservices/k3sctl-adapter/k3s/decision-center.k8s.yaml",
deploymentName: "decision-center",
serviceName: "decision-center",
servicePort: 4277,
containerName: "decision-center",
healthPath: "/health",
targets: {
dev: {
targetImage: "unidesk-decision-center:dev",
targetCommitImage: (commit: string) => `unidesk-decision-center:${commit}`,
deployRef: "deploy.json#environments.dev.services.decision-center",
k3s: {
namespace: "unidesk-dev",
manifestRepoPath: "src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-decision-center.k8s.yaml",
deploymentName: "decision-center-dev",
serviceName: "decision-center-dev",
servicePort: 4277,
containerName: "decision-center",
healthPath: "/health",
podLabelSelector: "app.kubernetes.io/name=decision-center,unidesk.ai/environment=dev",
},
},
prod: {
targetImage: "unidesk-decision-center:d601",
targetCommitImage: (commit: string) => `unidesk-decision-center:${commit}`,
deployRef: "deploy.json#environments.prod.services.decision-center",
k3s: {
namespace: "unidesk",
manifestRepoPath: "src/components/microservices/k3sctl-adapter/k3s/decision-center.k8s.yaml",
deploymentName: "decision-center",
serviceName: "decision-center",
servicePort: 4277,
containerName: "decision-center",
healthPath: "/health",
},
},
},
},
"frontend": {
@@ -161,15 +192,19 @@ const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
kind: "compose",
registryRepository: "unidesk/frontend",
dockerfile: "src/components/frontend/Dockerfile",
targetImage: "unidesk-frontend",
targetCommitImage: (commit: string) => `unidesk-frontend:${commit}`,
deployRef: "deploy.json#environments.prod.services.frontend",
compose: {
serviceName: "frontend",
containerName: "unidesk-frontend",
deployEnvPrefix: "UNIDESK_FRONTEND_DEPLOY",
healthProbeCommand: "bun -e \"fetch('http://127.0.0.1:8080/health').then(async r=>{const text=await r.text(); console.log(text); process.exit(r.ok?0:1)}).catch(e=>{console.error(e); process.exit(1)})\"",
requireHealthCommit: true,
targets: {
prod: {
targetImage: "unidesk-frontend",
targetCommitImage: (commit: string) => `unidesk-frontend:${commit}`,
deployRef: "deploy.json#environments.prod.services.frontend",
compose: {
serviceName: "frontend",
containerName: "unidesk-frontend",
deployEnvPrefix: "UNIDESK_FRONTEND_DEPLOY",
healthProbeCommand: "bun -e \"fetch('http://127.0.0.1:8080/health').then(async r=>{const text=await r.text(); console.log(text); process.exit(r.ok?0:1)}).catch(e=>{console.error(e); process.exit(1)})\"",
requireHealthCommit: true,
},
},
},
},
"dev:frontend": {
@@ -178,19 +213,23 @@ const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
kind: "d601-k3s",
registryRepository: "unidesk/frontend",
dockerfile: "src/components/frontend/Dockerfile",
targetImage: "unidesk-frontend:dev",
targetCommitImage: (commit: string) => `unidesk-frontend:${commit}`,
deployRef: "origin/master:deploy.json#environments.dev.services.frontend",
k3s: {
namespace: "unidesk-dev",
manifestPath: "/home/ubuntu/cq-deploy/src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-core.k8s.yaml",
deploymentName: "frontend-dev",
serviceName: "frontend-dev",
servicePort: 8080,
containerName: "frontend",
healthPath: "/health",
applySelector: "app.kubernetes.io/name=frontend",
podLabelSelector: "app.kubernetes.io/name=frontend,app.kubernetes.io/component=web,unidesk.ai/environment=dev",
targets: {
dev: {
targetImage: "unidesk-frontend:dev",
targetCommitImage: (commit: string) => `unidesk-frontend:${commit}`,
deployRef: "origin/master:deploy.json#environments.dev.services.frontend",
k3s: {
namespace: "unidesk-dev",
manifestRepoPath: "src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-core.k8s.yaml",
deploymentName: "frontend-dev",
serviceName: "frontend-dev",
servicePort: 8080,
containerName: "frontend",
healthPath: "/health",
applySelector: "app.kubernetes.io/name=frontend",
podLabelSelector: "app.kubernetes.io/name=frontend,app.kubernetes.io/component=web,unidesk.ai/environment=dev",
},
},
},
},
};
@@ -236,6 +275,11 @@ function parseOptions(args: string[]): ArtifactRegistryOptions {
options.dryRun = true;
} else if (arg === "--run-now") {
options.runNow = true;
} else if (arg === "--env" || arg === "--environment") {
const environment = requireValue(args, index, arg);
if (environment !== "dev" && environment !== "prod") throw new Error(`${arg} must be dev or prod`);
options.environment = environment;
index += 1;
} else if (arg === "--provider-id") {
options.providerId = requireValue(args, index, arg);
index += 1;
@@ -556,15 +600,22 @@ function commandTail(result: CommandResult): Record<string, unknown> {
function artifactConsumerSpec(serviceId: string, environment: ArtifactDeployEnvironment | null): ArtifactConsumerSpec | null {
const key = environment === null || environment === "prod" ? serviceId : `${environment}:${serviceId}`;
return artifactConsumerSpecs[key] ?? null;
const explicit = artifactConsumerSpecs[key];
if (explicit !== undefined) return explicit;
const shared = artifactConsumerSpecs[serviceId];
return environment !== null && shared?.targets[environment] !== undefined ? shared : null;
}
function supportedArtifactConsumers(): Array<{ environment: ArtifactDeployEnvironment; serviceId: SupportedArtifactConsumerService; kind: ArtifactConsumerSpec["kind"] }> {
return Object.values(artifactConsumerSpecs).map((spec) => ({
environment: spec.environment ?? "prod",
return Object.values(artifactConsumerSpecs).flatMap((spec) => Object.keys(spec.targets).map((environment) => ({
environment: environment as ArtifactDeployEnvironment,
serviceId: spec.serviceId,
kind: spec.kind,
}));
})));
}
function artifactConsumerTarget(spec: ArtifactConsumerSpec, environment: ArtifactDeployEnvironment | null): ArtifactConsumerTarget | null {
return spec.targets[environment ?? "prod"] ?? null;
}
function unsupportedService(serviceId: string, options: ArtifactRegistryOptions): Record<string, unknown> {
@@ -582,12 +633,28 @@ function unsupportedService(serviceId: string, options: ArtifactRegistryOptions)
};
}
function unsupportedEnvironment(spec: ArtifactConsumerSpec, options: ArtifactRegistryOptions): Record<string, unknown> {
const environment = options.environment ?? "prod";
return {
ok: false,
supported: false,
error: "unsupported-environment",
serviceId: spec.serviceId,
environment,
providerId: options.providerId,
reason: `No standardized ${environment} registry artifact consumer is implemented for ${spec.serviceId}.`,
supportedEnvironments: Object.keys(spec.targets),
policy: "artifact CD must not silently fall back to maintenance-channel source builds or legacy direct deployment",
};
}
function artifactImageRef(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, commit: string): string {
return `127.0.0.1:${options.port}/${spec.registryRepository}:${commit}`;
}
function deployRefFor(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec): string {
return options.deployRef ?? spec.deployRef;
const target = artifactConsumerTarget(spec, options.environment);
return options.deployRef ?? target?.deployRef ?? `deploy.json#environments.${options.environment ?? "prod"}.services.${spec.serviceId}`;
}
function runRemoteScript(options: ArtifactRegistryOptions, script: string, timeoutMs = options.timeoutMs): CommandResult {
@@ -879,9 +946,9 @@ function d601DevFrontendAuthPatchScript(config: UniDeskConfig): string {
].join("\n");
}
function composeArtifactEnvValues(spec: ArtifactConsumerSpec, options: ArtifactRegistryOptions, commit: string): Record<string, string> {
if (spec.compose === undefined) throw new Error(`${spec.serviceId} missing compose artifact consumer config`);
const prefix = spec.compose.deployEnvPrefix;
function composeArtifactEnvValues(spec: ArtifactConsumerSpec, target: ArtifactConsumerTarget, options: ArtifactRegistryOptions, commit: string): Record<string, string> {
if (target.compose === undefined) throw new Error(`${spec.serviceId} missing compose artifact consumer config`);
const prefix = target.compose.deployEnvPrefix;
return {
[`${prefix}_SERVICE_ID`]: spec.serviceId,
[`${prefix}_REF`]: deployRefFor(options, spec),
@@ -891,17 +958,17 @@ function composeArtifactEnvValues(spec: ArtifactConsumerSpec, options: ArtifactR
};
}
async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec): Promise<Record<string, unknown>> {
async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, target: ArtifactConsumerTarget): Promise<Record<string, unknown>> {
const commit = options.commit;
if (commit === null) throw new Error("artifact-registry deploy-service requires --commit <full-sha>");
if (spec.compose === undefined) throw new Error(`${spec.serviceId} missing compose artifact consumer config`);
if (target.compose === undefined) throw new Error(`${spec.serviceId} missing compose artifact consumer config`);
const health = runReadonlyStatus(options, true);
if (health.ok !== true) {
return { ok: false, serviceId: spec.serviceId, error: "D601 artifact registry is not healthy", health };
}
const sourceImage = artifactImageRef(options, spec, commit);
const composeImage = options.targetImage ?? spec.targetImage;
const commitImage = options.targetImage === null ? spec.targetCommitImage(commit) : `${options.targetImage}:${commit}`;
const composeImage = options.targetImage ?? target.targetImage;
const commitImage = `${composeImage}:${commit}`;
const registryProbe = runRemoteScript(options, registryArtifactProbeScript(options, spec, commit), Math.max(options.timeoutMs, 120_000));
if (registryProbe.exitCode !== 0 || registryProbe.timedOut) {
return {
@@ -938,13 +1005,13 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
const config = readConfig();
const runtimeEnv = writeComposeEnv(config, false);
upsertEnvFileValues(runtimeEnv.envFile, {
...composeArtifactEnvValues(spec, options, commit),
...composeArtifactEnvValues(spec, target, options, commit),
});
const compose = resolveComposeCommand(config, runtimeEnv.envFile);
const projectIndex = compose.indexOf("-p");
const composeProject = projectIndex >= 0 && compose[projectIndex + 1] !== undefined ? compose[projectIndex + 1] : "unidesk";
const serviceName = spec.compose.serviceName;
const containerName = spec.compose.containerName;
const serviceName = target.compose.serviceName;
const containerName = target.compose.containerName;
const serviceLogPrefix = spec.serviceId.replace(/-/gu, "_");
const upScript = [
"set -euo pipefail",
@@ -968,9 +1035,9 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
"actual_commit=$(docker image inspect -f '{{ index .Config.Labels \"unidesk.ai/source-commit\" }}' \"$image_id\")",
`test "$actual_commit" = ${shellQuote(commit)}`,
`health_json=/tmp/unidesk-${safeName(spec.serviceId)}-health.json`,
`docker exec "$cid" sh -lc ${shellQuote(spec.compose.healthProbeCommand)} > "$health_json"`,
`docker exec "$cid" sh -lc ${shellQuote(target.compose.healthProbeCommand)} > "$health_json"`,
"cat \"$health_json\"",
...(spec.compose.requireHealthCommit ? [
...(target.compose.requireHealthCommit ? [
"health_commit=$(python3 -c 'import json,sys; print(((json.load(open(sys.argv[1])).get(\"deploy\") or {}).get(\"commit\") or \"\"))' \"$health_json\")",
`test "$health_commit" = ${shellQuote(commit)}`,
] : []),
@@ -1005,7 +1072,7 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
validation: {
liveCommit: commit,
imageLabelCommit: commit,
serviceHealthCommit: spec.compose.requireHealthCommit ? commit : "not-required",
serviceHealthCommit: target.compose.requireHealthCommit ? commit : "not-required",
healthyOldVersionAccepted: false,
},
rollback: {
@@ -1017,11 +1084,17 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
async function deployBackendCoreNow(options: ArtifactRegistryOptions): Promise<Record<string, unknown>> {
if (options.commit === null) throw new Error("artifact-registry deploy-backend-core requires --commit <full-sha>");
return deployComposeArtifactNow(options, artifactConsumerSpecs["backend-core"]);
const spec = artifactConsumerSpecs["backend-core"];
const target = artifactConsumerTarget(spec, options.environment);
if (target === null) return unsupportedEnvironment(spec, options);
return deployComposeArtifactNow(options, spec, target);
}
function deployBackendCoreJob(args: string[], options: ArtifactRegistryOptions): Record<string, unknown> {
if (options.commit === null) throw new Error("artifact-registry deploy-backend-core requires --commit <full-sha>");
const spec = artifactConsumerSpecs["backend-core"];
const target = artifactConsumerTarget(spec, options.environment);
if (target === null) return unsupportedEnvironment(spec, options);
const runArgs = args.includes("--run-now") ? args : [...args, "--run-now"];
const command = [process.execPath, rootPath("scripts", "cli.ts"), "artifact-registry", ...runArgs];
const job = startJob("artifact_registry_backend_core_cd", command, `Pull and deploy backend-core artifact ${options.commit} from D601 registry`);
@@ -1035,13 +1108,15 @@ function deployBackendCoreJob(args: string[], options: ArtifactRegistryOptions):
};
}
function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, commit: string): Record<string, unknown> {
function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, target: ArtifactConsumerTarget, commit: string): Record<string, unknown> {
const environment = options.environment ?? "prod";
const sourceImage = artifactImageRef(options, spec, commit);
const common = {
ok: true,
supported: true,
dryRun: true,
mutation: false,
environment,
providerId: options.providerId,
serviceId: spec.serviceId,
commit,
@@ -1057,42 +1132,42 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
method: "HEAD",
url: `http://127.0.0.1:${options.port}/v2/${spec.registryRepository}/manifests/${commit}`,
},
boundary: "artifact 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",
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`,
};
if (spec.kind === "compose") {
if (spec.compose === undefined) throw new Error(`${spec.serviceId} missing compose artifact consumer config`);
if (target.compose === undefined) throw new Error(`${spec.serviceId} missing compose artifact consumer config`);
return {
...common,
target: {
kind: "compose",
composeService: spec.compose.serviceName,
containerName: spec.compose.containerName,
targetImage: options.targetImage ?? spec.targetImage,
runtimeImage: spec.targetCommitImage(commit),
deployEnvPrefix: spec.compose.deployEnvPrefix,
deployCommandShape: `docker compose up -d --no-build --no-deps --force-recreate ${spec.compose.serviceName}`,
composeService: target.compose.serviceName,
containerName: target.compose.containerName,
targetImage: options.targetImage ?? target.targetImage,
runtimeImage: target.targetCommitImage(commit),
deployEnvPrefix: target.compose.deployEnvPrefix,
deployCommandShape: `docker compose up -d --no-build --no-deps --force-recreate ${target.compose.serviceName}`,
},
validation: [
"D601 registry /v2 manifest exists for the commit tag",
"loaded image labels match service id, source commit, and Dockerfile",
"running Compose container image label matches the requested commit",
spec.compose.requireHealthCommit
target.compose.requireHealthCommit
? `${spec.serviceId} /health succeeds and reports deploy.commit matching the artifact commit`
: `${spec.serviceId} /health succeeds for the recreated container`,
],
rollback: rollbackInfo(spec, commit),
rollback: rollbackInfo(spec, target, environment, commit),
};
}
return {
...common,
target: {
kind: "d601-k3s",
namespace: spec.k3s?.namespace,
deployment: spec.k3s?.deploymentName,
service: spec.k3s?.serviceName,
stableImage: spec.targetImage,
runtimeImage: spec.targetCommitImage(commit),
manifestPath: spec.k3s?.manifestPath,
namespace: target.k3s?.namespace,
deployment: target.k3s?.deploymentName,
service: target.k3s?.serviceName,
stableImage: target.targetImage,
runtimeImage: target.targetCommitImage(commit),
manifestRepoPath: target.k3s?.manifestRepoPath,
deployCommandShape: "kubectl set image + set env + annotate + rollout status",
},
validation: [
@@ -1100,17 +1175,16 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
"D601 Docker-pulled image labels match service id, source commit, and Dockerfile",
"native k3s containerd has the commit image and stable runtime image tag",
"Deployment annotation and pod image id label match the requested commit",
"service health via Kubernetes API service proxy returns the same deploy.commit",
...(spec.environment === "dev" && spec.serviceId === "frontend" ? ["dev frontend auth/session config is synced from main-server config before rollout"] : []),
"service health via Kubernetes API service proxy returns the same deploy.commit and deploy.requestedCommit",
...(environment === "dev" && spec.serviceId === "frontend" ? ["dev frontend auth/session config is synced from main-server config before rollout"] : []),
],
rollback: rollbackInfo(spec, commit),
rollback: rollbackInfo(spec, target, environment, commit),
};
}
function rollbackInfo(spec: ArtifactConsumerSpec, commit: string): Record<string, unknown> {
const environment = spec.environment ?? "prod";
function rollbackInfo(spec: ArtifactConsumerSpec, target: ArtifactConsumerTarget, environment: ArtifactDeployEnvironment, commit: string): Record<string, unknown> {
if (spec.kind === "compose") {
const compose = spec.compose;
const compose = target.compose;
return {
type: "compose-retag-recreate",
composeService: compose?.serviceName,
@@ -1121,18 +1195,22 @@ function rollbackInfo(spec: ArtifactConsumerSpec, commit: string): Record<string
return {
type: "d601-k3s-previous-commit",
serviceId: spec.serviceId,
environment,
currentCommit: commit,
discovery: `kubectl -n ${spec.k3s?.namespace} rollout history deployment/${spec.k3s?.deploymentName} && kubectl -n ${spec.k3s?.namespace} get deployment ${spec.k3s?.deploymentName} -o jsonpath='{.metadata.annotations.unidesk\\.ai/deploy-previous-commit}'`,
commandShape: `bun scripts/cli.ts deploy apply --env ${environment} --service ${spec.serviceId}${environment === "prod" ? " --commit <previous-full-sha>" : ""}`,
discovery: `kubectl -n ${target.k3s?.namespace} rollout history deployment/${target.k3s?.deploymentName} && kubectl -n ${target.k3s?.namespace} get deployment ${target.k3s?.deploymentName} -o jsonpath='{.metadata.annotations.unidesk\\.ai/deploy-previous-commit}'`,
commandShape: `bun scripts/cli.ts deploy apply --env ${environment} --service ${spec.serviceId} --commit <previous-full-sha>`,
note: "Rollback is exposed as the same artifact consumer pointed at a previous commit-pinned image that still exists in D601 registry.",
};
}
function d601K3sArtifactDeployScript(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, commit: string, config: UniDeskConfig): string {
if (spec.k3s === undefined) throw new Error(`${spec.serviceId} missing k3s artifact consumer config`);
function d601K3sArtifactDeployScript(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, target: ArtifactConsumerTarget, commit: string): string {
const environment = options.environment ?? "prod";
if (target.k3s === undefined) throw new Error(`${spec.serviceId} missing k3s artifact consumer config for ${environment}`);
const manifestText = readFileSync(rootPath(target.k3s.manifestRepoPath), "utf8");
const manifestBase64 = Buffer.from(manifestText, "utf8").toString("base64");
const sourceImage = artifactImageRef(options, spec, commit);
const commitImage = spec.targetCommitImage(commit);
const k3s = spec.k3s;
const commitImage = target.targetCommitImage(commit);
const k3s = target.k3s;
const labels = [
`unidesk.ai/deploy-service-id=${spec.serviceId}`,
`unidesk.ai/deploy-ref=${deployRefFor(options, spec)}`,
@@ -1140,13 +1218,15 @@ function d601K3sArtifactDeployScript(options: ArtifactRegistryOptions, spec: Art
`unidesk.ai/deploy-commit=${commit}`,
`unidesk.ai/deploy-requested-commit=${commit}`,
`unidesk.ai/image-source=${sourceImage}`,
`unidesk.ai/deploy-environment=${environment}`,
];
return [
"set -euo pipefail",
rootExecPrelude(),
`registry_image=${shellQuote(sourceImage)}`,
`stable_image=${shellQuote(spec.targetImage)}`,
`stable_image=${shellQuote(target.targetImage)}`,
`commit_image=${shellQuote(commitImage)}`,
`environment=${shellQuote(environment)}`,
`service_id=${shellQuote(spec.serviceId)}`,
`source_repo=${shellQuote(options.sourceRepo)}`,
`deploy_ref=${shellQuote(deployRefFor(options, spec))}`,
@@ -1158,10 +1238,11 @@ function d601K3sArtifactDeployScript(options: ArtifactRegistryOptions, spec: Art
`service_name=${shellQuote(k3s.serviceName)}`,
`service_port=${shellQuote(String(k3s.servicePort))}`,
`health_path=${shellQuote(k3s.healthPath)}`,
`manifest=${shellQuote(k3s.manifestPath)}`,
`apply_selector=${shellQuote(k3s.applySelector ?? "")}`,
`pod_selector=${shellQuote(k3s.podLabelSelector ?? `app.kubernetes.io/name=${k3s.deploymentName}`)}`,
"health_tmp=",
`manifest_repo_path=${shellQuote(k3s.manifestRepoPath)}`,
`manifest_b64=${shellQuote(manifestBase64)}`,
"export KUBECONFIG=/etc/rancher/k3s/k3s.yaml",
"command -v docker >/dev/null",
"command -v kubectl >/dev/null",
@@ -1178,13 +1259,16 @@ function d601K3sArtifactDeployScript(options: ArtifactRegistryOptions, spec: Art
"docker tag \"$registry_image\" \"$stable_image\"",
"docker tag \"$registry_image\" \"$commit_image\"",
"archive=$(mktemp /tmp/unidesk-artifact-k3s-image.XXXXXX.tar)",
"trap 'rm -f \"$archive\" \"$health_tmp\"' EXIT",
"manifest=$(mktemp /tmp/unidesk-artifact-k3s-manifest.XXXXXX.yaml)",
"trap 'rm -f \"$archive\" \"$manifest\" \"$health_tmp\"' EXIT",
"docker save \"$commit_image\" \"$stable_image\" -o \"$archive\"",
"root_exec ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images import \"$archive\" >/dev/null",
"root_exec ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images ls | grep -F \"$stable_image\" >/dev/null",
"root_exec ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images ls | grep -F \"$commit_image\" >/dev/null",
"if [ -f \"$manifest\" ]; then if [ -n \"$apply_selector\" ]; then kubectl apply -f \"$manifest\" -l \"$apply_selector\"; else kubectl apply -f \"$manifest\"; fi; else echo artifact_cd_manifest_missing=$manifest; fi",
...(spec.environment === "dev" && spec.serviceId === "frontend" ? [d601DevFrontendAuthPatchScript(config)] : []),
"printf '%s' \"$manifest_b64\" | base64 -d > \"$manifest\"",
"grep -F \"name: $deployment\" \"$manifest\" >/dev/null",
"if [ -n \"$apply_selector\" ]; then kubectl apply -f \"$manifest\" -l \"$apply_selector\"; else kubectl apply -f \"$manifest\"; fi",
...(environment === "dev" && spec.serviceId === "frontend" ? [d601DevFrontendAuthPatchScript(readConfig())] : []),
"previous_commit=$(kubectl -n \"$namespace\" get deployment \"$deployment\" -o jsonpath='{.metadata.annotations.unidesk\\.ai/deploy-commit}' 2>/dev/null || true)",
"kubectl -n \"$namespace\" set image \"deployment/$deployment\" \"$container_name=$commit_image\"",
"kubectl -n \"$namespace\" set env \"deployment/$deployment\" \"UNIDESK_DEPLOY_SERVICE_ID=$service_id\" \"UNIDESK_DEPLOY_REF=$deploy_ref\" \"UNIDESK_DEPLOY_REPO=$source_repo\" \"UNIDESK_DEPLOY_COMMIT=$commit\" \"UNIDESK_DEPLOY_REQUESTED_COMMIT=$commit\"",
@@ -1192,7 +1276,9 @@ function d601K3sArtifactDeployScript(options: ArtifactRegistryOptions, spec: Art
"if [ -n \"$previous_commit\" ] && [ \"$previous_commit\" != \"$commit\" ]; then kubectl -n \"$namespace\" annotate \"deployment/$deployment\" \"unidesk.ai/deploy-previous-commit=$previous_commit\" --overwrite; fi",
"kubectl -n \"$namespace\" rollout status \"deployment/$deployment\" --timeout=180s",
"deployment_commit=$(kubectl -n \"$namespace\" get deployment \"$deployment\" -o jsonpath='{.metadata.annotations.unidesk\\.ai/deploy-commit}')",
"deployment_requested_commit=$(kubectl -n \"$namespace\" get deployment \"$deployment\" -o jsonpath='{.metadata.annotations.unidesk\\.ai/deploy-requested-commit}')",
"test \"$deployment_commit\" = \"$commit\"",
"test \"$deployment_requested_commit\" = \"$commit\"",
"deployment_image=$(kubectl -n \"$namespace\" get deployment \"$deployment\" -o jsonpath='{.spec.template.spec.containers[?(@.name==\"'\"$container_name\"'\")].image}')",
"test \"$deployment_image\" = \"$commit_image\"",
"containerd_config_label() {",
@@ -1247,20 +1333,22 @@ function d601K3sArtifactDeployScript(options: ArtifactRegistryOptions, spec: Art
"for attempt in $(seq 1 60); do",
" if kubectl get --raw \"$proxy_path\" > \"$health_tmp\" 2>/tmp/unidesk-artifact-health.err; then",
" health_commit=$(python3 -c 'import json,sys; print(((json.load(open(sys.argv[1])).get(\"deploy\") or {}).get(\"commit\") or \"\"))' \"$health_tmp\" 2>/dev/null || true)",
" health_requested_commit=$(python3 -c 'import json,sys; print(((json.load(open(sys.argv[1])).get(\"deploy\") or {}).get(\"requestedCommit\") or \"\"))' \"$health_tmp\" 2>/dev/null || true)",
" health_ok=$(python3 -c 'import json,sys; print(\"true\" if json.load(open(sys.argv[1])).get(\"ok\") is True else \"false\")' \"$health_tmp\" 2>/dev/null || true)",
" echo \"artifact_cd_health_probe attempt=$attempt ok=$health_ok commit=$health_commit\"",
" if [ \"$health_ok\" = \"true\" ] && [ \"$health_commit\" = \"$commit\" ]; then break; fi",
" echo \"artifact_cd_health_probe attempt=$attempt ok=$health_ok commit=$health_commit requestedCommit=$health_requested_commit\"",
" if [ \"$health_ok\" = \"true\" ] && [ \"$health_commit\" = \"$commit\" ] && [ \"$health_requested_commit\" = \"$commit\" ]; then break; fi",
" else",
" echo \"artifact_cd_health_probe attempt=$attempt request=failed\"",
" fi",
" if [ \"$attempt\" = \"60\" ]; then echo artifact_cd_health_failed >&2; cat \"$health_tmp\" >&2 || true; exit 1; fi",
" sleep 2",
"done",
"printf 'artifact_cd_service=%s\\nartifact_cd_source_image=%s\\nartifact_cd_stable_image=%s\\nartifact_cd_runtime_image=%s\\nartifact_cd_commit=%s\\nartifact_cd_previous_commit=%s\\nartifact_cd_pod=%s\\nartifact_cd_pod_image=%s\\nartifact_cd_pod_image_id=%s\\nartifact_cd_image_label_commit=%s\\nartifact_cd_health_commit=%s\\n' \"$service_id\" \"$registry_image\" \"$stable_image\" \"$commit_image\" \"$commit\" \"$previous_commit\" \"$pod\" \"$pod_image\" \"$pod_image_id\" \"$image_label_commit\" \"$health_commit\"",
"printf 'artifact_cd_service=%s\\nartifact_cd_environment=%s\\nartifact_cd_manifest_repo_path=%s\\nartifact_cd_source_image=%s\\nartifact_cd_stable_image=%s\\nartifact_cd_runtime_image=%s\\nartifact_cd_commit=%s\\nartifact_cd_requested_commit=%s\\nartifact_cd_previous_commit=%s\\nartifact_cd_pod=%s\\nartifact_cd_pod_image=%s\\nartifact_cd_pod_image_id=%s\\nartifact_cd_image_label_commit=%s\\nartifact_cd_health_commit=%s\\nartifact_cd_health_requested_commit=%s\\n' \"$service_id\" \"$environment\" \"$manifest_repo_path\" \"$registry_image\" \"$stable_image\" \"$commit_image\" \"$commit\" \"$deployment_requested_commit\" \"$previous_commit\" \"$pod\" \"$pod_image\" \"$pod_image_id\" \"$image_label_commit\" \"$health_commit\" \"$health_requested_commit\"",
].join("\n");
}
async function deployD601K3sArtifactNow(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec): Promise<Record<string, unknown>> {
async function deployD601K3sArtifactNow(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, target: ArtifactConsumerTarget): Promise<Record<string, unknown>> {
const environment = options.environment ?? "prod";
const commit = options.commit;
if (commit === null) throw new Error("artifact-registry deploy-service requires --commit <full-sha>");
const health = runReadonlyStatus(options, true);
@@ -1278,8 +1366,7 @@ async function deployD601K3sArtifactNow(options: ArtifactRegistryOptions, spec:
registryProbe: commandTail(registryProbe),
};
}
const config = readConfig();
const deployScript = d601K3sArtifactDeployScript(options, spec, commit, config);
const deployScript = d601K3sArtifactDeployScript(options, spec, target, commit);
const deploy = runRemoteScript(options, deployScript, Math.max(options.timeoutMs, 420_000));
if (deploy.exitCode !== 0 || deploy.timedOut) {
return {
@@ -1290,29 +1377,33 @@ async function deployD601K3sArtifactNow(options: ArtifactRegistryOptions, spec:
sourceImage,
registryProbe: commandTail(registryProbe),
deploy: commandTail(deploy),
rollback: rollbackInfo(spec, commit),
rollback: rollbackInfo(spec, target, environment, commit),
};
}
return {
ok: true,
supported: true,
serviceId: spec.serviceId,
environment,
commit,
providerId: options.providerId,
sourceRepo: options.sourceRepo,
deployRef: deployRefFor(options, spec),
sourceImage,
stableImage: spec.targetImage,
runtimeImage: spec.targetCommitImage(commit),
stableImage: target.targetImage,
runtimeImage: target.targetCommitImage(commit),
manifestRepoPath: target.k3s?.manifestRepoPath,
registryProbe: commandTail(registryProbe),
deploy: commandTail(deploy),
validation: {
liveCommit: commit,
liveRequestedCommit: commit,
imageLabelCommit: commit,
serviceHealthCommit: commit,
serviceHealthRequestedCommit: commit,
healthyOldVersionAccepted: false,
},
rollback: rollbackInfo(spec, commit),
rollback: rollbackInfo(spec, target, environment, commit),
};
}
@@ -1321,9 +1412,11 @@ async function deployServiceNow(options: ArtifactRegistryOptions): Promise<Recor
if (options.commit === null) throw new Error("artifact-registry deploy-service requires --commit <full-sha>");
const spec = artifactConsumerSpec(options.serviceId, options.environment);
if (spec === null) return unsupportedService(options.serviceId, options);
if (options.dryRun) return dryRunArtifactConsumerPlan(options, spec, options.commit);
if (spec.kind === "compose") return deployComposeArtifactNow(options, spec);
return deployD601K3sArtifactNow(options, spec);
const target = artifactConsumerTarget(spec, options.environment);
if (target === null) return unsupportedEnvironment(spec, options);
if (options.dryRun) return dryRunArtifactConsumerPlan(options, spec, target, options.commit);
if (spec.kind === "compose") return deployComposeArtifactNow(options, spec, target);
return deployD601K3sArtifactNow(options, spec, target);
}
function deployServiceJob(args: string[], options: ArtifactRegistryOptions): Record<string, unknown> {
@@ -1331,7 +1424,9 @@ function deployServiceJob(args: string[], options: ArtifactRegistryOptions): Rec
if (options.commit === null) throw new Error("artifact-registry deploy-service requires --commit <full-sha>");
const spec = artifactConsumerSpec(options.serviceId, options.environment);
if (spec === null) return unsupportedService(options.serviceId, options);
if (options.dryRun) return dryRunArtifactConsumerPlan(options, spec, options.commit);
const target = artifactConsumerTarget(spec, options.environment);
if (target === null) return unsupportedEnvironment(spec, options);
if (options.dryRun) return dryRunArtifactConsumerPlan(options, spec, target, options.commit);
const runArgs = args.includes("--run-now") ? args : [...args, "--run-now"];
const command = [process.execPath, rootPath("scripts", "cli.ts"), "artifact-registry", ...runArgs];
const job = startJob("artifact_registry_service_cd", command, `Pull and deploy ${options.environment ?? "prod"} ${options.serviceId} artifact ${options.commit} from D601 registry`);
@@ -1344,7 +1439,7 @@ function deployServiceJob(args: string[], options: ArtifactRegistryOptions): Rec
statusCommand: `bun scripts/cli.ts job status ${job.id}`,
tailCommand: `bun scripts/cli.ts job status ${job.id} --tail-bytes 30000`,
note: "User-service CD continues in the background: D601 registry health, commit-pinned artifact check, pull/import, rollout, image-label and live health commit verification.",
rollback: rollbackInfo(spec, options.commit),
rollback: rollbackInfo(spec, target, options.environment ?? "prod", options.commit),
};
}
@@ -1367,7 +1462,8 @@ function localHelp(): Record<string, unknown> {
"bun scripts/cli.ts artifact-registry deploy-service --service baidu-netdisk --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --service frontend --env prod --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --service frontend --env dev --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --service decision-center --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env dev --service decision-center --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service decision-center --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
],
firstStage: "install now writes the rendered systemd/Compose/config files and starts the registry",
artifactConsumers: {
@@ -1382,6 +1478,7 @@ function localHelp(): Record<string, unknown> {
],
devCommands: [
"bun scripts/cli.ts deploy apply --env dev --service frontend",
"bun scripts/cli.ts deploy apply --env dev --service decision-center",
],
rollbackShape: "rerun the same artifact consumer with a previous commit-pinned image",
},
+143 -25
View File
@@ -79,6 +79,7 @@ interface ServiceRuntimeState {
deploymentMode: string;
currentCommit: string | null;
healthCommit: string | null;
healthRequestedCommit: string | null;
imageCommit: string | null;
orchestratorCommit: string | null;
healthOk: boolean;
@@ -135,7 +136,7 @@ const nativeK3sCtrAddress = "/run/k3s/containerd/containerd.sock";
const unideskRepoUrl = "https://github.com/pikasTech/unidesk";
const d601MaintenanceDeployAllowedServiceIds = new Set<string>(["backend-core", "k3sctl-adapter", "code-queue"]);
const devApplySupportedServiceIds = new Set<string>(["backend-core"]);
const devArtifactConsumerServiceIds = new Set<string>(["baidu-netdisk", "frontend"]);
const devArtifactConsumerServiceIds = new Set<string>(["baidu-netdisk", "decision-center", "frontend"]);
const prodArtifactConsumerServiceIds = new Set<string>(["backend-core", "baidu-netdisk", "decision-center", "frontend"]);
const deployEnvironmentTargets: Record<DeployEnvironment, DeployEnvironmentTarget> = {
dev: {
@@ -199,7 +200,7 @@ export function deployHelp(action: string | undefined = undefined): Record<strin
},
options: [
{ name: "--file <path>", default: defaultDeployFile, description: "Desired-state manifest path relative to the repo root. JSON and ESM JS manifests are supported, for example deploy.json or develop.js. Local manifest apply allows k3sctl-adapter and explicit production code-queue controlled rollout on D601." },
{ name: "--env <dev|prod>", description: "Read the named environment from origin/master:deploy.json. Dev apply supports backend-core target-side rollout plus frontend/baidu-netdisk artifact consumers; prod apply uses the D601 registry artifact consumer for backend-core, frontend, baidu-netdisk, and decision-center." },
{ name: "--env <dev|prod>", description: "Read the named environment from origin/master:deploy.json. Dev apply supports backend-core target-side rollout plus reviewed artifact consumers for frontend, baidu-netdisk, and decision-center; prod apply uses the D601 registry artifact consumer for backend-core, frontend, baidu-netdisk, and decision-center." },
{ name: "--service <id>", description: "Limit reconcile to one service from the manifest." },
{ name: "--commit <full-sha>", description: "Prod artifact rollback/apply override for a selected service; the image must already exist in D601 registry." },
{ name: "--dry-run", description: "Prepare and validate without mutating the target service." },
@@ -662,6 +663,20 @@ function devK3sDeployService(id: string): UniDeskMicroserviceConfig | undefined
allowedMethods: ["GET", "HEAD"],
allowedPathPrefixes: ["/"],
},
"decision-center": {
name: "UniDesk Dev Decision Center",
description: "Isolated dev Decision Center deployed into D601 native k3s namespace unidesk-dev.",
dockerfile: "src/components/microservices/decision-center/Dockerfile",
composeFile: "src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-decision-center.k8s.yaml",
composeService: "decision-center-dev",
containerName: "k3s:decision-center-dev",
nodeBaseUrl: "k3s://decision-center-dev",
nodePort: 4277,
healthPath: "/health",
route: "/dev/decision-center",
allowedMethods: ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"],
allowedPathPrefixes: ["/", "/api/", "/logs"],
},
"code-queue": {
name: "UniDesk Dev Code Queue",
description: "Isolated dev Code Queue execution plane deployed into D601 native k3s namespace unidesk-dev.",
@@ -738,7 +753,12 @@ function isCoreDeployService(service: UniDeskMicroserviceConfig): boolean {
function isDevK3sDeployService(service: UniDeskMicroserviceConfig): boolean {
return service.deployment.mode === "k3sctl-managed"
&& devApplySupportedServiceIds.has(service.id);
&& (devApplySupportedServiceIds.has(service.id) || devArtifactConsumerServiceIds.has(service.id));
}
function isDevArtifactConsumerService(service: UniDeskMicroserviceConfig): boolean {
return service.deployment.mode === "k3sctl-managed"
&& devArtifactConsumerServiceIds.has(service.id);
}
function isD601MaintenanceDeployBlocked(service: UniDeskMicroserviceConfig): boolean {
@@ -1822,6 +1842,12 @@ function healthDeployCommit(body: Record<string, unknown> | null): string | null
return commit.length > 0 ? commit : null;
}
function healthDeployRequestedCommit(body: Record<string, unknown> | null): string | null {
const deploy = asRecord(body?.deploy);
const commit = asString(deploy?.requestedCommit).toLowerCase();
return commit.length > 0 ? commit : null;
}
function healthSummary(response: unknown): Record<string, unknown> {
const record = asRecord(response);
const body = asRecord(record?.body);
@@ -1943,23 +1969,29 @@ function commitMatches(actual: string | null, desired: string): boolean {
function runtimeCommitVerified(
service: UniDeskMicroserviceConfig,
healthCommit: string | null,
healthRequestedCommit: string | null,
imageCommit: string | null,
orchestratorCommit: string | null,
desired: string,
): boolean {
if (service.deployment.mode === "k3sctl-managed") return commitMatches(orchestratorCommit, desired);
if (service.deployment.mode === "k3sctl-managed") {
if (healthRequestedCommit !== null && healthRequestedCommit.length > 0 && !commitMatches(healthRequestedCommit, desired)) return false;
return commitMatches(orchestratorCommit, desired);
}
if (healthCommit !== null && healthCommit.length > 0 && !commitMatches(healthCommit, desired)) return false;
if (healthRequestedCommit !== null && healthRequestedCommit.length > 0 && !commitMatches(healthRequestedCommit, desired)) return false;
return commitMatches(imageCommit, desired);
}
function runtimeCurrentCommit(
service: UniDeskMicroserviceConfig,
healthCommit: string | null,
healthRequestedCommit: string | null,
imageCommit: string | null,
orchestratorCommit: string | null,
): string | null {
if (service.deployment.mode === "k3sctl-managed") return orchestratorCommit ?? imageCommit;
return healthCommit ?? imageCommit ?? orchestratorCommit;
if (service.deployment.mode === "k3sctl-managed") return orchestratorCommit ?? healthCommit ?? healthRequestedCommit ?? imageCommit;
return healthCommit ?? healthRequestedCommit ?? imageCommit ?? orchestratorCommit;
}
function coreBody(response: unknown): Record<string, unknown> | null {
@@ -2195,18 +2227,37 @@ async function readK8sCommit(config: UniDeskConfig, service: UniDeskMicroservice
return commit.length > 0 ? commit : null;
}
function manifestEnvironmentForService(service: UniDeskMicroserviceConfig): DeployEnvironment {
return service.id === "decision-center" && service.deployment.namespace === "unidesk-dev" ? "dev" : "prod";
}
function serviceDeployRef(service: UniDeskMicroserviceConfig): string {
return `deploy.json#environments.${manifestEnvironmentForService(service)}.services.${service.id}`;
}
function k3sDeploymentManifestPath(service: UniDeskMicroserviceConfig): string {
if (service.id === "decision-center" && service.deployment.namespace === "unidesk-dev") {
return "src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-decision-center.k8s.yaml";
}
if (service.id === "decision-center") {
return "src/components/microservices/k3sctl-adapter/k3s/decision-center.k8s.yaml";
}
return k8sManifestPath(service);
}
async function readRuntimeState(config: UniDeskConfig, service: UniDeskMicroserviceConfig, desired: DeployManifestService): Promise<ServiceRuntimeState> {
const reason = unsupportedReason(service);
const health = await serviceHealth(config, service);
const healthBody = coreBody(health);
const healthCommit = healthDeployCommit(healthBody);
const healthRequestedCommit = healthDeployRequestedCommit(healthBody);
const healthRecord = asRecord(health);
const healthOk = healthRecord?.ok === true && healthBody?.ok !== false;
const [imageCommit, orchestratorCommit] = await Promise.all([
readDockerImageCommit(config, service).catch(() => null),
readK8sCommit(config, service).catch(() => null),
]);
const currentCommit = runtimeCurrentCommit(service, healthCommit, imageCommit, orchestratorCommit);
const currentCommit = runtimeCurrentCommit(service, healthCommit, healthRequestedCommit, imageCommit, orchestratorCommit);
return {
serviceId: service.id,
ok: reason === null,
@@ -2218,10 +2269,11 @@ async function readRuntimeState(config: UniDeskConfig, service: UniDeskMicroserv
deploymentMode: service.deployment.mode,
currentCommit,
healthCommit,
healthRequestedCommit,
imageCommit,
orchestratorCommit,
healthOk,
upToDate: reason === null && healthOk && runtimeCommitVerified(service, healthCommit, imageCommit, orchestratorCommit, desired.commitId),
upToDate: reason === null && healthOk && runtimeCommitVerified(service, healthCommit, healthRequestedCommit, imageCommit, orchestratorCommit, desired.commitId),
raw: { health: healthSummary(health) },
};
}
@@ -2233,9 +2285,9 @@ async function healthVerify(config: UniDeskConfig, service: UniDeskMicroserviceC
let latest: ServiceRuntimeState | null = null;
while (Date.now() < deadline) {
latest = await readRuntimeState(config, service, { ...desired, commitId: resolvedCommit });
const commitOk = runtimeCommitVerified(service, latest.healthCommit, latest.imageCommit, latest.orchestratorCommit, resolvedCommit);
const commitOk = runtimeCommitVerified(service, latest.healthCommit, latest.healthRequestedCommit, latest.imageCommit, latest.orchestratorCommit, resolvedCommit);
const ok = latest.healthOk && commitOk;
progressLine("live-health", "probe", { serviceId: service.id, ok, healthOk: latest.healthOk, expectedCommit: resolvedCommit, healthCommit: latest.healthCommit, imageCommit: latest.imageCommit, orchestratorCommit: latest.orchestratorCommit });
progressLine("live-health", "probe", { serviceId: service.id, ok, healthOk: latest.healthOk, expectedCommit: resolvedCommit, healthCommit: latest.healthCommit, healthRequestedCommit: latest.healthRequestedCommit, imageCommit: latest.imageCommit, orchestratorCommit: latest.orchestratorCommit });
if (ok) {
return {
step: "live-health",
@@ -2297,23 +2349,75 @@ async function ensureGithubSshIdentityStep(config: UniDeskConfig, service: UniDe
}
}
async function runDevArtifactConsumerService(
service: UniDeskMicroserviceConfig,
desired: DeployManifestService,
options: DeployOptions,
before: ServiceRuntimeState,
): Promise<Record<string, unknown>> {
const commit = options.commitOverride ?? desired.commitId;
const artifactArgs = [
"deploy-service",
"--env", "dev",
"--service", service.id,
"--commit", commit,
"--source-repo", desired.repo,
"--timeout-ms", String(options.timeoutMs),
"--run-now",
...(options.dryRun ? ["--dry-run"] : []),
];
progressLine("dev-artifact-cd", options.dryRun ? "dry-run" : "reconcile", { serviceId: service.id, commit });
const result = await runArtifactRegistryCommand(artifactArgs);
const ok = asRecord(result)?.ok === true;
return {
ok,
action: ok ? "deployed" : "failed",
environment: "dev",
executor: "d601-registry-artifact-consumer",
resolvedCommit: commit,
before,
after: result,
steps: [{
step: "artifact-registry-consumer",
ok,
detail: JSON.stringify(result),
startedAt: nowIso(),
finishedAt: nowIso(),
raw: result,
}],
};
}
async function applyOneService(config: UniDeskConfig, service: UniDeskMicroserviceConfig, desired: DeployManifestService, options: DeployOptions): Promise<Record<string, unknown>> {
const steps: StepResult[] = [];
const startedAt = nowIso();
const reason = unsupportedReason(service);
if (reason !== null) return { ok: false, serviceId: service.id, skipped: true, reason, steps };
const before = await readRuntimeState(config, service, desired);
if (!options.force && before.upToDate) return { ok: true, serviceId: service.id, action: "noop", before, steps };
if (manifestEnvironmentForService(service) === "dev" && isDevArtifactConsumerService(service)) {
const artifact = await runDevArtifactConsumerService(service, desired, options, before);
return {
...artifact,
before,
serviceId: service.id,
startedAt,
finishedAt: nowIso(),
};
}
if (options.dryRun) return { ok: true, serviceId: service.id, action: "would-deploy", before, steps };
if (!options.dryRun && isD601MaintenanceDeployBlocked(service)) {
return {
ok: false,
serviceId: service.id,
skipped: true,
reason: `D601 target-side deployment is allowed only for k3sctl-adapter and dev backend-core; ${service.id} is not enabled. Use ci run-dev-e2e for smoke verification.`,
reason: `D601 target-side deployment is allowed only for k3sctl-adapter and dev backend-core; artifact consumers must use registry CD. ${service.id} is not enabled. Use ci run-dev-e2e for smoke verification.`,
steps,
};
}
const reason = unsupportedReason(service);
if (reason !== null) return { ok: false, serviceId: service.id, skipped: true, reason, steps };
const before = await readRuntimeState(config, service, desired);
if (!options.force && before.upToDate) return { ok: true, serviceId: service.id, action: "noop", before, steps };
if (options.dryRun) return { ok: true, serviceId: service.id, action: "would-deploy", before, steps };
const runId = `${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
const exportDir = targetExportDir(service, runId);
@@ -2438,6 +2542,8 @@ function environmentDryRunPlan(
commitId: options.commitOverride !== null && service.id === options.serviceId ? options.commitOverride : service.commitId,
}));
const fingerprint = databaseFingerprint(target);
const devUnsupported = devUnsupportedServices(manifest, options.serviceId);
const prodArtifactUnsupported = prodArtifactUnsupportedServices(manifest, options.serviceId);
return {
ok: environment === "prod"
? services.every((service) => prodArtifactConsumerServiceIds.has(service.id))
@@ -2480,7 +2586,7 @@ function environmentDryRunPlan(
? "d601-registry-artifact-consumer"
: "unsupported"
: devArtifactConsumerServiceIds.has(service.id)
? service.id === "frontend" ? "d601-registry-artifact-consumer" : "d601-registry-artifact-consumer-dev-validation"
? "d601-registry-artifact-consumer"
: devApplySupportedServiceIds.has(service.id)
? "d601-dev-target-side-build"
: "unsupported",
@@ -2493,8 +2599,15 @@ function environmentDryRunPlan(
? "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.",
}
: environment === "dev" && !devApplySupportedServiceIds.has(service.id) && !devArtifactConsumerServiceIds.has(service.id)
? {
ok: false,
supported: false,
reason: "No standardized dev D601 registry artifact consumer is implemented for this service; legacy maintenance-channel deployment is not allowed.",
}
: undefined,
})),
unsupported: environment === "prod" ? prodArtifactUnsupported : devUnsupported,
};
}
@@ -2512,7 +2625,7 @@ function blockedD601MaintenanceDeployServices(config: UniDeskConfig, manifest: D
}
function d601MaintenanceDeployBlockMessage(blocked: string[]): string {
return `D601 target-side deployment is enabled only for k3sctl-adapter and dev backend-core; blocked services: ${blocked.join(", ")}. Use ci run-dev-e2e for dev smoke verification.`;
return `D601 target-side deployment is enabled only for k3sctl-adapter and dev backend-core; artifact consumers must use registry CD. Blocked services: ${blocked.join(", ")}. Use ci run-dev-e2e for dev smoke verification.`;
}
function selectedDevArtifactServices(manifest: DeployManifest, serviceId: string | null): DeployManifestService[] {
@@ -2532,6 +2645,11 @@ function selectedEnvironmentServices(manifest: DeployManifest, serviceId: string
return services;
}
function devUnsupportedServices(manifest: DeployManifest, serviceId: string | null): DeployManifestService[] {
if (manifest.environment !== "dev") return [];
return selectedEnvironmentServices(manifest, serviceId).filter((service) => !devApplySupportedServiceIds.has(service.id) && !devArtifactConsumerServiceIds.has(service.id));
}
function prodArtifactUnsupportedServices(manifest: DeployManifest, serviceId: string | null): DeployManifestService[] {
return selectedEnvironmentServices(manifest, serviceId).filter((service) => !prodArtifactConsumerServiceIds.has(service.id));
}
@@ -2569,7 +2687,7 @@ async function runArtifactConsumerApplyNow(
"--commit", commit,
"--source-repo", service.repo,
"--deploy-ref", `${deployEnvironmentTargets[environment].gitRef}:deploy.json#environments.${environment}.services.${service.id}`,
...(environment === "prod" || service.id === "frontend" ? ["--env", environment] : []),
...(environment === "prod" || service.id === "frontend" || service.id === "decision-center" ? ["--env", environment] : []),
"--timeout-ms", String(options.timeoutMs),
"--run-now",
...(options.dryRun ? ["--dry-run"] : []),
@@ -2690,7 +2808,7 @@ export async function runDeployCommand(config: UniDeskConfig | null, args: strin
}
const unsupported = unsupportedDevApplyServices(manifest, options.serviceId);
if (unsupported.length > 0) {
throw new Error(`deploy apply --env dev currently supports backend-core target-side rollout plus frontend/baidu-netdisk artifact consumers; unsupported selected services: ${unsupported.join(", ")}. Use ci run-dev-e2e for smoke verification.`);
throw new Error(`deploy apply --env dev currently supports backend-core target-side rollout plus frontend/baidu-netdisk/decision-center artifact consumers; unsupported selected services: ${unsupported.join(", ")}. Use ci run-dev-e2e for smoke verification.`);
}
const devArtifactServices = selectedDevArtifactServices(manifest, options.serviceId);
const devTargetServices = selectedDevTargetServices(manifest, options.serviceId);
@@ -2703,11 +2821,11 @@ export async function runDeployCommand(config: UniDeskConfig | null, args: strin
}
if (config === null) throw new Error("deploy apply --env dev requires config.json");
if (!options.dryRun) {
const blocked = blockedD601MaintenanceDeployServices(config, manifest, options.serviceId);
const blocked = blockedD601MaintenanceDeployServices(config, manifest, options.serviceId).filter((serviceId) => serviceId !== "decision-center");
if (blocked.length > 0) throw new Error(d601MaintenanceDeployBlockMessage(blocked));
}
if (!options.runNow) return applyJob(config, args, options);
return await runApplyNow(config, manifest, options);
if (options.dryRun || options.runNow) return await runApplyNow(config, manifest, options);
return applyJob(config, args, options);
}
if (config === null) throw new Error("deploy local manifest mode requires config.json");
const manifest = resolveManifestCommits(await readDeployManifest(options.file), options.serviceId);
@@ -2716,8 +2834,8 @@ export async function runDeployCommand(config: UniDeskConfig | null, args: strin
const blocked = blockedD601MaintenanceDeployServices(config, manifest, options.serviceId);
if (blocked.length > 0) throw new Error(d601MaintenanceDeployBlockMessage(blocked));
}
if (!options.runNow) return applyJob(config, args, options);
return await runApplyNow(config, manifest, options);
if (options.dryRun || options.runNow) return await runApplyNow(config, manifest, options);
return applyJob(config, args, options);
}
export async function runCodeQueueDeployCompatCommand(_config: UniDeskConfig, args: string[]): Promise<unknown> {
+2 -2
View File
@@ -36,9 +36,9 @@ export function rootHelp(): unknown {
{ command: "decision list [--type ...] [--status ...] [--level ...] [--linked-goal-id id] [--limit N]", description: "List Decision Center records through the user-service proxy." },
{ command: "decision requirement list|upsert [--id id] [--title text] [--body-file path] [--type goal|decision|blocker|debt|experiment]", description: "Manage requirement records over the existing records model, excluding meeting records." },
{ command: "decision show <id>", description: "Show one Decision Center record." },
{ command: "deploy check|plan|apply [--file deploy.json|--env dev|prod] [--service id] [--commit full-sha] [--dry-run] [--force]", description: "Reconcile services from a repo+commit manifest; --env reads origin/master:deploy.json environments and can apply supported dev services or supported prod artifact consumers." },
{ command: "deploy check|plan|apply [--file deploy.json|--env dev|prod] [--service id] [--commit full-sha] [--dry-run] [--force]", description: "Reconcile services from a repo+commit manifest; --env reads origin/master:deploy.json environments and can apply supported dev target-side builds plus decision-center's dev artifact consumer, or supported prod artifact consumers." },
{ command: "dev-env validate|prewarm-images", description: "Validate D601 unidesk-dev guardrails or prewarm dev foundation images into native k3s containerd through a bounded async job." },
{ command: "artifact-registry plan|render|status|health|install|deploy-backend-core|deploy-service", description: "Manage the D601 host-managed CNCF Distribution registry and run pull-only artifact CD for supported services." },
{ command: "artifact-registry plan|render|status|health|install|deploy-backend-core|deploy-service", description: "Manage the D601 host-managed CNCF Distribution registry and run pull-only artifact CD for supported services, including decision-center dev/prod artifact consumers." },
{ command: "schedule list|get|runs|run|delete", description: "Manage backend-core scheduled tasks and run history; schedule run <id> supports --wait-ms N." },
{ command: "schedule upsert-pgdata-backup [--time HH:MM] [--remote-base /SERVER_DATA/UNIDESK_PG_DATA]", description: "Create or update the daily PGDATA physical backup task that uploads monthly rotated archives to Baidu Netdisk." },
{ command: "codex deploy <commitId> [--provider-id D601] [--timeout-ms N]", description: "Compatibility wrapper for deploy apply --service code-queue with a temporary repo+commit manifest." },
@@ -41,7 +41,7 @@ services:
K3SCTL_NATIVE_SERVICE_URL_MDTODO: "${K3SCTL_NATIVE_SERVICE_URL_MDTODO:-}"
K3SCTL_NATIVE_SERVICE_URL_DECISION_CENTER: "${K3SCTL_NATIVE_SERVICE_URL_DECISION_CENTER:-}"
K3SCTL_NATIVE_SERVICE_URL_DEVOPS: "${K3SCTL_NATIVE_SERVICE_URL_DEVOPS:-}"
K3SCTL_MANIFEST_PATHS: "${K3SCTL_MANIFEST_PATHS:-k3s/code-queue.k3s.json,k3s/mdtodo.k3s.json,k3s/claudeqq.k3s.json,k3s/decision-center.k3s.json,k3s/dev/unidesk-dev-core.k3s.json}"
K3SCTL_MANIFEST_PATHS: "${K3SCTL_MANIFEST_PATHS:-k3s/code-queue.k3s.json,k3s/mdtodo.k3s.json,k3s/claudeqq.k3s.json,k3s/decision-center.k3s.json,k3s/dev/unidesk-dev-core.k3s.json,k3s/dev/unidesk-dev-decision-center.k3s.json}"
K3SCTL_SERVICES_JSON: "${K3SCTL_SERVICES_JSON:-[]}"
UNIDESK_LOG_RETENTION_BYTES: "${UNIDESK_LOG_RETENTION_BYTES:-512MiB}"
volumes:
@@ -0,0 +1,37 @@
{
"apiVersion": "unidesk.ai/k3s/v1",
"kind": "ManagedKubernetesService",
"metadata": {
"name": "decision-center-dev",
"namespace": "unidesk-dev"
},
"spec": {
"adapterServiceId": "k3sctl-adapter",
"controlPlane": {
"type": "kubernetes",
"cluster": "unidesk-k3s",
"context": "unidesk-k3s"
},
"route": {
"kind": "kubernetes-service",
"serviceName": "decision-center-dev",
"servicePort": 4277
},
"activeInstanceId": "D601-dev-decision-center",
"singleWriter": true,
"expectedNodeIds": [
"D601"
],
"instances": [
{
"id": "D601-dev-decision-center",
"nodeId": "D601",
"role": "primary",
"baseUrl": "kubernetes://unidesk-dev/services/decision-center-dev:4277",
"healthPath": "/health",
"healthMode": "service-proxy"
}
],
"requireAllInstancesHealthy": true
}
}
@@ -0,0 +1,122 @@
apiVersion: v1
kind: Service
metadata:
name: decision-center-dev
namespace: unidesk-dev
labels:
app.kubernetes.io/name: decision-center
app.kubernetes.io/part-of: unidesk
app.kubernetes.io/component: decision-center
unidesk.ai/environment: dev
unidesk.ai/deployment-mode: k3sctl-managed
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: decision-center
app.kubernetes.io/part-of: unidesk
app.kubernetes.io/component: decision-center
unidesk.ai/environment: dev
ports:
- name: http
port: 4277
targetPort: http
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: decision-center-dev
namespace: unidesk-dev
labels:
app.kubernetes.io/name: decision-center
app.kubernetes.io/part-of: unidesk
app.kubernetes.io/component: decision-center
unidesk.ai/environment: dev
unidesk.ai/deployment-mode: k3sctl-managed
annotations:
unidesk.ai/deploy-ref: deploy.json#environments.dev.services.decision-center
unidesk.ai/deploy-service-id: decision-center
unidesk.ai/image-source: deploy-env-commit
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: decision-center
app.kubernetes.io/part-of: unidesk
app.kubernetes.io/component: decision-center
unidesk.ai/environment: dev
template:
metadata:
labels:
app.kubernetes.io/name: decision-center
app.kubernetes.io/part-of: unidesk
app.kubernetes.io/component: decision-center
unidesk.ai/environment: dev
unidesk.ai/node-id: D601
unidesk.ai/deploy-service-id: decision-center
spec:
nodeSelector:
unidesk.ai/node-id: D601
terminationGracePeriodSeconds: 15
containers:
- name: decision-center
image: unidesk-decision-center:dev-placeholder
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 4277
env:
- name: HOST
value: "0.0.0.0"
- name: PORT
value: "4277"
- name: DATABASE_URL
value: "postgres://unidesk:unidesk_dev_password@d601-tcp-egress-gateway.unidesk.svc.cluster.local:15432/unidesk_dev"
- name: DATABASE_POOL_MAX
value: "2"
- name: UNIDESK_DEPLOY_SERVICE_ID
value: decision-center
- name: UNIDESK_DEPLOY_REPO
value: https://github.com/pikasTech/unidesk
- name: UNIDESK_DEPLOY_COMMIT
value: replace-with-deploy-env-commit
- name: UNIDESK_DEPLOY_REQUESTED_COMMIT
value: replace-with-deploy-env-commit
- name: LOG_FILE
value: /var/log/unidesk-dev/decision-center-dev.jsonl
volumeMounts:
- name: logs
mountPath: /var/log/unidesk-dev
readinessProbe:
httpGet:
path: /health
port: http
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 18
livenessProbe:
httpGet:
path: /live
port: http
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 6
startupProbe:
httpGet:
path: /live
port: http
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 30
resources:
requests:
cpu: 50m
memory: 96Mi
limits:
memory: 512Mi
volumes:
- name: logs
hostPath:
path: /home/ubuntu/cq-deploy/.state/decision-center-dev/logs
type: DirectoryOrCreate
@@ -291,7 +291,7 @@ function mergeServices(services: ManagedService[]): ManagedService[] {
}
function readConfig(): RuntimeConfig {
const paths = manifestPaths(envString("K3SCTL_MANIFEST_PATHS", "k3s/code-queue.k3s.json,k3s/mdtodo.k3s.json,k3s/claudeqq.k3s.json,k3s/decision-center.k3s.json,k3s/dev/unidesk-dev-core.k3s.json"));
const paths = manifestPaths(envString("K3SCTL_MANIFEST_PATHS", "k3s/code-queue.k3s.json,k3s/mdtodo.k3s.json,k3s/claudeqq.k3s.json,k3s/decision-center.k3s.json,k3s/dev/unidesk-dev-core.k3s.json,k3s/dev/unidesk-dev-decision-center.k3s.json"));
const inlineServices = parseServices(envString("K3SCTL_SERVICES_JSON", "[]"));
const manifestServices = readManifestServices(paths);
return {