Add decision-center k3s artifact consumer paths
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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 server,retag 为 `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 server,retag 为 `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 路径必须满足:
|
||||
|
||||
|
||||
@@ -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 consumers,dev 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 consumers,dev 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 guard,core manifest 必须包含 `backend-core-dev`/`frontend-dev` Deployment/Service,Code 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 rollout,persistent 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 rollout,persistent 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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 CD:Decision 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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
+37
@@ -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
|
||||
}
|
||||
}
|
||||
+122
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user