feat: extend main-server artifact consumers

This commit is contained in:
Codex
2026-05-20 01:07:52 +00:00
parent b28f693fda
commit 93d9488d36
17 changed files with 519 additions and 34 deletions
+1 -1
View File
@@ -38,7 +38,7 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文
- `bun scripts/cli.ts microservice list/status/health/diagnostics/tunnel-self-test/proxy`:管理和验证挂载在主 server、计算节点 Docker 或 k3s 控制面上的用户服务,`proxy` 支持受控 JSON bodyOA Event Flow/Todo Note/Baidu Netdisk/Code Queue Manager on main-server、k3s Control/Code Queue 执行面/MDTODO/Decision Center/FindJob/Pipeline/MET Nonlinear on D601 的规则见 `docs/reference/microservices.md` - `bun scripts/cli.ts microservice list/status/health/diagnostics/tunnel-self-test/proxy`:管理和验证挂载在主 server、计算节点 Docker 或 k3s 控制面上的用户服务,`proxy` 支持受控 JSON bodyOA Event Flow/Todo Note/Baidu Netdisk/Code Queue Manager on main-server、k3s Control/Code Queue 执行面/MDTODO/Decision Center/FindJob/Pipeline/MET Nonlinear on D601 的规则见 `docs/reference/microservices.md`
- `bun scripts/cli.ts decision upload/list/show/health`:通过 backend-core 用户服务代理上传会议记录/决议 Markdown、列出记录和查看详情;Decision Center 运行在 D601 k3s,规则见 `docs/reference/microservices.md` - `bun scripts/cli.ts decision upload/list/show/health`:通过 backend-core 用户服务代理上传会议记录/决议 Markdown、列出记录和查看详情;Decision Center 运行在 D601 k3s,规则见 `docs/reference/microservices.md`
- `bun scripts/cli.ts decision diary import/list/months/show`:把带日期标题的工作日志 Markdown 拆成 `YYYY-MM/YYYY-MM-DD.md` 日记条目并写入 PostgreSQL,规则见 `docs/reference/microservices.md` - `bun scripts/cli.ts decision diary import/list/months/show`:把带日期标题的工作日志 Markdown 拆成 `YYYY-MM/YYYY-MM-DD.md` 日记条目并写入 PostgreSQL,规则见 `docs/reference/microservices.md`
- `bun scripts/cli.ts deploy check/plan/apply [--file deploy.json|--env dev|prod] [--service <id>]`:按根目录 `deploy.json``origin/master:deploy.json#environments.<env>` 的服务 repo 和 commit 期望状态校验或更新用户服务;`--env dev` 当前开放 D601 `backend-core`/`frontend` persistent dev rollout `baidu-netdisk` artifact-consumer validation,规则见 `docs/reference/deploy.md``docs/reference/dev-environment.md` - `bun scripts/cli.ts deploy check/plan/apply [--file deploy.json|--env dev|prod] [--service <id>]`:按根目录 `deploy.json``origin/master:deploy.json#environments.<env>` 的服务 repo 和 commit 期望状态校验或更新用户服务;`--env dev` 当前开放 D601 `backend-core` persistent dev rollout 以及 `frontend`/`baidu-netdisk`/`decision-center`/`project-manager`/`oa-event-flow`/`code-queue-mgr` artifact-consumer validation`todo-note` 仅 dry-run规则见 `docs/reference/deploy.md``docs/reference/dev-environment.md`
- `bun scripts/cli.ts dev-env validate [--manifest path] [--kubectl-dry-run]` / `dev-env prewarm-images`:离线校验 D601 `unidesk-dev` 生产隔离护栏,或把开发底座基础镜像预热到 D601 原生 k3s containerd,规则见 `docs/reference/deploy.md``docs/reference/microservices.md` - `bun scripts/cli.ts dev-env validate [--manifest path] [--kubectl-dry-run]` / `dev-env prewarm-images`:离线校验 D601 `unidesk-dev` 生产隔离护栏,或把开发底座基础镜像预热到 D601 原生 k3s containerd,规则见 `docs/reference/deploy.md``docs/reference/microservices.md`
- `bun scripts/cli.ts artifact-registry plan|render|status|health|install|deploy-backend-core|deploy-service`:管理 D601 host-managed CNCF Distribution registry,并通过短生命周期 relay 或 D601 pull/import 做 commit-pinned pull-only artifact CD`deploy-backend-core` 是 deprecated 兼容名,backend-core prod CD 标准入口是 `deploy apply --env prod --service backend-core`,规则见 `docs/reference/artifact-registry.md` - `bun scripts/cli.ts artifact-registry plan|render|status|health|install|deploy-backend-core|deploy-service`:管理 D601 host-managed CNCF Distribution registry,并通过短生命周期 relay 或 D601 pull/import 做 commit-pinned pull-only artifact CD`deploy-backend-core` 是 deprecated 兼容名,backend-core prod CD 标准入口是 `deploy apply --env prod --service backend-core`,规则见 `docs/reference/artifact-registry.md`
- `bun scripts/cli.ts ci install/status/run/publish-backend-core/publish-user-service/run-dev-e2e/logs`:在 D601 原生 k3s 上安装和运行 Tekton CI,支持每 commit 检查、Code Queue 只读性能门禁、backend-core 与 user-service commit-pinned 镜像发布和手动触发的 `origin/master:deploy.json#environments.dev` 临时 namespace e2e`run-dev-e2e` 的 Git 控制 runner、短 launcher 和 no-CD 边界见 `docs/reference/dev-ci-runner.md`Tekton 规则见 `docs/reference/ci.md` - `bun scripts/cli.ts ci install/status/run/publish-backend-core/publish-user-service/run-dev-e2e/logs`:在 D601 原生 k3s 上安装和运行 Tekton CI,支持每 commit 检查、Code Queue 只读性能门禁、backend-core 与 user-service commit-pinned 镜像发布和手动触发的 `origin/master:deploy.json#environments.dev` 临时 namespace e2e`run-dev-e2e` 的 Git 控制 runner、短 launcher 和 no-CD 边界见 `docs/reference/dev-ci-runner.md`Tekton 规则见 `docs/reference/ci.md`
+27
View File
@@ -127,6 +127,33 @@
"imageRef": "127.0.0.1:5000/unidesk/frontend:{{sourceCommit}}", "imageRef": "127.0.0.1:5000/unidesk/frontend:{{sourceCommit}}",
"digestRef": "127.0.0.1:5000/unidesk/frontend@{{digest}}" "digestRef": "127.0.0.1:5000/unidesk/frontend@{{digest}}"
}, },
{
"serviceId": "project-manager",
"sourceRepo": "https://github.com/pikasTech/unidesk",
"dockerfile": "src/components/microservices/project-manager/Dockerfile",
"imageRepository": "unidesk/project-manager",
"imageRef": "127.0.0.1:5000/unidesk/project-manager:{{sourceCommit}}",
"digestRef": "127.0.0.1:5000/unidesk/project-manager@{{digest}}",
"publishCommand": "bun scripts/cli.ts ci publish-user-service --service project-manager --commit <full-sha>"
},
{
"serviceId": "oa-event-flow",
"sourceRepo": "https://github.com/pikasTech/unidesk",
"dockerfile": "src/components/microservices/oa-event-flow/Dockerfile",
"imageRepository": "unidesk/oa-event-flow",
"imageRef": "127.0.0.1:5000/unidesk/oa-event-flow:{{sourceCommit}}",
"digestRef": "127.0.0.1:5000/unidesk/oa-event-flow@{{digest}}",
"publishCommand": "bun scripts/cli.ts ci publish-user-service --service oa-event-flow --commit <full-sha>"
},
{
"serviceId": "code-queue-mgr",
"sourceRepo": "https://github.com/pikasTech/unidesk",
"dockerfile": "src/components/microservices/code-queue-mgr/Dockerfile",
"imageRepository": "unidesk/code-queue-mgr",
"imageRef": "127.0.0.1:5000/unidesk/code-queue-mgr:{{sourceCommit}}",
"digestRef": "127.0.0.1:5000/unidesk/code-queue-mgr@{{digest}}",
"publishCommand": "bun scripts/cli.ts ci publish-user-service --service code-queue-mgr --commit <full-sha>"
},
{ {
"serviceId": "backend-core", "serviceId": "backend-core",
"sourceRepo": "https://github.com/pikasTech/unidesk", "sourceRepo": "https://github.com/pikasTech/unidesk",
+20
View File
@@ -107,6 +107,26 @@
"repo": "https://github.com/pikasTech/unidesk", "repo": "https://github.com/pikasTech/unidesk",
"commitId": "54c1f8e165f90fa8509fda1f0c01f8c3fa82cbee" "commitId": "54c1f8e165f90fa8509fda1f0c01f8c3fa82cbee"
}, },
{
"id": "todo-note",
"repo": "https://gitee.com/Lyon1998/todo_note",
"commitId": "a14ce0eb855a685fa17b47adacd54623e72cd2ff"
},
{
"id": "project-manager",
"repo": "https://github.com/pikasTech/unidesk",
"commitId": "0c3cdb4ee06a23361ed511a2da033d67b53d16f4"
},
{
"id": "oa-event-flow",
"repo": "https://github.com/pikasTech/unidesk",
"commitId": "0c3cdb4ee06a23361ed511a2da033d67b53d16f4"
},
{
"id": "code-queue-mgr",
"repo": "https://github.com/pikasTech/unidesk",
"commitId": "22b02e7ce98a32647f8c3962dbf90aafabd53ff0"
},
{ {
"id": "code-queue", "id": "code-queue",
"repo": "https://github.com/pikasTech/unidesk", "repo": "https://github.com/pikasTech/unidesk",
+20
View File
@@ -108,6 +108,11 @@ services:
CODE_QUEUE_REMOTE_WORKDIR: "${UNIDESK_CODE_QUEUE_REMOTE_WORKDIR:-/home/ubuntu}" CODE_QUEUE_REMOTE_WORKDIR: "${UNIDESK_CODE_QUEUE_REMOTE_WORKDIR:-/home/ubuntu}"
LOG_FILE: "/var/log/unidesk/${UNIDESK_LOG_DAY}/${UNIDESK_LOG_PREFIX}_code-queue-mgr.jsonl" LOG_FILE: "/var/log/unidesk/${UNIDESK_LOG_DAY}/${UNIDESK_LOG_PREFIX}_code-queue-mgr.jsonl"
UNIDESK_LOG_RETENTION_BYTES: "${UNIDESK_LOG_RETENTION_BYTES:-1GiB}" UNIDESK_LOG_RETENTION_BYTES: "${UNIDESK_LOG_RETENTION_BYTES:-1GiB}"
UNIDESK_CODE_QUEUE_MGR_DEPLOY_REF: "${UNIDESK_CODE_QUEUE_MGR_DEPLOY_REF:-deploy.json#environments.prod.services.code-queue-mgr}"
UNIDESK_CODE_QUEUE_MGR_DEPLOY_SERVICE_ID: "${UNIDESK_CODE_QUEUE_MGR_DEPLOY_SERVICE_ID:-code-queue-mgr}"
UNIDESK_CODE_QUEUE_MGR_DEPLOY_REPO: "${UNIDESK_CODE_QUEUE_MGR_DEPLOY_REPO:-}"
UNIDESK_CODE_QUEUE_MGR_DEPLOY_COMMIT: "${UNIDESK_CODE_QUEUE_MGR_DEPLOY_COMMIT:-}"
UNIDESK_CODE_QUEUE_MGR_DEPLOY_REQUESTED_COMMIT: "${UNIDESK_CODE_QUEUE_MGR_DEPLOY_REQUESTED_COMMIT:-}"
volumes: volumes:
- ${UNIDESK_LOG_DIR}:/var/log/unidesk - ${UNIDESK_LOG_DIR}:/var/log/unidesk
healthcheck: healthcheck:
@@ -144,6 +149,11 @@ services:
TODO_NOTE_REMINDER_SCAN_INTERVAL_MS: "${UNIDESK_TODO_NOTE_REMINDER_SCAN_INTERVAL_MS:-30000}" TODO_NOTE_REMINDER_SCAN_INTERVAL_MS: "${UNIDESK_TODO_NOTE_REMINDER_SCAN_INTERVAL_MS:-30000}"
TODO_NOTE_REMINDER_CLAUDEQQ_TIMEOUT_MS: "${UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_TIMEOUT_MS:-15000}" TODO_NOTE_REMINDER_CLAUDEQQ_TIMEOUT_MS: "${UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_TIMEOUT_MS:-15000}"
TODO_NOTE_REMINDER_CLAUDEQQ_SEND_ATTEMPTS: "${UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_SEND_ATTEMPTS:-3}" TODO_NOTE_REMINDER_CLAUDEQQ_SEND_ATTEMPTS: "${UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_SEND_ATTEMPTS:-3}"
UNIDESK_DEPLOY_REF: "${UNIDESK_TODO_NOTE_DEPLOY_REF:-deploy.json#environments.prod.services.todo-note}"
UNIDESK_DEPLOY_SERVICE_ID: "${UNIDESK_TODO_NOTE_DEPLOY_SERVICE_ID:-todo-note}"
UNIDESK_DEPLOY_REPO: "${UNIDESK_TODO_NOTE_DEPLOY_REPO:-}"
UNIDESK_DEPLOY_COMMIT: "${UNIDESK_TODO_NOTE_DEPLOY_COMMIT:-}"
UNIDESK_DEPLOY_REQUESTED_COMMIT: "${UNIDESK_TODO_NOTE_DEPLOY_REQUESTED_COMMIT:-}"
volumes: volumes:
- ${UNIDESK_LOG_DIR}:/var/log/unidesk - ${UNIDESK_LOG_DIR}:/var/log/unidesk
healthcheck: healthcheck:
@@ -176,6 +186,11 @@ services:
PIPELINE_OA_BRIDGE_RUN_LIMIT: "${UNIDESK_PIPELINE_OA_BRIDGE_RUN_LIMIT:-50}" PIPELINE_OA_BRIDGE_RUN_LIMIT: "${UNIDESK_PIPELINE_OA_BRIDGE_RUN_LIMIT:-50}"
LOG_FILE: "/var/log/unidesk/${UNIDESK_LOG_DAY}/${UNIDESK_LOG_PREFIX}_oa-event-flow.jsonl" LOG_FILE: "/var/log/unidesk/${UNIDESK_LOG_DAY}/${UNIDESK_LOG_PREFIX}_oa-event-flow.jsonl"
UNIDESK_LOG_RETENTION_BYTES: "${UNIDESK_LOG_RETENTION_BYTES:-1GiB}" UNIDESK_LOG_RETENTION_BYTES: "${UNIDESK_LOG_RETENTION_BYTES:-1GiB}"
UNIDESK_DEPLOY_REF: "${UNIDESK_OA_EVENT_FLOW_DEPLOY_REF:-deploy.json#environments.prod.services.oa-event-flow}"
UNIDESK_DEPLOY_SERVICE_ID: "${UNIDESK_OA_EVENT_FLOW_DEPLOY_SERVICE_ID:-oa-event-flow}"
UNIDESK_DEPLOY_REPO: "${UNIDESK_OA_EVENT_FLOW_DEPLOY_REPO:-}"
UNIDESK_DEPLOY_COMMIT: "${UNIDESK_OA_EVENT_FLOW_DEPLOY_COMMIT:-}"
UNIDESK_DEPLOY_REQUESTED_COMMIT: "${UNIDESK_OA_EVENT_FLOW_DEPLOY_REQUESTED_COMMIT:-}"
volumes: volumes:
- ${UNIDESK_LOG_DIR}:/var/log/unidesk - ${UNIDESK_LOG_DIR}:/var/log/unidesk
healthcheck: healthcheck:
@@ -202,6 +217,11 @@ services:
DATABASE_POOL_MAX: "${UNIDESK_PROJECT_MANAGER_DATABASE_POOL_MAX:-1}" DATABASE_POOL_MAX: "${UNIDESK_PROJECT_MANAGER_DATABASE_POOL_MAX:-1}"
LOG_FILE: "/var/log/unidesk/${UNIDESK_LOG_DAY}/${UNIDESK_LOG_PREFIX}_project-manager.jsonl" LOG_FILE: "/var/log/unidesk/${UNIDESK_LOG_DAY}/${UNIDESK_LOG_PREFIX}_project-manager.jsonl"
UNIDESK_LOG_RETENTION_BYTES: "${UNIDESK_LOG_RETENTION_BYTES:-1GiB}" UNIDESK_LOG_RETENTION_BYTES: "${UNIDESK_LOG_RETENTION_BYTES:-1GiB}"
UNIDESK_DEPLOY_REF: "${UNIDESK_PROJECT_MANAGER_DEPLOY_REF:-deploy.json#environments.prod.services.project-manager}"
UNIDESK_DEPLOY_SERVICE_ID: "${UNIDESK_PROJECT_MANAGER_DEPLOY_SERVICE_ID:-project-manager}"
UNIDESK_DEPLOY_REPO: "${UNIDESK_PROJECT_MANAGER_DEPLOY_REPO:-}"
UNIDESK_DEPLOY_COMMIT: "${UNIDESK_PROJECT_MANAGER_DEPLOY_COMMIT:-}"
UNIDESK_DEPLOY_REQUESTED_COMMIT: "${UNIDESK_PROJECT_MANAGER_DEPLOY_REQUESTED_COMMIT:-}"
volumes: volumes:
- ${UNIDESK_LOG_DIR}:/var/log/unidesk - ${UNIDESK_LOG_DIR}:/var/log/unidesk
healthcheck: healthcheck:
+1 -1
View File
@@ -123,7 +123,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。 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。 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 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. 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`. Project Manager and OA Event Flow use the same master-server Compose artifact-consumer shape as Baidu Netdisk, with `project-manager-backend` and `oa-event-flow-backend` as the runtime containers. Code Queue Manager is supported as an artifact consumer for validation, but prod live apply is supervisor-gated. Todo Note currently returns a structured runtime-verification block until the checked-in health contract proves `deploy.commit` and `deploy.requestedCommit`. 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 路径必须满足: 这个 CD 路径必须满足:
+5 -5
View File
@@ -40,7 +40,7 @@ The root `deploy.json` is the single desired-state source for both prod and dev.
The optional non-service execution declaration under `environments.dev` is intentionally not specified here. The only currently allowed declaration is `ci`, and its authoritative `repo`, `scriptPath`, `timeoutMs`, short launcher, host fetch boundary and no-CD rules are defined only in `docs/reference/dev-ci-runner.md`. 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`/`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`. 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 reviewed artifact consumers `frontend`, `baidu-netdisk`, `decision-center`, `project-manager`, `oa-event-flow`, and `code-queue-mgr`; `todo-note` remains dry-run only until runtime verification is proven. `deploy apply --env prod` exposes only reviewed registry artifact consumers (`backend-core`, `frontend`, `baidu-netdisk`, `decision-center`, `project-manager`, `oa-event-flow`), while `code-queue-mgr` remains supervisor-gated and `todo-note` stays blocked. 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`.
For services with reviewed production artifact consumers, local-manifest `deploy apply --file ...` is not a production fallback. The CLI blocks `backend-core`, `frontend`, `baidu-netdisk` and `decision-center` before source materialization or Docker build and directs operators to `deploy apply --env prod --service <id> --commit <full-sha>`. This prevents a dirty worktree, local manifest or target-side source build from bypassing the pull-only artifact CD guardrails. The broader precheck and legacy-path classification live in `docs/reference/cicd-standardization.md`. For services with reviewed production artifact consumers, local-manifest `deploy apply --file ...` is not a production fallback. The CLI blocks `backend-core`, `frontend`, `baidu-netdisk` and `decision-center` before source materialization or Docker build and directs operators to `deploy apply --env prod --service <id> --commit <full-sha>`. This prevents a dirty worktree, local manifest or target-side source build from bypassing the pull-only artifact CD guardrails. The broader precheck and legacy-path classification live in `docs/reference/cicd-standardization.md`.
@@ -85,7 +85,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. `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. 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: 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`, `decision-center`, `project-manager` and `oa-event-flow` use the same selected dev manifest objects but consume the existing D601 registry artifact instead of building source on the target. `code-queue-mgr` uses the same dev manifest validation path for Compose artifact consumer checks; live prod apply remains supervisor-gated. `todo-note` currently returns a structured runtime-verification block until the checked-in health contract proves `deploy.commit` and `deploy.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` - `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` - `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`
@@ -108,7 +108,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 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`/`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. `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`/`project-manager`/`oa-event-flow`/`code-queue-mgr` artifact consumers, while `todo-note` remains dry-run only until runtime verification is proven. `--env prod` apply exposes the D601 registry artifact consumer for `backend-core`, `frontend`, `baidu-netdisk`, `decision-center`, `project-manager`, and `oa-event-flow`; `code-queue-mgr` prod live apply is supervisor-gated and `todo-note` remains blocked. 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. All deploy commands output JSON. Long operations must use `.state/jobs/` and bounded log tails; no deploy path may succeed with missing progress output.
@@ -174,8 +174,8 @@ Main server targets may build without a proxy unless a service explicitly requir
The reconciler selects the executor from `config.json`: The reconciler selects the executor from `config.json`:
- `deployment.mode=unidesk-direct` on `main-server`: the legacy/local manifest executor builds the image on the main server, then uses the fixed UniDesk Compose project and `up -d --no-build --no-deps --force-recreate <service>`. Reviewed artifact-consumer services such as `frontend` and `baidu-netdisk` use the D601 registry pull-only path for `--env dev` and `--env prod` instead. - `deployment.mode=unidesk-direct` on `main-server`: the legacy/local manifest executor builds the image on the main server, then uses the fixed UniDesk Compose project and `up -d --no-build --no-deps --force-recreate <service>`. Reviewed artifact-consumer services such as `frontend`, `baidu-netdisk`, `project-manager` and `oa-event-flow` use the D601 registry pull-only path for `--env dev` and `--env prod` instead.
- `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=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, and prod live apply remains supervisor-gated.
- `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. - `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`. - 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 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. - `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.
+2 -2
View File
@@ -30,7 +30,7 @@ CLI 会优先使用 `docker compose` v2 plugin;当 v2 plugin 不存在时才
Compose v2 安装后仍然必须遵守 UniDesk 的服务控制入口:全栈生命周期用 `server start` / `server stop`,单服务重建用 `server rebuild <service>`。不要因为 v2 可用就直接在生产栈上手工执行未纳入 CLI 的 `up --build``down -v` 或跨项目清理命令;所有会影响容器的动作都应保持 job 可观测、Compose project 固定、database named volume 保留。主 server Compose 命令必须从 `providerGateway.upgrade.hostProjectRoot` 指定的 canonical UniDesk 根目录运行,临时 worktree、Code Queue 导出目录或实验分支不得复用生产 `-p unidesk` 和固定 `container_name` 去替换生产容器。 Compose v2 安装后仍然必须遵守 UniDesk 的服务控制入口:全栈生命周期用 `server start` / `server stop`,单服务重建用 `server rebuild <service>`。不要因为 v2 可用就直接在生产栈上手工执行未纳入 CLI 的 `up --build``down -v` 或跨项目清理命令;所有会影响容器的动作都应保持 job 可观测、Compose project 固定、database named volume 保留。主 server Compose 命令必须从 `providerGateway.upgrade.hostProjectRoot` 指定的 canonical UniDesk 根目录运行,临时 worktree、Code Queue 导出目录或实验分支不得复用生产 `-p unidesk` 和固定 `container_name` 去替换生产容器。
版本化用户服务部署优先使用 `bun scripts/cli.ts deploy apply` 已支持的受控路径;D601 persistent dev apply 当前支持 `backend-core` target-side rollout`frontend` artifact consumer`baidu-netdisk` dev 验证也使用 artifact consumerdev desired-state smoke 使用 `ci run-dev-e2e``deploy.json` 只声明服务 `id``repo``commitId`;目标节点、Dockerfile、Compose、Kubernetes manifest、健康检查和代理路径继续来自 `config.json` 与现有 manifest。主 server 直管微服务和内部 sidecar,例如 `code-queue-mgr`,也必须支持这一路径:`deploy apply --service code-queue-mgr``deploy.json` 指定 commit 导出源码、构建镜像、替换固定 Compose service 并验证运行中镜像/健康信息的 commit。部署默认遵循 target-side build:服务部署到哪台 target,就在哪台 target 从 remote commit 导出源码、一次性代理构建镜像并部署;不得把中心构建镜像作为默认分发路径,也不得用 `docker commit` 或脏 worktree 作为部署输入。production backend-core 是明确例外;`frontend` 是用户服务 UI / 前端镜像化样板,`baidu-netdisk` 是主 server 直管微服务的镜像化样板:D601 CI 构建并推送 commit-pinned 镜像到 D601 artifact registrydev/prod CD 只拉取、retag 或导入、recreate/rollout 和验证,不在 master server 或 dev target 执行 frontend Compose build。完整规则见 `docs/reference/deploy.md`D601 dev/Rust 边界见 `docs/reference/dev-environment.md`artifact registry 见 `docs/reference/artifact-registry.md` 版本化用户服务部署优先使用 `bun scripts/cli.ts deploy apply` 已支持的受控路径;D601 persistent dev apply 当前支持 `backend-core` target-side rollout,以及 `frontend``baidu-netdisk``decision-center``project-manager``oa-event-flow``code-queue-mgr` 的 artifact consumer validation`todo-note` 仅能做 runtime-verification dry-run`deploy.json` 只声明服务 `id``repo``commitId`;目标节点、Dockerfile、Compose、Kubernetes manifest、健康检查和代理路径继续来自 `config.json` 与现有 manifest。主 server 直管微服务和内部 sidecar,例如 `code-queue-mgr`,也必须支持这一路径:`deploy apply --service code-queue-mgr``deploy.json` 指定 commit 导出源码、构建镜像、替换固定 Compose service 并验证运行中镜像/健康信息的 commit,但 prod live apply 仍需 supervisor 确认。部署默认遵循 target-side build:服务部署到哪台 target,就在哪台 target 从 remote commit 导出源码、一次性代理构建镜像并部署;不得把中心构建镜像作为默认分发路径,也不得用 `docker commit` 或脏 worktree 作为部署输入。production backend-core 是明确例外;`frontend` 是用户服务 UI / 前端镜像化样板,`baidu-netdisk` 是主 server 直管微服务的镜像化样板:D601 CI 构建并推送 commit-pinned 镜像到 D601 artifact registrydev/prod CD 只拉取、retag 或导入、recreate/rollout 和验证,不在 master server 或 dev target 执行 frontend Compose build。完整规则见 `docs/reference/deploy.md`D601 dev/Rust 边界见 `docs/reference/dev-environment.md`artifact registry 见 `docs/reference/artifact-registry.md`
## Main Server Swap ## Main Server Swap
@@ -46,7 +46,7 @@ swap 管理不能被强塞进所有热路径。`server start/status` 可以暴
## Single Service Rebuild ## Single Service Rebuild
前端、本机 provider-gateway、dev-frontend-proxy 或主 server 承载的 Todo Note/Code Queue Manager/Project Manager/Baidu Netdisk/OA Event Flow 用户服务需要非版本化本地重建时,统一使用 `bun scripts/cli.ts server rebuild <service>`,其中 `<service>` 只能是 `backend-core``frontend``dev-frontend-proxy``provider-gateway``todo-note``code-queue-mgr``project-manager``baidu-netdisk``oa-event-flow`。需要按 commit 上线或恢复到 desired-state 时必须改用 `bun scripts/cli.ts deploy apply --service <id>`、backend-core artifact CD、`deploy apply --env dev|prod --service frontend` artifact consumer 或 `deploy apply --env dev|prod --service baidu-netdisk` artifact consumer;直管微服务也不能把脏工作树或手工重建作为部署真相。Rust backend-core 迭代不得在 master server 用 `server rebuild backend-core` 编译,生产 backend-core 也不得用该命令完成 Rust 构建,必须走 D601 dev deploy/CI 或 D601 artifact registry CD。`server rebuild frontend``server rebuild baidu-netdisk` 只作为维护/非标准路径保留,不得作为标准发布完成证据。D601 Code Queue 执行面、File Browser、FindJob、Pipeline、MET Nonlinear 和 ClaudeQQ 部署在计算节点,不属于主 server Compose 可重建服务;其中 D601 Code Queue 执行面不得再通过 `codex deploy` 或维护通道直连 D601 部署;未来正式 CD 必须经受控 target-side 路径执行 build-first、rollout 和 live commit 验证。 前端、本机 provider-gateway、dev-frontend-proxy 或主 server 承载的 Todo Note/Code Queue Manager/Project Manager/Baidu Netdisk/OA Event Flow 用户服务需要非版本化本地重建时,统一使用 `bun scripts/cli.ts server rebuild <service>`,其中 `<service>` 只能是 `backend-core``frontend``dev-frontend-proxy``provider-gateway``todo-note``code-queue-mgr``project-manager``baidu-netdisk``oa-event-flow`。需要按 commit 上线或恢复到 desired-state 时必须改用 `bun scripts/cli.ts deploy apply --service <id>`、backend-core artifact CD、`deploy apply --env dev|prod --service frontend` artifact consumer 或 `deploy apply --env dev|prod --service baidu-netdisk` artifact consumer;直管微服务也不能把脏工作树或手工重建作为部署真相。`server rebuild frontend``server rebuild baidu-netdisk``server rebuild project-manager``server rebuild oa-event-flow``server rebuild todo-note``server rebuild code-queue-mgr` 都只作为维护/非标准路径保留,不得作为标准发布完成证据。Rust backend-core 迭代不得在 master server 用 `server rebuild backend-core` 编译,生产 backend-core 也不得用该命令完成 Rust 构建,必须走 D601 dev deploy/CI 或 D601 artifact registry CD。D601 Code Queue 执行面、File Browser、FindJob、Pipeline、MET Nonlinear 和 ClaudeQQ 部署在计算节点,不属于主 server Compose 可重建服务;其中 D601 Code Queue 执行面不得再通过 `codex deploy` 或维护通道直连 D601 部署;未来正式 CD 必须经受控 target-side 路径执行 build-first、rollout 和 live commit 验证。
frontend 改动必须明确上线到公网:修改 `src/components/frontend/src/``src/components/frontend/public/style.css`、frontend 使用的共享 TSX/TS 模块或 WebUI 导航后,标准发布顺序是先把 pushed commit 交给 `bun scripts/cli.ts ci publish-user-service --service frontend --commit <full-sha>`,再用 `bun scripts/cli.ts deploy apply --env dev --service frontend``bun scripts/cli.ts deploy apply --env prod --service frontend` 消费同一个 commit-pinned artifact。公网 WebUI 的 `/app.js``unidesk-frontend` 镜像内运行时 bundle;只改工作区文件、只跑 `bun run check`、只跑 `Bun.build`、只刷新浏览器或只 `server rebuild frontend` 都不能作为标准版本发布证据。 frontend 改动必须明确上线到公网:修改 `src/components/frontend/src/``src/components/frontend/public/style.css`、frontend 使用的共享 TSX/TS 模块或 WebUI 导航后,标准发布顺序是先把 pushed commit 交给 `bun scripts/cli.ts ci publish-user-service --service frontend --commit <full-sha>`,再用 `bun scripts/cli.ts deploy apply --env dev --service frontend``bun scripts/cli.ts deploy apply --env prod --service frontend` 消费同一个 commit-pinned artifact。公网 WebUI 的 `/app.js``unidesk-frontend` 镜像内运行时 bundle;只改工作区文件、只跑 `bun run check`、只跑 `Bun.build`、只刷新浏览器或只 `server rebuild frontend` 都不能作为标准版本发布证据。
+4 -3
View File
@@ -57,7 +57,7 @@ UniDesk 用户服务是挂载到 UniDesk 核心服务上的、面向用户使用
- 代理路径:只允许 `/api/` 前缀;允许方法为 `GET``HEAD``POST``DELETE`,用于保持 Todo Note 原有清单创建/删除、任务增删改、提醒、展开/收起、移动、撤销/重做等功能。 - 代理路径:只允许 `/api/` 前缀;允许方法为 `GET``HEAD``POST``DELETE`,用于保持 Todo Note 原有清单创建/删除、任务增删改、提醒、展开/收起、移动、撤销/重做等功能。
- UniDesk 前端:`用户服务 / Todo Note` React 页面负责展示清单列表、树形任务、筛选、提醒、拖放/上移下移、撤销/重做、字号控制和显式原始 JSON 按钮。 - UniDesk 前端:`用户服务 / Todo Note` React 页面负责展示清单列表、树形任务、筛选、提醒、拖放/上移下移、撤销/重做、字号控制和显式原始 JSON 按钮。
Todo Note 在 UniDesk 语境中按纯后端服务管理:不得继续公开 Todo Note 自身 Vite/Web 前端,也不得把 `4211` 映射为公网端口。浏览器只能通过 UniDesk frontend 的 `/api/microservices/todo-note/...` 同源代理访问 Todo Note 后端。 Todo Note 在 UniDesk 语境中按纯后端服务管理:不得继续公开 Todo Note 自身 Vite/Web 前端,也不得把 `4211` 映射为公网端口。浏览器只能通过 UniDesk frontend 的 `/api/microservices/todo-note/...` 同源代理访问 Todo Note 后端;当前 live prod artifact CD 仍被阻塞,直到 checked-in health contract 能证明 `deploy.commit``deploy.requestedCommit`
Todo Note 首次迁移或源 JSON 修复时,在主 server 通过 Docker 内网执行 `/root/todo_note/scripts/migrate-json-to-pg.ts`,并显式指向主 PostgreSQL`docker run --rm --network unidesk_default -v /root/todo_note:/app -w /app -e DATABASE_URL='postgres://unidesk:unidesk_dev_password@database:5432/unidesk' oven/bun:1-alpine bun scripts/migrate-json-to-pg.ts`。迁移脚本必须输出 `importedInstances: 5``totalTodos: 100``completedTodos: 54` 这一类可审计摘要,不能只依赖前端页面观察。 Todo Note 首次迁移或源 JSON 修复时,在主 server 通过 Docker 内网执行 `/root/todo_note/scripts/migrate-json-to-pg.ts`,并显式指向主 PostgreSQL`docker run --rm --network unidesk_default -v /root/todo_note:/app -w /app -e DATABASE_URL='postgres://unidesk:unidesk_dev_password@database:5432/unidesk' oven/bun:1-alpine bun scripts/migrate-json-to-pg.ts`。迁移脚本必须输出 `importedInstances: 5``totalTodos: 100``completedTodos: 54` 这一类可审计摘要,不能只依赖前端页面观察。
@@ -76,7 +76,7 @@ Todo Note 数据迁移后必须验证:`microservice proxy todo-note /api/insta
- 代理路径:只允许 `/health``/logs``/api/` 前缀;允许方法为 `GET``HEAD``POST` - 代理路径:只允许 `/health``/logs``/api/` 前缀;允许方法为 `GET``HEAD``POST`
- UniDesk 前端:`用户服务 / OA Event Flow` React 页面负责展示服务健康、事件表、tag 过滤、live stream 状态、Trace/STEP stats 表、Code Queue/Pipeline 标签入口和显式原始 JSON 按钮。 - UniDesk 前端:`用户服务 / OA Event Flow` React 页面负责展示服务健康、事件表、tag 过滤、live stream 状态、Trace/STEP stats 表、Code Queue/Pipeline 标签入口和显式原始 JSON 按钮。
OA Event Flow 在 UniDesk 语境中按共享控制面基础设施管理:不得暴露公网端口,不得把事件或统计权威状态写入 `.state/`Code Queue 与 Pipeline 都必须通过该服务发布事实事件、订阅 tag stream 和读取统计中心。共享事件流、统计中心和完成门禁见 `docs/reference/oa-event-flow.md` OA Event Flow 在 UniDesk 语境中按共享控制面基础设施管理:不得暴露公网端口,不得把事件或统计权威状态写入 `.state/`标准发布为 `bun scripts/cli.ts ci publish-user-service --service oa-event-flow --commit <full-sha>`dev/prod 都通过 `bun scripts/cli.ts deploy apply --env dev|prod --service oa-event-flow` 消费同一 commit-pinned artifact 并验证 `/health.deploy.commit`Code Queue 与 Pipeline 都必须通过该服务发布事实事件、订阅 tag stream 和读取统计中心。共享事件流、统计中心和完成门禁见 `docs/reference/oa-event-flow.md`
### Code Queue Manager On Main Server ### Code Queue Manager On Main Server
@@ -85,6 +85,7 @@ OA Event Flow 在 UniDesk 语境中按共享控制面基础设施管理:不得
- 职责:队列 CRUD、任务提交、批量提交、任务移动、queued prompt edit、已读状态、历史摘要、overview、stats、summary、prompt、output/transcript/trace 的轻量 PostgreSQL 读取。 - 职责:队列 CRUD、任务提交、批量提交、任务移动、queued prompt edit、已读状态、历史摘要、overview、stats、summary、prompt、output/transcript/trace 的轻量 PostgreSQL 读取。
- 非职责:不运行 Codex/OpenCode,不包含 Playwright/Chromium,不持有 Docker socket,不创建 dev-container,不执行 judge,不管理 active run steer/interrupt,不做任务调度或 runner。 - 非职责:不运行 Codex/OpenCode,不包含 Playwright/Chromium,不持有 Docker socket,不创建 dev-container,不执行 judge,不管理 active run steer/interrupt,不做任务调度或 runner。
- 资源边界:目标常驻内存不超过 100 MB,默认 PostgreSQL pool 为 `CODE_QUEUE_MGR_DATABASE_POOL_MAX=2``CODE_QUEUE_TRACE_DATABASE_POOL_MAX=1``/health` 必须暴露 `role=master-control-plane``schemaReady`、连接池上限和 `noRunnerDependencies=true` - 资源边界:目标常驻内存不超过 100 MB,默认 PostgreSQL pool 为 `CODE_QUEUE_MGR_DATABASE_POOL_MAX=2``CODE_QUEUE_TRACE_DATABASE_POOL_MAX=1``/health` 必须暴露 `role=master-control-plane``schemaReady`、连接池上限和 `noRunnerDependencies=true`
- 标准发布:`bun scripts/cli.ts ci publish-user-service --service code-queue-mgr --commit <full-sha>` 会在 D601 registry 发布 `127.0.0.1:5000/unidesk/code-queue-mgr:<commit>`dev 可以做 `bun scripts/cli.ts deploy apply --env dev --service code-queue-mgr` 验证,prod 只允许 `bun scripts/cli.ts deploy apply --env prod --service code-queue-mgr --dry-run` 或经 supervisor 单独确认后的 live apply。
- Runtime healthcheck:生产镜像是 Rust slim runtime,不包含 BunDocker Compose healthcheck 必须使用镜像内 `code-queue-mgr --healthcheck``127.0.0.1:4278/health` 做带超时探针,或使用等价的 Rust binary 子命令。不得把 TypeScript/Bun `fetch` 探针用于 `code-queue-mgr`,否则 `server rebuild code-queue-mgr` 的 post-up validation 会误判失败。 - Runtime healthcheck:生产镜像是 Rust slim runtime,不包含 BunDocker Compose healthcheck 必须使用镜像内 `code-queue-mgr --healthcheck``127.0.0.1:4278/health` 做带超时探针,或使用等价的 Rust binary 子命令。不得把 TypeScript/Bun `fetch` 探针用于 `code-queue-mgr`,否则 `server rebuild code-queue-mgr` 的 post-up validation 会误判失败。
- 路由:CLI/WebUI 仍只访问 `/api/microservices/code-queue/proxy/...`backend-core 在内部把控制/读取路径转到 `code-queue-mgr`,把 active run、judge、dev-container、执行面健康和 scheduler 相关路径转到 D601 执行面。 - 路由:CLI/WebUI 仍只访问 `/api/microservices/code-queue/proxy/...`backend-core 在内部把控制/读取路径转到 `code-queue-mgr`,把 active run、judge、dev-container、执行面健康和 scheduler 相关路径转到 D601 执行面。
- 行为兼容:提交与 queued prompt edit 必须保留 Code Queue 环境提示注入、`--reference-task-id`/引用输入解析和引用任务上下文注入,避免 master 控制面路径与 D601 原写服务语义分叉。 - 行为兼容:提交与 queued prompt edit 必须保留 Code Queue 环境提示注入、`--reference-task-id`/引用输入解析和引用任务上下文注入,避免 master 控制面路径与 D601 原写服务语义分叉。
@@ -102,7 +103,7 @@ OA Event Flow 在 UniDesk 语境中按共享控制面基础设施管理:不得
- 代理路径:只允许 `/health``/logs``/api/` 前缀;允许方法为 `GET``HEAD``POST``PUT``DELETE` - 代理路径:只允许 `/health``/logs``/api/` 前缀;允许方法为 `GET``HEAD``POST``PUT``DELETE`
- UniDesk 前端:`用户服务 / Project Manager` React 页面负责展示主 server 仓库引用、私有后端映射、项目指标、项目表格、筛选搜索、编辑表单、Excel 导入和 Excel 导出;完整原始 JSON 只能通过显式 `查看原始JSON` 打开。 - UniDesk 前端:`用户服务 / Project Manager` React 页面负责展示主 server 仓库引用、私有后端映射、项目指标、项目表格、筛选搜索、编辑表单、Excel 导入和 Excel 导出;完整原始 JSON 只能通过显式 `查看原始JSON` 打开。
Project Manager 在 UniDesk 语境中按纯后端服务管理:不得将 `4233` 映射为公网端口。浏览器只能通过 UniDesk frontend 的 `/api/microservices/project-manager/health``/api/microservices/project-manager/proxy/...` 同源代理访问项目管理后端。 Project Manager 的标准发布是 `bun scripts/cli.ts ci publish-user-service --service project-manager --commit <full-sha>`,随后用 `bun scripts/cli.ts deploy apply --env dev --service project-manager``bun scripts/cli.ts deploy apply --env prod --service project-manager` 消费同一 commit-pinned artifact 并验证 live commit / requestedCommit。Project Manager 在 UniDesk 语境中按纯后端服务管理:不得将 `4233` 映射为公网端口。浏览器只能通过 UniDesk frontend 的 `/api/microservices/project-manager/health``/api/microservices/project-manager/proxy/...` 同源代理访问项目管理后端。
### Baidu Netdisk On Main Server ### Baidu Netdisk On Main Server
+41
View File
@@ -15,6 +15,8 @@ This policy does not apply to:
- `release/v1` governance; - `release/v1` governance;
- one-off infrastructure repair actions. - one-off infrastructure repair actions.
`todo-note` is a special-case main-server service: it can participate in deploy validation, but live prod artifact CD stays blocked until the checked-in health contract proves `deploy.commit` and `deploy.requestedCommit`.
## Default Release Flow ## Default Release Flow
The default release flow for a user-service change is: The default release flow for a user-service change is:
@@ -74,6 +76,45 @@ Baidu Netdisk is the canonical main-server direct user-service sample.
- `server rebuild baidu-netdisk` remains a maintenance/local rebuild path only. It is not the standard versioned release truth for Baidu Netdisk. - `server rebuild baidu-netdisk` remains a maintenance/local rebuild path only. It is not the standard versioned release truth for Baidu Netdisk.
- Production acceptance must explicitly verify `microservice health baidu-netdisk`, `microservice proxy baidu-netdisk /api/transfers`, private `4244` exposure, and live commit / artifact information. - Production acceptance must explicitly verify `microservice health baidu-netdisk`, `microservice proxy baidu-netdisk /api/transfers`, private `4244` exposure, and live commit / artifact information.
## Project Manager
Project Manager follows the same commit-pinned artifact flow as Baidu Netdisk, but the runtime target is the master-server Compose service `project-manager`.
- The minimal standard artifact command is `bun scripts/cli.ts ci publish-user-service --service project-manager --commit <full-sha> --wait-ms 1200000`.
- The expected artifact is `127.0.0.1:5000/unidesk/project-manager:<commit>` plus its registry digest from the CI output.
- Dev CD consumes the same artifact with `bun scripts/cli.ts deploy apply --env dev --service project-manager`; prod CD consumes it with `bun scripts/cli.ts deploy apply --env prod --service project-manager`.
- `server rebuild project-manager` remains a maintenance/local rebuild path only. It is not the standard versioned release truth for Project Manager.
- Production acceptance must explicitly verify `microservice health project-manager`, `microservice proxy project-manager /api/projects`, private `4233` exposure, and live commit / artifact information.
## OA Event Flow
OA Event Flow follows the same master-server Compose artifact flow as Project Manager.
- The minimal standard artifact command is `bun scripts/cli.ts ci publish-user-service --service oa-event-flow --commit <full-sha> --wait-ms 1200000`.
- The expected artifact is `127.0.0.1:5000/unidesk/oa-event-flow:<commit>` plus its registry digest from the CI output.
- Dev CD consumes the same artifact with `bun scripts/cli.ts deploy apply --env dev --service oa-event-flow`; prod CD consumes it with `bun scripts/cli.ts deploy apply --env prod --service oa-event-flow`.
- `server rebuild oa-event-flow` remains a maintenance/local rebuild path only. It is not the standard versioned release truth for OA Event Flow.
- Production acceptance must explicitly verify `microservice health oa-event-flow`, `microservice proxy oa-event-flow /api/diagnostics`, private `4255` exposure, and live commit / artifact information.
## Code Queue Manager
`code-queue-mgr` is the master-server internal sidecar control plane, so it follows artifact consumer validation but not unrestricted prod automation.
- The minimal standard artifact command is `bun scripts/cli.ts ci publish-user-service --service code-queue-mgr --commit <full-sha> --wait-ms 1200000`.
- The expected artifact is `127.0.0.1:5000/unidesk/code-queue-mgr:<commit>` plus its registry digest from the CI output.
- Dev validation uses `bun scripts/cli.ts deploy apply --env dev --service code-queue-mgr`; prod dry-run uses `bun scripts/cli.ts deploy apply --env prod --service code-queue-mgr --dry-run`.
- Live prod apply requires explicit supervisor confirmation and is not the worker default.
- `server rebuild code-queue-mgr` remains a maintenance/local rebuild path only. It is not the standard versioned release truth for Code Queue Manager.
- Production acceptance must explicitly verify `microservice health code-queue-mgr`, the Compose container image label, and the live health payload.
## Todo Note
Todo Note is the current blocker case.
- The expected source reference remains `https://gitee.com/Lyon1998/todo_note`.
- Validation may use `bun scripts/cli.ts deploy apply --env dev --service todo-note --dry-run` or the equivalent prod dry-run, but live artifact CD is blocked until the checked-in runtime contract proves `deploy.commit` and `deploy.requestedCommit`.
- `server rebuild todo-note` remains a maintenance/local rebuild path only. It is not the standard versioned release truth for Todo Note.
## Decision Center ## Decision Center
Decision Center is the canonical example of a user service that doubles as a product workflow for requirements, decisions, and daily work diaries. Decision Center is the canonical example of a user service that doubles as a product workflow for requirements, decisions, and daily work diaries.
+245 -4
View File
@@ -69,7 +69,7 @@ const defaultOptions: ArtifactRegistryOptions = {
sourceRepo: "https://github.com/pikasTech/unidesk", sourceRepo: "https://github.com/pikasTech/unidesk",
deployRef: null, deployRef: null,
}; };
const supportedArtifactConsumerServices = ["backend-core", "baidu-netdisk", "decision-center", "frontend"] as const; const supportedArtifactConsumerServices = ["backend-core", "baidu-netdisk", "code-queue-mgr", "decision-center", "frontend", "oa-event-flow", "project-manager", "todo-note"] as const;
type SupportedArtifactConsumerService = typeof supportedArtifactConsumerServices[number]; type SupportedArtifactConsumerService = typeof supportedArtifactConsumerServices[number];
const legacyDeployBackendCoreDisabled = true; const legacyDeployBackendCoreDisabled = true;
@@ -80,6 +80,10 @@ interface ArtifactConsumerSpec {
registryRepository: string; registryRepository: string;
dockerfile: string; dockerfile: string;
targets: Partial<Record<ArtifactDeployEnvironment, ArtifactConsumerTarget>>; targets: Partial<Record<ArtifactDeployEnvironment, ArtifactConsumerTarget>>;
prodLiveApply: "enabled" | "supervisor-only" | "unsupported";
prodLiveBlockReason?: string;
runtimeVerification?: "strict" | "blocked";
runtimeVerificationBlockReason?: string;
} }
interface ArtifactConsumerTarget { interface ArtifactConsumerTarget {
@@ -113,6 +117,7 @@ const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
kind: "compose", kind: "compose",
registryRepository: "unidesk/backend-core", registryRepository: "unidesk/backend-core",
dockerfile: "src/components/backend-core/Dockerfile", dockerfile: "src/components/backend-core/Dockerfile",
prodLiveApply: "enabled",
targets: { targets: {
prod: { prod: {
targetImage: "unidesk-backend-core", targetImage: "unidesk-backend-core",
@@ -134,7 +139,20 @@ const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
kind: "compose", kind: "compose",
registryRepository: "unidesk/baidu-netdisk", registryRepository: "unidesk/baidu-netdisk",
dockerfile: "src/components/microservices/baidu-netdisk/Dockerfile", dockerfile: "src/components/microservices/baidu-netdisk/Dockerfile",
prodLiveApply: "enabled",
targets: { targets: {
dev: {
targetImage: "baidu-netdisk",
targetCommitImage: (commit: string) => `baidu-netdisk:${commit}`,
deployRef: "deploy.json#environments.dev.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,
},
},
prod: { prod: {
targetImage: "baidu-netdisk", targetImage: "baidu-netdisk",
targetCommitImage: (commit: string) => `baidu-netdisk:${commit}`, targetCommitImage: (commit: string) => `baidu-netdisk:${commit}`,
@@ -149,12 +167,48 @@ const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
}, },
}, },
}, },
"code-queue-mgr": {
serviceId: "code-queue-mgr",
environment: "prod",
kind: "compose",
registryRepository: "unidesk/code-queue-mgr",
dockerfile: "src/components/microservices/code-queue-mgr/Dockerfile",
prodLiveApply: "supervisor-only",
prodLiveBlockReason: "code-queue-mgr is the main-server Code Queue control-plane sidecar; live production apply requires explicit supervisor confirmation.",
targets: {
dev: {
targetImage: "code-queue-mgr",
targetCommitImage: (commit: string) => `code-queue-mgr:${commit}`,
deployRef: "deploy.json#environments.dev.services.code-queue-mgr",
compose: {
serviceName: "code-queue-mgr",
containerName: "code-queue-mgr-backend",
deployEnvPrefix: "UNIDESK_CODE_QUEUE_MGR_DEPLOY",
healthProbeCommand: "code-queue-mgr --print-health",
requireHealthCommit: true,
},
},
prod: {
targetImage: "code-queue-mgr",
targetCommitImage: (commit: string) => `code-queue-mgr:${commit}`,
deployRef: "deploy.json#environments.prod.services.code-queue-mgr",
compose: {
serviceName: "code-queue-mgr",
containerName: "code-queue-mgr-backend",
deployEnvPrefix: "UNIDESK_CODE_QUEUE_MGR_DEPLOY",
healthProbeCommand: "code-queue-mgr --print-health",
requireHealthCommit: true,
},
},
},
},
"decision-center": { "decision-center": {
serviceId: "decision-center", serviceId: "decision-center",
environment: "prod", environment: "prod",
kind: "d601-k3s", kind: "d601-k3s",
registryRepository: "unidesk/decision-center", registryRepository: "unidesk/decision-center",
dockerfile: "src/components/microservices/decision-center/Dockerfile", dockerfile: "src/components/microservices/decision-center/Dockerfile",
prodLiveApply: "enabled",
targets: { targets: {
dev: { dev: {
targetImage: "unidesk-decision-center:dev", targetImage: "unidesk-decision-center:dev",
@@ -193,6 +247,7 @@ const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
kind: "compose", kind: "compose",
registryRepository: "unidesk/frontend", registryRepository: "unidesk/frontend",
dockerfile: "src/components/frontend/Dockerfile", dockerfile: "src/components/frontend/Dockerfile",
prodLiveApply: "enabled",
targets: { targets: {
prod: { prod: {
targetImage: "unidesk-frontend", targetImage: "unidesk-frontend",
@@ -208,12 +263,118 @@ const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
}, },
}, },
}, },
"oa-event-flow": {
serviceId: "oa-event-flow",
environment: "prod",
kind: "compose",
registryRepository: "unidesk/oa-event-flow",
dockerfile: "src/components/microservices/oa-event-flow/Dockerfile",
prodLiveApply: "enabled",
targets: {
dev: {
targetImage: "oa-event-flow",
targetCommitImage: (commit: string) => `oa-event-flow:${commit}`,
deployRef: "deploy.json#environments.dev.services.oa-event-flow",
compose: {
serviceName: "oa-event-flow",
containerName: "oa-event-flow-backend",
deployEnvPrefix: "UNIDESK_OA_EVENT_FLOW_DEPLOY",
healthProbeCommand: "bun -e \"fetch('http://127.0.0.1:4255/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,
},
},
prod: {
targetImage: "oa-event-flow",
targetCommitImage: (commit: string) => `oa-event-flow:${commit}`,
deployRef: "deploy.json#environments.prod.services.oa-event-flow",
compose: {
serviceName: "oa-event-flow",
containerName: "oa-event-flow-backend",
deployEnvPrefix: "UNIDESK_OA_EVENT_FLOW_DEPLOY",
healthProbeCommand: "bun -e \"fetch('http://127.0.0.1:4255/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,
},
},
},
},
"project-manager": {
serviceId: "project-manager",
environment: "prod",
kind: "compose",
registryRepository: "unidesk/project-manager",
dockerfile: "src/components/microservices/project-manager/Dockerfile",
prodLiveApply: "enabled",
targets: {
dev: {
targetImage: "project-manager",
targetCommitImage: (commit: string) => `project-manager:${commit}`,
deployRef: "deploy.json#environments.dev.services.project-manager",
compose: {
serviceName: "project-manager",
containerName: "project-manager-backend",
deployEnvPrefix: "UNIDESK_PROJECT_MANAGER_DEPLOY",
healthProbeCommand: "bun -e \"fetch('http://127.0.0.1:4233/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,
},
},
prod: {
targetImage: "project-manager",
targetCommitImage: (commit: string) => `project-manager:${commit}`,
deployRef: "deploy.json#environments.prod.services.project-manager",
compose: {
serviceName: "project-manager",
containerName: "project-manager-backend",
deployEnvPrefix: "UNIDESK_PROJECT_MANAGER_DEPLOY",
healthProbeCommand: "bun -e \"fetch('http://127.0.0.1:4233/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,
},
},
},
},
"todo-note": {
serviceId: "todo-note",
environment: "prod",
kind: "compose",
registryRepository: "unidesk/todo-note",
dockerfile: "Dockerfile",
prodLiveApply: "unsupported",
prodLiveBlockReason: "todo-note source is external to this repository and the current checked-in contract cannot prove /api/health deploy.commit/deploy.requestedCommit support.",
runtimeVerification: "blocked",
runtimeVerificationBlockReason: "todo-note source is external to this repository and the current checked-in contract cannot prove /api/health deploy.commit/deploy.requestedCommit support.",
targets: {
dev: {
targetImage: "todo-note",
targetCommitImage: (commit: string) => `todo-note:${commit}`,
deployRef: "deploy.json#environments.dev.services.todo-note",
compose: {
serviceName: "todo-note",
containerName: "todo-note-backend",
deployEnvPrefix: "UNIDESK_TODO_NOTE_DEPLOY",
healthProbeCommand: "bun -e \"fetch('http://127.0.0.1:4211/api/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,
},
},
prod: {
targetImage: "todo-note",
targetCommitImage: (commit: string) => `todo-note:${commit}`,
deployRef: "deploy.json#environments.prod.services.todo-note",
compose: {
serviceName: "todo-note",
containerName: "todo-note-backend",
deployEnvPrefix: "UNIDESK_TODO_NOTE_DEPLOY",
healthProbeCommand: "bun -e \"fetch('http://127.0.0.1:4211/api/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": { "dev:frontend": {
serviceId: "frontend", serviceId: "frontend",
environment: "dev", environment: "dev",
kind: "d601-k3s", kind: "d601-k3s",
registryRepository: "unidesk/frontend", registryRepository: "unidesk/frontend",
dockerfile: "src/components/frontend/Dockerfile", dockerfile: "src/components/frontend/Dockerfile",
prodLiveApply: "enabled",
targets: { targets: {
dev: { dev: {
targetImage: "unidesk-frontend:dev", targetImage: "unidesk-frontend:dev",
@@ -649,6 +810,58 @@ function unsupportedEnvironment(spec: ArtifactConsumerSpec, options: ArtifactReg
}; };
} }
function artifactConsumerLiveBlock(spec: ArtifactConsumerSpec, options: ArtifactRegistryOptions): Record<string, unknown> | null {
const environment = options.environment ?? "prod";
if (spec.runtimeVerification === "blocked") {
return {
ok: false,
supported: false,
liveApplyAllowed: false,
error: "runtime-verification-blocked",
serviceId: spec.serviceId,
environment,
providerId: options.providerId,
reason: spec.runtimeVerificationBlockReason ?? `${spec.serviceId} does not yet satisfy strict runtime deploy commit verification.`,
requiredBeforeLiveApply: [
"runtime Compose env injects deploy commit/requestedCommit metadata",
"service health reports deploy.commit and deploy.requestedCommit for strict verification",
],
policy: "artifact CD must not accept a healthy old service or silently fall back to legacy rebuild paths",
};
}
if (environment !== "prod" || spec.prodLiveApply === "enabled") return null;
if (spec.prodLiveApply === "supervisor-only") {
return {
ok: false,
supported: true,
liveApplyAllowed: false,
error: "supervisor-confirmation-required",
serviceId: spec.serviceId,
environment,
providerId: options.providerId,
reason: spec.prodLiveBlockReason ?? `${spec.serviceId} production artifact apply requires supervisor confirmation.`,
dryRunCommandShape: `bun scripts/cli.ts artifact-registry deploy-service --env prod --service ${spec.serviceId} --commit <full-sha> --dry-run`,
policy: "worker automation must not perform live production apply for this infrastructure control-plane service",
};
}
return {
ok: false,
supported: false,
liveApplyAllowed: false,
error: "artifact-consumer-blocked",
serviceId: spec.serviceId,
environment,
providerId: options.providerId,
reason: spec.prodLiveBlockReason ?? `${spec.serviceId} does not yet satisfy the artifact consumer runtime verification contract.`,
requiredBeforeLiveApply: [
"CI can publish a commit-pinned image with matching service id, source commit, and Dockerfile labels",
"runtime Compose env injects deploy commit/requestedCommit metadata",
"service health reports deploy.commit and deploy.requestedCommit for strict verification",
],
policy: "do not silently fall back to server rebuild, dirty worktrees, mutable tags, or source builds on the runtime target",
};
}
function artifactImageRef(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, commit: string): string { function artifactImageRef(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, commit: string): string {
return `127.0.0.1:${options.port}/${spec.registryRepository}:${commit}`; return `127.0.0.1:${options.port}/${spec.registryRepository}:${commit}`;
} }
@@ -1040,7 +1253,9 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
"cat \"$health_json\"", "cat \"$health_json\"",
...(target.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\")", "health_commit=$(python3 -c 'import json,sys; print(((json.load(open(sys.argv[1])).get(\"deploy\") or {}).get(\"commit\") or \"\"))' \"$health_json\")",
"health_requested_commit=$(python3 -c 'import json,sys; print(((json.load(open(sys.argv[1])).get(\"deploy\") or {}).get(\"requestedCommit\") or \"\"))' \"$health_json\")",
`test "$health_commit" = ${shellQuote(commit)}`, `test "$health_commit" = ${shellQuote(commit)}`,
`test "$health_requested_commit" = ${shellQuote(commit)}`,
] : []), ] : []),
].join("\n"); ].join("\n");
const deploy = runCommand(["bash", "-lc", composeLockScript(upScript)], repoRoot, { timeoutMs: Math.max(options.timeoutMs, 300_000) }); const deploy = runCommand(["bash", "-lc", composeLockScript(upScript)], repoRoot, { timeoutMs: Math.max(options.timeoutMs, 300_000) });
@@ -1072,8 +1287,10 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
running: running.stdout.trim(), running: running.stdout.trim(),
validation: { validation: {
liveCommit: commit, liveCommit: commit,
liveRequestedCommit: target.compose.requireHealthCommit ? commit : "not-required",
imageLabelCommit: commit, imageLabelCommit: commit,
serviceHealthCommit: target.compose.requireHealthCommit ? commit : "not-required", serviceHealthCommit: target.compose.requireHealthCommit ? commit : "not-required",
serviceHealthRequestedCommit: target.compose.requireHealthCommit ? commit : "not-required",
healthyOldVersionAccepted: false, healthyOldVersionAccepted: false,
}, },
rollback: { rollback: {
@@ -1125,12 +1342,14 @@ function legacyDeployBackendCoreResult(options: ArtifactRegistryOptions): Record
function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, target: ArtifactConsumerTarget, commit: string): Record<string, unknown> { function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, target: ArtifactConsumerTarget, commit: string): Record<string, unknown> {
const environment = options.environment ?? "prod"; const environment = options.environment ?? "prod";
const verificationBlocked = spec.runtimeVerification === "blocked";
const sourceImage = artifactImageRef(options, spec, commit); const sourceImage = artifactImageRef(options, spec, commit);
const common = { const common = {
ok: true, ok: !verificationBlocked,
supported: true, supported: !verificationBlocked,
dryRun: true, dryRun: true,
mutation: false, mutation: false,
error: verificationBlocked ? "runtime-verification-blocked" : undefined,
environment, environment,
providerId: options.providerId, providerId: options.providerId,
serviceId: spec.serviceId, serviceId: spec.serviceId,
@@ -1148,6 +1367,11 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
url: `http://127.0.0.1:${options.port}/v2/${spec.registryRepository}/manifests/${commit}`, url: `http://127.0.0.1:${options.port}/v2/${spec.registryRepository}/manifests/${commit}`,
}, },
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`, boundary: `${environment} CD is artifact-consumer only: verify commit-pinned registry image, pull/import, deploy, then verify live commit/image/health; it never builds source on the runtime target`,
liveApply: {
policy: spec.prodLiveApply,
allowed: !verificationBlocked && (environment !== "prod" || spec.prodLiveApply === "enabled"),
reason: spec.runtimeVerificationBlockReason ?? spec.prodLiveBlockReason ?? null,
},
}; };
if (spec.kind === "compose") { if (spec.kind === "compose") {
if (target.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`);
@@ -1166,7 +1390,9 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
"D601 registry /v2 manifest exists for the commit tag", "D601 registry /v2 manifest exists for the commit tag",
"loaded image labels match service id, source commit, and Dockerfile", "loaded image labels match service id, source commit, and Dockerfile",
"running Compose container image label matches the requested commit", "running Compose container image label matches the requested commit",
target.compose.requireHealthCommit verificationBlocked
? `blocked: ${spec.runtimeVerificationBlockReason}`
: target.compose.requireHealthCommit
? `${spec.serviceId} /health succeeds and reports deploy.commit matching the artifact commit` ? `${spec.serviceId} /health succeeds and reports deploy.commit matching the artifact commit`
: `${spec.serviceId} /health succeeds for the recreated container`, : `${spec.serviceId} /health succeeds for the recreated container`,
], ],
@@ -1430,6 +1656,8 @@ async function deployServiceNow(options: ArtifactRegistryOptions): Promise<Recor
const target = artifactConsumerTarget(spec, options.environment); const target = artifactConsumerTarget(spec, options.environment);
if (target === null) return unsupportedEnvironment(spec, options); if (target === null) return unsupportedEnvironment(spec, options);
if (options.dryRun) return dryRunArtifactConsumerPlan(options, spec, target, options.commit); if (options.dryRun) return dryRunArtifactConsumerPlan(options, spec, target, options.commit);
const liveBlock = artifactConsumerLiveBlock(spec, options);
if (liveBlock !== null) return liveBlock;
if (spec.kind === "compose") return deployComposeArtifactNow(options, spec, target); if (spec.kind === "compose") return deployComposeArtifactNow(options, spec, target);
return deployD601K3sArtifactNow(options, spec, target); return deployD601K3sArtifactNow(options, spec, target);
} }
@@ -1442,6 +1670,8 @@ function deployServiceJob(args: string[], options: ArtifactRegistryOptions): Rec
const target = artifactConsumerTarget(spec, options.environment); const target = artifactConsumerTarget(spec, options.environment);
if (target === null) return unsupportedEnvironment(spec, options); if (target === null) return unsupportedEnvironment(spec, options);
if (options.dryRun) return dryRunArtifactConsumerPlan(options, spec, target, options.commit); if (options.dryRun) return dryRunArtifactConsumerPlan(options, spec, target, options.commit);
const liveBlock = artifactConsumerLiveBlock(spec, options);
if (liveBlock !== null) return liveBlock;
const runArgs = args.includes("--run-now") ? args : [...args, "--run-now"]; const runArgs = args.includes("--run-now") ? args : [...args, "--run-now"];
const command = [process.execPath, rootPath("scripts", "cli.ts"), "artifact-registry", ...runArgs]; 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`); const job = startJob("artifact_registry_service_cd", command, `Pull and deploy ${options.environment ?? "prod"} ${options.serviceId} artifact ${options.commit} from D601 registry`);
@@ -1478,6 +1708,10 @@ function localHelp(): Record<string, unknown> {
"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 frontend --env dev --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 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]", "bun scripts/cli.ts artifact-registry deploy-service --env prod --service decision-center --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service project-manager --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service oa-event-flow --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service code-queue-mgr --commit <full-sha> --dry-run [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service todo-note --commit <full-sha> --dry-run [--provider-id D601]",
], ],
firstStage: "install now writes the rendered systemd/Compose/config files and starts the registry", firstStage: "install now writes the rendered systemd/Compose/config files and starts the registry",
artifactConsumers: { artifactConsumers: {
@@ -1489,10 +1723,17 @@ function localHelp(): Record<string, unknown> {
"bun scripts/cli.ts deploy apply --env prod --service baidu-netdisk", "bun scripts/cli.ts deploy apply --env prod --service baidu-netdisk",
"bun scripts/cli.ts deploy apply --env prod --service frontend", "bun scripts/cli.ts deploy apply --env prod --service frontend",
"bun scripts/cli.ts deploy apply --env prod --service decision-center", "bun scripts/cli.ts deploy apply --env prod --service decision-center",
"bun scripts/cli.ts deploy apply --env prod --service project-manager",
"bun scripts/cli.ts deploy apply --env prod --service oa-event-flow",
"bun scripts/cli.ts deploy apply --env prod --service code-queue-mgr --dry-run",
], ],
devCommands: [ devCommands: [
"bun scripts/cli.ts deploy apply --env dev --service frontend", "bun scripts/cli.ts deploy apply --env dev --service frontend",
"bun scripts/cli.ts deploy apply --env dev --service decision-center", "bun scripts/cli.ts deploy apply --env dev --service decision-center",
"bun scripts/cli.ts deploy apply --env dev --service project-manager --dry-run",
"bun scripts/cli.ts deploy apply --env dev --service oa-event-flow --dry-run",
"bun scripts/cli.ts deploy apply --env dev --service code-queue-mgr --dry-run",
"bun scripts/cli.ts deploy apply --env dev --service todo-note --dry-run",
], ],
rollbackShape: "rerun the same artifact consumer with a previous commit-pinned image", rollbackShape: "rerun the same artifact consumer with a previous commit-pinned image",
}, },
+14 -3
View File
@@ -29,6 +29,14 @@ const ciRuntimeImages = [
"alpine/git:2.45.2", "alpine/git:2.45.2",
ciCodeQueueImage, ciCodeQueueImage,
]; ];
const publishUserServiceArtifactAllowedServiceIds = new Set([
"baidu-netdisk",
"code-queue-mgr",
"decision-center",
"frontend",
"oa-event-flow",
"project-manager",
]);
interface CiOptions { interface CiOptions {
repoUrl: string; repoUrl: string;
@@ -213,7 +221,10 @@ function requireSupportedUserService(config: UniDeskConfig, serviceId: string):
const isMainServerDirectService = service.providerId === "main-server" const isMainServerDirectService = service.providerId === "main-server"
&& service.development.providerId === "main-server" && service.development.providerId === "main-server"
&& service.deployment.mode === "unidesk-direct"; && service.deployment.mode === "unidesk-direct";
if (!isD601K3sService && !isMainServerDirectService) { const isMainServerInternalSidecar = service.providerId === "main-server"
&& service.development.providerId === "main-server"
&& service.deployment.mode === "internal-sidecar";
if (!isD601K3sService && !isMainServerDirectService && !isMainServerInternalSidecar) {
throw new Error(`ci publish-user-service supports only reviewed k3sctl-managed D601 services or main-server unidesk-direct services; ${serviceId} is ${service.providerId}/${service.deployment.mode}`); throw new Error(`ci publish-user-service supports only reviewed k3sctl-managed D601 services or main-server unidesk-direct services; ${serviceId} is ${service.providerId}/${service.deployment.mode}`);
} }
return service; return service;
@@ -1539,8 +1550,8 @@ export async function runCiCommand(config: UniDeskConfig, args: string[]): Promi
const waitMs = numberOption(args, "--wait-ms", 0); const waitMs = numberOption(args, "--wait-ms", 0);
const dryRun = boolFlag(args, "--dry-run"); const dryRun = boolFlag(args, "--dry-run");
const dockerfile = requireRepoRelativePath(target.dockerfile, serviceId === "frontend" ? "frontend.dockerfile" : `microservices.${serviceId}.repository.dockerfile`); const dockerfile = requireRepoRelativePath(target.dockerfile, serviceId === "frontend" ? "frontend.dockerfile" : `microservices.${serviceId}.repository.dockerfile`);
if (!["baidu-netdisk", "decision-center", "frontend"].includes(serviceId)) { if (!publishUserServiceArtifactAllowedServiceIds.has(serviceId)) {
throw new Error("ci publish-user-service currently allows only baidu-netdisk, decision-center, and frontend until each user-service Dockerfile contract is reviewed"); throw new Error(`ci publish-user-service currently allows only ${Array.from(publishUserServiceArtifactAllowedServiceIds).join(", ")} until each user-service Dockerfile contract is reviewed`);
} }
return publishUserServiceArtifact(config, { return publishUserServiceArtifact(config, {
repoUrl: target.repoUrl, repoUrl: target.repoUrl,
+56 -13
View File
@@ -136,9 +136,13 @@ const nativeK3sCtrAddress = "/run/k3s/containerd/containerd.sock";
const unideskRepoUrl = "https://github.com/pikasTech/unidesk"; const unideskRepoUrl = "https://github.com/pikasTech/unidesk";
const d601MaintenanceDeployAllowedServiceIds = new Set<string>(["backend-core", "k3sctl-adapter", "code-queue"]); const d601MaintenanceDeployAllowedServiceIds = new Set<string>(["backend-core", "k3sctl-adapter", "code-queue"]);
const devApplySupportedServiceIds = new Set<string>(["backend-core"]); const devApplySupportedServiceIds = new Set<string>(["backend-core"]);
const devArtifactConsumerServiceIds = new Set<string>(["baidu-netdisk", "decision-center", "frontend"]); const devArtifactConsumerServiceIds = new Set<string>(["baidu-netdisk", "code-queue-mgr", "decision-center", "frontend", "oa-event-flow", "project-manager", "todo-note"]);
const prodArtifactConsumerServiceIds = new Set<string>(["backend-core", "baidu-netdisk", "decision-center", "frontend"]); const devArtifactConsumerProdDesiredFallbackServiceIds = new Set<string>(["code-queue-mgr", "oa-event-flow", "project-manager", "todo-note"]);
const prodForbiddenTargetSideBuildServiceIds = prodArtifactConsumerServiceIds; const prodArtifactConsumerServiceIds = new Set<string>(["backend-core", "baidu-netdisk", "code-queue-mgr", "decision-center", "frontend", "oa-event-flow", "project-manager", "todo-note"]);
const prodArtifactLiveApplyBlockedServiceIds = new Map<string, string>([
["code-queue-mgr", "code-queue-mgr is the main-server Code Queue control-plane sidecar; live production apply requires explicit supervisor confirmation."],
["todo-note", "todo-note source is external to this repository and the current checked-in contract cannot prove /api/health deploy.commit/deploy.requestedCommit support."],
]);
const deployEnvironmentTargets: Record<DeployEnvironment, DeployEnvironmentTarget> = { const deployEnvironmentTargets: Record<DeployEnvironment, DeployEnvironmentTarget> = {
dev: { dev: {
environment: "dev", environment: "dev",
@@ -201,7 +205,7 @@ export function deployHelp(action: string | undefined = undefined): Record<strin
}, },
options: [ 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: "--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 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: "--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, decision-center, project-manager, oa-event-flow, code-queue-mgr validation, while todo-note remains dry-run only. Prod apply uses the D601 registry artifact consumer for reviewed services; code-queue-mgr live apply is supervisor-gated and todo-note remains blocked." },
{ name: "--service <id>", description: "Limit reconcile to one service from the manifest." }, { 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: "--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." }, { name: "--dry-run", description: "Prepare and validate without mutating the target service." },
@@ -569,6 +573,11 @@ function readEnvironmentDeployManifest(environment: DeployEnvironment): { manife
}; };
} }
function readProdDeployManifestSnapshot(): DeployManifest {
const raw = runGitOrThrow(["show", `${deployEnvironmentTargets.prod.gitRef}:deploy.json`], repoRoot, `failed to read ${deployEnvironmentTargets.prod.gitRef}:deploy.json`).stdout;
return parseDeployManifest(JSON.parse(raw) as unknown, `${deployEnvironmentTargets.prod.gitRef}:deploy.json#environments.prod`, "prod");
}
async function readDeployManifest(file: string): Promise<DeployManifest> { async function readDeployManifest(file: string): Promise<DeployManifest> {
const path = resolve(repoRoot, file); const path = resolve(repoRoot, file);
if (!existsSync(path)) throw new Error(`deploy manifest not found: ${path}`); if (!existsSync(path)) throw new Error(`deploy manifest not found: ${path}`);
@@ -2607,6 +2616,15 @@ function environmentDryRunPlan(
reason: "No standardized dev D601 registry artifact consumer is implemented for this service; legacy maintenance-channel deployment is not allowed.", reason: "No standardized dev D601 registry artifact consumer is implemented for this service; legacy maintenance-channel deployment is not allowed.",
} }
: undefined, : undefined,
liveApply: environment === "prod" && prodArtifactLiveApplyBlockedServiceIds.has(service.id)
? {
allowed: false,
reason: prodArtifactLiveApplyBlockedServiceIds.get(service.id),
dryRunOnly: true,
}
: environment === "prod"
? { allowed: prodArtifactConsumerServiceIds.has(service.id) }
: undefined,
})), })),
unsupported: environment === "prod" ? prodArtifactUnsupported : devUnsupported, unsupported: environment === "prod" ? prodArtifactUnsupported : devUnsupported,
}; };
@@ -2615,6 +2633,7 @@ function environmentDryRunPlan(
function unsupportedDevApplyServices(manifest: DeployManifest, serviceId: string | null): string[] { function unsupportedDevApplyServices(manifest: DeployManifest, serviceId: string | null): string[] {
if (manifest.environment !== "dev") return []; if (manifest.environment !== "dev") return [];
const services = serviceId === null ? manifest.services : manifest.services.filter((service) => service.id === serviceId); const services = serviceId === null ? manifest.services : manifest.services.filter((service) => service.id === serviceId);
if (serviceId !== null && services.length === 0 && devArtifactConsumerProdDesiredFallbackServiceIds.has(serviceId)) return [];
return services.map((service) => service.id).filter((id) => !devApplySupportedServiceIds.has(id) && !devArtifactConsumerServiceIds.has(id)); return services.map((service) => service.id).filter((id) => !devApplySupportedServiceIds.has(id) && !devArtifactConsumerServiceIds.has(id));
} }
@@ -2636,6 +2655,7 @@ function selectedDevArtifactServices(manifest: DeployManifest, serviceId: string
function selectedDevTargetServices(manifest: DeployManifest, serviceId: string | null): DeployManifestService[] { function selectedDevTargetServices(manifest: DeployManifest, serviceId: string | null): DeployManifestService[] {
if (manifest.environment !== "dev") return []; if (manifest.environment !== "dev") return [];
if (serviceId !== null && devArtifactConsumerProdDesiredFallbackServiceIds.has(serviceId) && !manifest.services.some((service) => service.id === serviceId)) return [];
return selectedEnvironmentServices(manifest, serviceId) return selectedEnvironmentServices(manifest, serviceId)
.filter((service) => devApplySupportedServiceIds.has(service.id) && !devArtifactConsumerServiceIds.has(service.id)); .filter((service) => devApplySupportedServiceIds.has(service.id) && !devArtifactConsumerServiceIds.has(service.id));
} }
@@ -2646,6 +2666,14 @@ function selectedEnvironmentServices(manifest: DeployManifest, serviceId: string
return services; return services;
} }
function selectedDevArtifactServicesWithProdFallback(manifest: DeployManifest, serviceId: string | null): DeployManifestService[] {
if (manifest.environment !== "dev" || serviceId === null) return selectedDevArtifactServices(manifest, serviceId);
const selected = manifest.services.filter((service) => service.id === serviceId && devArtifactConsumerServiceIds.has(service.id));
if (selected.length > 0 || !devArtifactConsumerProdDesiredFallbackServiceIds.has(serviceId)) return selected;
const prodManifest = readProdDeployManifestSnapshot();
return prodManifest.services.filter((service) => service.id === serviceId);
}
function devUnsupportedServices(manifest: DeployManifest, serviceId: string | null): DeployManifestService[] { function devUnsupportedServices(manifest: DeployManifest, serviceId: string | null): DeployManifestService[] {
if (manifest.environment !== "dev") return []; if (manifest.environment !== "dev") return [];
return selectedEnvironmentServices(manifest, serviceId).filter((service) => !devApplySupportedServiceIds.has(service.id) && !devArtifactConsumerServiceIds.has(service.id)); return selectedEnvironmentServices(manifest, serviceId).filter((service) => !devApplySupportedServiceIds.has(service.id) && !devArtifactConsumerServiceIds.has(service.id));
@@ -2672,20 +2700,21 @@ function prodArtifactUnsupportedResult(services: DeployManifestService[]): Recor
}; };
} }
function prodArtifactConsumerLocalManifestResult(services: DeployManifestService[]): Record<string, unknown> { function prodArtifactLiveApplyBlockedResult(services: DeployManifestService[]): Record<string, unknown> {
return { return {
ok: false, ok: false,
supported: false, supported: true,
error: "prod-artifact-consumer-local-manifest-blocked", error: "live-prod-apply-blocked",
services: services.map((service) => ({ services: services.map((service) => ({
id: service.id, id: service.id,
repo: service.repo, repo: service.repo,
commitId: service.commitId, commitId: service.commitId,
supported: true, supported: true,
reason: "This service has a reviewed artifact consumer; production source-build deploy from a local manifest is blocked.", liveApplyAllowed: false,
reason: prodArtifactLiveApplyBlockedServiceIds.get(service.id) ?? "live production artifact apply is blocked by policy",
})), })),
policy: "prod artifact consumers must enter through deploy apply --env prod so CD consumes an existing commit-pinned registry image and never falls back to source build or a dirty worktree", policy: "prod dry-run/plan is available, but worker automation must not run live production apply for these services",
commandShape: "bun scripts/cli.ts deploy apply --env prod --service <service-id> --commit <full-sha>", dryRunCommandShape: "bun scripts/cli.ts deploy apply --env prod --service <id> --dry-run",
}; };
} }
@@ -2705,7 +2734,7 @@ async function runArtifactConsumerApplyNow(
"--commit", commit, "--commit", commit,
"--source-repo", service.repo, "--source-repo", service.repo,
"--deploy-ref", `${deployEnvironmentTargets[environment].gitRef}:deploy.json#environments.${environment}.services.${service.id}`, "--deploy-ref", `${deployEnvironmentTargets[environment].gitRef}:deploy.json#environments.${environment}.services.${service.id}`,
...(environment === "prod" || service.id === "frontend" || service.id === "decision-center" ? ["--env", environment] : []), "--env", environment,
"--timeout-ms", String(options.timeoutMs), "--timeout-ms", String(options.timeoutMs),
"--run-now", "--run-now",
...(options.dryRun ? ["--dry-run"] : []), ...(options.dryRun ? ["--dry-run"] : []),
@@ -2731,6 +2760,10 @@ async function runProdArtifactApplyNow(manifest: DeployManifest, options: Deploy
const selected = selectedEnvironmentServices(manifest, options.serviceId); const selected = selectedEnvironmentServices(manifest, options.serviceId);
const unsupported = selected.filter((service) => !prodArtifactConsumerServiceIds.has(service.id)); const unsupported = selected.filter((service) => !prodArtifactConsumerServiceIds.has(service.id));
if (unsupported.length > 0) return prodArtifactUnsupportedResult(unsupported); if (unsupported.length > 0) return prodArtifactUnsupportedResult(unsupported);
if (!options.dryRun) {
const blocked = selected.filter((service) => prodArtifactLiveApplyBlockedServiceIds.has(service.id));
if (blocked.length > 0) return prodArtifactLiveApplyBlockedResult(blocked);
}
return runArtifactConsumerApplyNow(manifest, options, "prod", selected); return runArtifactConsumerApplyNow(manifest, options, "prod", selected);
} }
@@ -2739,6 +2772,16 @@ function prodArtifactApplyJob(args: string[], options: DeployOptions): Record<st
throw new Error("deploy apply --env prod --dry-run should run in the foreground so the plan is visible immediately"); throw new Error("deploy apply --env prod --dry-run should run in the foreground so the plan is visible immediately");
} }
const runArgs = args.includes("--run-now") ? args : [...args, "--run-now"]; const runArgs = args.includes("--run-now") ? args : [...args, "--run-now"];
if (options.serviceId === null) {
throw new Error("deploy apply --env prod without --service is not allowed for mixed artifact consumers; use --service to avoid applying supervisor-gated services accidentally");
}
if (prodArtifactLiveApplyBlockedServiceIds.has(options.serviceId)) {
return prodArtifactLiveApplyBlockedResult([{
id: options.serviceId,
repo: "",
commitId: options.commitOverride ?? "",
}]);
}
const command = [process.execPath, rootPath("scripts", "cli.ts"), "deploy", ...runArgs]; const command = [process.execPath, rootPath("scripts", "cli.ts"), "deploy", ...runArgs];
const source = `${deployEnvironmentTargets.prod.gitRef}:deploy.json#environments.prod`; const source = `${deployEnvironmentTargets.prod.gitRef}:deploy.json#environments.prod`;
const job = startJob("deploy_prod_artifact_apply", command, `Deploy prod artifact consumer from ${source}${options.serviceId === null ? "" : ` service=${options.serviceId}`}`); const job = startJob("deploy_prod_artifact_apply", command, `Deploy prod artifact consumer from ${source}${options.serviceId === null ? "" : ` service=${options.serviceId}`}`);
@@ -2826,9 +2869,9 @@ export async function runDeployCommand(config: UniDeskConfig | null, args: strin
} }
const unsupported = unsupportedDevApplyServices(manifest, options.serviceId); const unsupported = unsupportedDevApplyServices(manifest, options.serviceId);
if (unsupported.length > 0) { if (unsupported.length > 0) {
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.`); throw new Error(`deploy apply --env dev currently supports backend-core target-side rollout plus frontend/baidu-netdisk/decision-center/project-manager/oa-event-flow/code-queue-mgr artifact consumers; unsupported selected services: ${unsupported.join(", ")}. todo-note is dry-run only until runtime verification is proven. Use ci run-dev-e2e for smoke verification.`);
} }
const devArtifactServices = selectedDevArtifactServices(manifest, options.serviceId); const devArtifactServices = selectedDevArtifactServicesWithProdFallback(manifest, options.serviceId);
const devTargetServices = selectedDevTargetServices(manifest, options.serviceId); const devTargetServices = selectedDevTargetServices(manifest, options.serviceId);
if (devArtifactServices.length > 0 && devTargetServices.length > 0) { if (devArtifactServices.length > 0 && devTargetServices.length > 0) {
throw new Error("deploy apply --env dev cannot mix artifact consumer services with target-side rollout services in one invocation; pass --service"); throw new Error("deploy apply --env dev cannot mix artifact consumer services with target-side rollout services in one invocation; pass --service");
+20
View File
@@ -116,6 +116,26 @@ export function writeComposeEnv(config: UniDeskConfig, freshLogPrefix: boolean):
UNIDESK_FRONTEND_DEPLOY_REPO: runtimeSecret("UNIDESK_FRONTEND_DEPLOY_REPO"), UNIDESK_FRONTEND_DEPLOY_REPO: runtimeSecret("UNIDESK_FRONTEND_DEPLOY_REPO"),
UNIDESK_FRONTEND_DEPLOY_COMMIT: runtimeSecret("UNIDESK_FRONTEND_DEPLOY_COMMIT"), UNIDESK_FRONTEND_DEPLOY_COMMIT: runtimeSecret("UNIDESK_FRONTEND_DEPLOY_COMMIT"),
UNIDESK_FRONTEND_DEPLOY_REQUESTED_COMMIT: runtimeSecret("UNIDESK_FRONTEND_DEPLOY_REQUESTED_COMMIT"), UNIDESK_FRONTEND_DEPLOY_REQUESTED_COMMIT: runtimeSecret("UNIDESK_FRONTEND_DEPLOY_REQUESTED_COMMIT"),
UNIDESK_PROJECT_MANAGER_DEPLOY_REF: runtimeSecret("UNIDESK_PROJECT_MANAGER_DEPLOY_REF"),
UNIDESK_PROJECT_MANAGER_DEPLOY_SERVICE_ID: runtimeSecret("UNIDESK_PROJECT_MANAGER_DEPLOY_SERVICE_ID") || "project-manager",
UNIDESK_PROJECT_MANAGER_DEPLOY_REPO: runtimeSecret("UNIDESK_PROJECT_MANAGER_DEPLOY_REPO"),
UNIDESK_PROJECT_MANAGER_DEPLOY_COMMIT: runtimeSecret("UNIDESK_PROJECT_MANAGER_DEPLOY_COMMIT"),
UNIDESK_PROJECT_MANAGER_DEPLOY_REQUESTED_COMMIT: runtimeSecret("UNIDESK_PROJECT_MANAGER_DEPLOY_REQUESTED_COMMIT"),
UNIDESK_OA_EVENT_FLOW_DEPLOY_REF: runtimeSecret("UNIDESK_OA_EVENT_FLOW_DEPLOY_REF"),
UNIDESK_OA_EVENT_FLOW_DEPLOY_SERVICE_ID: runtimeSecret("UNIDESK_OA_EVENT_FLOW_DEPLOY_SERVICE_ID") || "oa-event-flow",
UNIDESK_OA_EVENT_FLOW_DEPLOY_REPO: runtimeSecret("UNIDESK_OA_EVENT_FLOW_DEPLOY_REPO"),
UNIDESK_OA_EVENT_FLOW_DEPLOY_COMMIT: runtimeSecret("UNIDESK_OA_EVENT_FLOW_DEPLOY_COMMIT"),
UNIDESK_OA_EVENT_FLOW_DEPLOY_REQUESTED_COMMIT: runtimeSecret("UNIDESK_OA_EVENT_FLOW_DEPLOY_REQUESTED_COMMIT"),
UNIDESK_CODE_QUEUE_MGR_DEPLOY_REF: runtimeSecret("UNIDESK_CODE_QUEUE_MGR_DEPLOY_REF"),
UNIDESK_CODE_QUEUE_MGR_DEPLOY_SERVICE_ID: runtimeSecret("UNIDESK_CODE_QUEUE_MGR_DEPLOY_SERVICE_ID") || "code-queue-mgr",
UNIDESK_CODE_QUEUE_MGR_DEPLOY_REPO: runtimeSecret("UNIDESK_CODE_QUEUE_MGR_DEPLOY_REPO"),
UNIDESK_CODE_QUEUE_MGR_DEPLOY_COMMIT: runtimeSecret("UNIDESK_CODE_QUEUE_MGR_DEPLOY_COMMIT"),
UNIDESK_CODE_QUEUE_MGR_DEPLOY_REQUESTED_COMMIT: runtimeSecret("UNIDESK_CODE_QUEUE_MGR_DEPLOY_REQUESTED_COMMIT"),
UNIDESK_TODO_NOTE_DEPLOY_REF: runtimeSecret("UNIDESK_TODO_NOTE_DEPLOY_REF"),
UNIDESK_TODO_NOTE_DEPLOY_SERVICE_ID: runtimeSecret("UNIDESK_TODO_NOTE_DEPLOY_SERVICE_ID") || "todo-note",
UNIDESK_TODO_NOTE_DEPLOY_REPO: runtimeSecret("UNIDESK_TODO_NOTE_DEPLOY_REPO"),
UNIDESK_TODO_NOTE_DEPLOY_COMMIT: runtimeSecret("UNIDESK_TODO_NOTE_DEPLOY_COMMIT"),
UNIDESK_TODO_NOTE_DEPLOY_REQUESTED_COMMIT: runtimeSecret("UNIDESK_TODO_NOTE_DEPLOY_REQUESTED_COMMIT"),
UNIDESK_AUTH_USERNAME: config.auth.username, UNIDESK_AUTH_USERNAME: config.auth.username,
UNIDESK_AUTH_PASSWORD: config.auth.password, UNIDESK_AUTH_PASSWORD: config.auth.password,
UNIDESK_SESSION_SECRET: config.auth.sessionSecret, UNIDESK_SESSION_SECRET: config.auth.sessionSecret,
+2 -2
View File
@@ -270,8 +270,8 @@ function artifactRegistryHelp(): unknown {
"registry endpoint is D601 loopback 127.0.0.1:5000 only", "registry endpoint is D601 loopback 127.0.0.1:5000 only",
"service is host-managed by systemd + Docker Compose, not k3s-managed", "service is host-managed by systemd + Docker Compose, not k3s-managed",
"install writes the rendered host unit/config and starts the registry", "install writes the rendered host unit/config and starts the registry",
"deploy-backend-core is a deprecated compatibility name; use deploy apply --env prod --service backend-core so standard artifact-consumer guardrails run first", "deploy-backend-core only pulls commit-pinned backend-core artifacts and does not build backend-core on the master server",
"deploy-service currently supports backend-core, baidu-netdisk, prod/dev frontend, and decision-center as standardized consumers", "deploy-service currently supports backend-core, baidu-netdisk, prod/dev frontend, decision-center, project-manager, oa-event-flow, and code-queue-mgr as standardized consumers; todo-note is runtime-verification blocked",
"status and health use provider-gateway Host SSH readonly checks", "status and health use provider-gateway Host SSH readonly checks",
], ],
legacyEntrypoints: { legacyEntrypoints: {
@@ -2681,6 +2681,9 @@ fn main() {
if env::args().any(|arg| arg == "--healthcheck") { if env::args().any(|arg| arg == "--healthcheck") {
std::process::exit(if run_healthcheck(config.port) { 0 } else { 1 }); std::process::exit(if run_healthcheck(config.port) { 0 } else { 1 });
} }
if env::args().any(|arg| arg == "--print-health") {
std::process::exit(if print_health(config.port) { 0 } else { 1 });
}
let state = AppState { let state = AppState {
config, config,
started_at: now_iso(), started_at: now_iso(),
@@ -2726,6 +2729,30 @@ fn run_healthcheck(port: u16) -> bool {
stream.read_to_string(&mut body).is_ok() && body.contains("\"ok\": true") stream.read_to_string(&mut body).is_ok() && body.contains("\"ok\": true")
} }
fn print_health(port: u16) -> bool {
let Ok(mut stream) = TcpStream::connect(("127.0.0.1", port)) else {
return false;
};
let _ = stream.set_read_timeout(Some(std::time::Duration::from_secs(3)));
let _ = stream.set_write_timeout(Some(std::time::Duration::from_secs(3)));
if stream
.write_all(b"GET /health HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n")
.is_err()
{
return false;
}
let mut response = String::new();
if stream.read_to_string(&mut response).is_err() {
return false;
}
if let Some((_, body)) = response.split_once("\r\n\r\n") {
println!("{}", body.trim());
} else {
println!("{}", response.trim());
}
response.contains("\"ok\": true")
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -16,6 +16,11 @@ interface RuntimeConfig {
pipelineBridgeBaseUrl: string; pipelineBridgeBaseUrl: string;
pipelineBridgeIntervalMs: number; pipelineBridgeIntervalMs: number;
pipelineBridgeRunLimit: number; pipelineBridgeRunLimit: number;
deployServiceId: string;
deployRef: string;
deployRepo: string;
deployCommit: string;
deployRequestedCommit: string;
} }
interface OaEventInput { interface OaEventInput {
@@ -184,6 +189,11 @@ function configFromEnv(): RuntimeConfig {
pipelineBridgeBaseUrl: envString("PIPELINE_OA_BRIDGE_BASE_URL", "").replace(/\/+$/u, ""), pipelineBridgeBaseUrl: envString("PIPELINE_OA_BRIDGE_BASE_URL", "").replace(/\/+$/u, ""),
pipelineBridgeIntervalMs: envNumber("PIPELINE_OA_BRIDGE_INTERVAL_MS", 15_000), pipelineBridgeIntervalMs: envNumber("PIPELINE_OA_BRIDGE_INTERVAL_MS", 15_000),
pipelineBridgeRunLimit: envNumber("PIPELINE_OA_BRIDGE_RUN_LIMIT", 50), pipelineBridgeRunLimit: envNumber("PIPELINE_OA_BRIDGE_RUN_LIMIT", 50),
deployServiceId: envString("UNIDESK_DEPLOY_SERVICE_ID", "oa-event-flow"),
deployRef: envString("UNIDESK_DEPLOY_REF", ""),
deployRepo: envString("UNIDESK_DEPLOY_REPO", ""),
deployCommit: envString("UNIDESK_DEPLOY_COMMIT", ""),
deployRequestedCommit: envString("UNIDESK_DEPLOY_REQUESTED_COMMIT", ""),
}; };
} }
@@ -1328,6 +1338,13 @@ async function healthRoute(): Promise<Response> {
insertedCount: pipelineBridgeState.insertedCount, insertedCount: pipelineBridgeState.insertedCount,
duplicateCount: pipelineBridgeState.duplicateCount, duplicateCount: pipelineBridgeState.duplicateCount,
}, },
deploy: {
serviceId: config.deployServiceId,
ref: config.deployRef,
repo: config.deployRepo,
commit: config.deployCommit,
requestedCommit: config.deployRequestedCommit,
},
}, databaseReady ? 200 : 503); }, databaseReady ? 200 : 503);
} }
@@ -14,6 +14,11 @@ interface RuntimeConfig {
databaseUrl: string; databaseUrl: string;
logFile: string; logFile: string;
databasePoolMax: number; databasePoolMax: number;
deployServiceId: string;
deployRef: string;
deployRepo: string;
deployCommit: string;
deployRequestedCommit: string;
} }
interface ProjectRow { interface ProjectRow {
@@ -92,6 +97,11 @@ function configFromEnv(): RuntimeConfig {
databaseUrl, databaseUrl,
logFile: process.env.LOG_FILE || "", logFile: process.env.LOG_FILE || "",
databasePoolMax: Math.max(1, Math.min(8, Number(process.env.DATABASE_POOL_MAX || 1) || 1)), databasePoolMax: Math.max(1, Math.min(8, Number(process.env.DATABASE_POOL_MAX || 1) || 1)),
deployServiceId: process.env.UNIDESK_DEPLOY_SERVICE_ID || "project-manager",
deployRef: process.env.UNIDESK_DEPLOY_REF || "",
deployRepo: process.env.UNIDESK_DEPLOY_REPO || "",
deployCommit: process.env.UNIDESK_DEPLOY_COMMIT || "",
deployRequestedCommit: process.env.UNIDESK_DEPLOY_REQUESTED_COMMIT || "",
}; };
} }
@@ -569,6 +579,13 @@ async function health(): Promise<Response> {
service: "project-manager", service: "project-manager",
storage: { primary: "postgres", table: "project_manager_projects", projects: Number(rows[0]?.count ?? 0) }, storage: { primary: "postgres", table: "project_manager_projects", projects: Number(rows[0]?.count ?? 0) },
capabilities: ["crud", "excel-import", "excel-export"], capabilities: ["crud", "excel-import", "excel-export"],
deploy: {
serviceId: config.deployServiceId,
ref: config.deployRef,
repo: config.deployRepo,
commit: config.deployCommit,
requestedCommit: config.deployRequestedCommit,
},
startedAt: serviceStartedAt, startedAt: serviceStartedAt,
}); });
} }