diff --git a/AGENTS.md b/AGENTS.md index 4d6bac80..557f353a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,7 +38,7 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文 - `bun scripts/cli.ts microservice list/status/health/diagnostics/tunnel-self-test/proxy`:管理和验证挂载在主 server、计算节点 Docker 或 k3s 控制面上的用户服务,`proxy` 支持受控 JSON body,OA 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 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 ]`:按根目录 `deploy.json` 或 `origin/master:deploy.json#environments.` 的服务 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 ]`:按根目录 `deploy.json` 或 `origin/master:deploy.json#environments.` 的服务 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 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`。 diff --git a/CI.json b/CI.json index 64460183..5719ea81 100644 --- a/CI.json +++ b/CI.json @@ -127,6 +127,33 @@ "imageRef": "127.0.0.1:5000/unidesk/frontend:{{sourceCommit}}", "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 " + }, + { + "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 " + }, + { + "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 " + }, { "serviceId": "backend-core", "sourceRepo": "https://github.com/pikasTech/unidesk", diff --git a/deploy.json b/deploy.json index aeb1bf65..bf2775b8 100644 --- a/deploy.json +++ b/deploy.json @@ -107,6 +107,26 @@ "repo": "https://github.com/pikasTech/unidesk", "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", "repo": "https://github.com/pikasTech/unidesk", diff --git a/docker-compose.yml b/docker-compose.yml index 37235639..23ab6994 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -108,6 +108,11 @@ services: 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" 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: - ${UNIDESK_LOG_DIR}:/var/log/unidesk healthcheck: @@ -144,6 +149,11 @@ services: 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_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: - ${UNIDESK_LOG_DIR}:/var/log/unidesk healthcheck: @@ -176,6 +186,11 @@ services: 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" 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: - ${UNIDESK_LOG_DIR}:/var/log/unidesk healthcheck: @@ -202,6 +217,11 @@ services: DATABASE_POOL_MAX: "${UNIDESK_PROJECT_MANAGER_DATABASE_POOL_MAX:-1}" LOG_FILE: "/var/log/unidesk/${UNIDESK_LOG_DAY}/${UNIDESK_LOG_PREFIX}_project-manager.jsonl" 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: - ${UNIDESK_LOG_DIR}:/var/log/unidesk healthcheck: diff --git a/docs/reference/artifact-registry.md b/docs/reference/artifact-registry.md index ed1e774d..b8855e20 100644 --- a/docs/reference/artifact-registry.md +++ b/docs/reference/artifact-registry.md @@ -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 `;k3s runtime 设置 Deployment image/env/annotations 并等待 rollout。 7. 部署后通过 image label、runtime env、health payload 验证 live commit。 -Baidu Netdisk is the first main-server direct user-service sample in this flow. Its dev validation command and prod CD command both consume `127.0.0.1:5000/unidesk/baidu-netdisk:` 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:`, 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:` 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:`, 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 路径必须满足: diff --git a/docs/reference/deploy.md b/docs/reference/deploy.md index 84bb41cc..936728d7 100644 --- a/docs/reference/deploy.md +++ b/docs/reference/deploy.md @@ -40,7 +40,7 @@ The root `deploy.json` is the single desired-state source for both prod and dev. The optional non-service execution declaration under `environments.dev` is intentionally not specified here. The only currently allowed declaration is `ci`, and its authoritative `repo`, `scriptPath`, `timeoutMs`, short launcher, host fetch boundary and no-CD rules are defined only in `docs/reference/dev-ci-runner.md`. -Environment mode never reads the local dirty working tree manifest. `deploy check --env ...`, `deploy plan --env ...` and `deploy apply --env ...` fetch `origin/master`, read `origin/master:deploy.json`, select `environments.`, 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.`, 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 --commit `. 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. -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:` 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` - `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 ]` 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 ] [--commit ] [--dry-run] [--force]` starts an asynchronous job only for supported targets. Use `bun scripts/cli.ts job status --tail-bytes 30000` to observe progress. `--dry-run` resolves the same plan but does not build or replace runtime objects. `--force` rebuilds even when the live commit matches. Environment apply is not the dev e2e trigger; use `bun scripts/cli.ts ci run-dev-e2e` for the Git-controlled temporary namespace smoke flow. `--env dev` apply is enabled for persistent D601 `backend-core` target-side rollout and for `frontend`/`baidu-netdisk`/`decision-center` 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 ] [--commit ] [--dry-run] [--force]` starts an asynchronous job only for supported targets. Use `bun scripts/cli.ts job status --tail-bytes 30000` to observe progress. `--dry-run` resolves the same plan but does not build or replace runtime objects. `--force` rebuilds even when the live commit matches. Environment apply is not the dev e2e trigger; use `bun scripts/cli.ts ci run-dev-e2e` for the Git-controlled temporary namespace smoke flow. `--env dev` apply is enabled for persistent D601 `backend-core` target-side rollout and for `frontend`/`baidu-netdisk`/`decision-center`/`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. @@ -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`: -- `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 `. 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=internal-sidecar` on `main-server`: use the same main-server target-side source export, Docker build, image label stamping, fixed Compose project replacement and live commit verification as direct Compose services. This class is for private sidecars such as `code-queue-mgr`; it is still versioned by `deploy.json.commitId`, not by the operator's current worktree. +- `deployment.mode=unidesk-direct` on `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 `. 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, 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. - 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. diff --git a/docs/reference/deployment.md b/docs/reference/deployment.md index eef9a0c3..5a0d9690 100644 --- a/docs/reference/deployment.md +++ b/docs/reference/deployment.md @@ -30,7 +30,7 @@ CLI 会优先使用 `docker compose` v2 plugin;当 v2 plugin 不存在时才 Compose v2 安装后仍然必须遵守 UniDesk 的服务控制入口:全栈生命周期用 `server start` / `server stop`,单服务重建用 `server rebuild `。不要因为 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 consumer,dev 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 registry,dev/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 registry,dev/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 @@ -46,7 +46,7 @@ swap 管理不能被强塞进所有热路径。`server start/status` 可以暴 ## 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 `,其中 `` 只能是 `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 `、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 `,其中 `` 只能是 `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 `、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 `,再用 `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` 都不能作为标准版本发布证据。 diff --git a/docs/reference/microservices.md b/docs/reference/microservices.md index 08174ef4..927d0bad 100644 --- a/docs/reference/microservices.md +++ b/docs/reference/microservices.md @@ -57,7 +57,7 @@ UniDesk 用户服务是挂载到 UniDesk 核心服务上的、面向用户使用 - 代理路径:只允许 `/api/` 前缀;允许方法为 `GET`、`HEAD`、`POST`、`DELETE`,用于保持 Todo Note 原有清单创建/删除、任务增删改、提醒、展开/收起、移动、撤销/重做等功能。 - 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` 这一类可审计摘要,不能只依赖前端页面观察。 @@ -76,7 +76,7 @@ Todo Note 数据迁移后必须验证:`microservice proxy todo-note /api/insta - 代理路径:只允许 `/health`、`/logs` 和 `/api/` 前缀;允许方法为 `GET`、`HEAD`、`POST`。 - 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 `,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 @@ -85,6 +85,7 @@ OA Event Flow 在 UniDesk 语境中按共享控制面基础设施管理:不得 - 职责:队列 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。 - 资源边界:目标常驻内存不超过 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 ` 会在 D601 registry 发布 `127.0.0.1:5000/unidesk/code-queue-mgr:`;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,不包含 Bun;Docker 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 执行面。 - 行为兼容:提交与 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`。 - 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 `,随后用 `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 diff --git a/docs/reference/user-service-delivery.md b/docs/reference/user-service-delivery.md index a1e96329..ed9ff9be 100644 --- a/docs/reference/user-service-delivery.md +++ b/docs/reference/user-service-delivery.md @@ -15,6 +15,8 @@ This policy does not apply to: - `release/v1` governance; - 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 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. - 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 --wait-ms 1200000`. +- The expected artifact is `127.0.0.1:5000/unidesk/project-manager:` 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 --wait-ms 1200000`. +- The expected artifact is `127.0.0.1:5000/unidesk/oa-event-flow:` 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 --wait-ms 1200000`. +- The expected artifact is `127.0.0.1:5000/unidesk/code-queue-mgr:` 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 is the canonical example of a user service that doubles as a product workflow for requirements, decisions, and daily work diaries. diff --git a/scripts/src/artifact-registry.ts b/scripts/src/artifact-registry.ts index cb53d1d2..d79e091b 100644 --- a/scripts/src/artifact-registry.ts +++ b/scripts/src/artifact-registry.ts @@ -69,7 +69,7 @@ const defaultOptions: ArtifactRegistryOptions = { sourceRepo: "https://github.com/pikasTech/unidesk", 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]; const legacyDeployBackendCoreDisabled = true; @@ -80,6 +80,10 @@ interface ArtifactConsumerSpec { registryRepository: string; dockerfile: string; targets: Partial>; + prodLiveApply: "enabled" | "supervisor-only" | "unsupported"; + prodLiveBlockReason?: string; + runtimeVerification?: "strict" | "blocked"; + runtimeVerificationBlockReason?: string; } interface ArtifactConsumerTarget { @@ -113,6 +117,7 @@ const artifactConsumerSpecs: Record = { kind: "compose", registryRepository: "unidesk/backend-core", dockerfile: "src/components/backend-core/Dockerfile", + prodLiveApply: "enabled", targets: { prod: { targetImage: "unidesk-backend-core", @@ -134,7 +139,20 @@ const artifactConsumerSpecs: Record = { kind: "compose", registryRepository: "unidesk/baidu-netdisk", dockerfile: "src/components/microservices/baidu-netdisk/Dockerfile", + prodLiveApply: "enabled", 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: { targetImage: "baidu-netdisk", targetCommitImage: (commit: string) => `baidu-netdisk:${commit}`, @@ -149,12 +167,48 @@ const artifactConsumerSpecs: Record = { }, }, }, + "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": { serviceId: "decision-center", environment: "prod", kind: "d601-k3s", registryRepository: "unidesk/decision-center", dockerfile: "src/components/microservices/decision-center/Dockerfile", + prodLiveApply: "enabled", targets: { dev: { targetImage: "unidesk-decision-center:dev", @@ -193,6 +247,7 @@ const artifactConsumerSpecs: Record = { kind: "compose", registryRepository: "unidesk/frontend", dockerfile: "src/components/frontend/Dockerfile", + prodLiveApply: "enabled", targets: { prod: { targetImage: "unidesk-frontend", @@ -208,12 +263,118 @@ const artifactConsumerSpecs: Record = { }, }, }, + "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": { serviceId: "frontend", environment: "dev", kind: "d601-k3s", registryRepository: "unidesk/frontend", dockerfile: "src/components/frontend/Dockerfile", + prodLiveApply: "enabled", targets: { dev: { targetImage: "unidesk-frontend:dev", @@ -649,6 +810,58 @@ function unsupportedEnvironment(spec: ArtifactConsumerSpec, options: ArtifactReg }; } +function artifactConsumerLiveBlock(spec: ArtifactConsumerSpec, options: ArtifactRegistryOptions): Record | 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 --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 { return `127.0.0.1:${options.port}/${spec.registryRepository}:${commit}`; } @@ -1040,7 +1253,9 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec: "cat \"$health_json\"", ...(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_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_requested_commit" = ${shellQuote(commit)}`, ] : []), ].join("\n"); 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(), validation: { liveCommit: commit, + liveRequestedCommit: target.compose.requireHealthCommit ? commit : "not-required", imageLabelCommit: commit, serviceHealthCommit: target.compose.requireHealthCommit ? commit : "not-required", + serviceHealthRequestedCommit: target.compose.requireHealthCommit ? commit : "not-required", healthyOldVersionAccepted: false, }, rollback: { @@ -1125,12 +1342,14 @@ function legacyDeployBackendCoreResult(options: ArtifactRegistryOptions): Record function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, target: ArtifactConsumerTarget, commit: string): Record { const environment = options.environment ?? "prod"; + const verificationBlocked = spec.runtimeVerification === "blocked"; const sourceImage = artifactImageRef(options, spec, commit); const common = { - ok: true, - supported: true, + ok: !verificationBlocked, + supported: !verificationBlocked, dryRun: true, mutation: false, + error: verificationBlocked ? "runtime-verification-blocked" : undefined, environment, providerId: options.providerId, 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}`, }, 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 (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", "loaded image labels match service id, source commit, and Dockerfile", "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 for the recreated container`, ], @@ -1430,6 +1656,8 @@ async function deployServiceNow(options: ArtifactRegistryOptions): Promise { "bun scripts/cli.ts artifact-registry deploy-service --service frontend --env dev --commit [--dry-run] [--run-now] [--provider-id D601]", "bun scripts/cli.ts artifact-registry deploy-service --env dev --service decision-center --commit [--dry-run] [--run-now] [--provider-id D601]", "bun scripts/cli.ts artifact-registry deploy-service --env prod --service decision-center --commit [--dry-run] [--run-now] [--provider-id D601]", + "bun scripts/cli.ts artifact-registry deploy-service --env prod --service project-manager --commit [--dry-run] [--run-now] [--provider-id D601]", + "bun scripts/cli.ts artifact-registry deploy-service --env prod --service oa-event-flow --commit [--dry-run] [--run-now] [--provider-id D601]", + "bun scripts/cli.ts artifact-registry deploy-service --env prod --service code-queue-mgr --commit --dry-run [--provider-id D601]", + "bun scripts/cli.ts artifact-registry deploy-service --env prod --service todo-note --commit --dry-run [--provider-id D601]", ], firstStage: "install now writes the rendered systemd/Compose/config files and starts the registry", artifactConsumers: { @@ -1489,10 +1723,17 @@ function localHelp(): Record { "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 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: [ "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 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", }, diff --git a/scripts/src/ci.ts b/scripts/src/ci.ts index 7dcea0ea..58a81cb0 100644 --- a/scripts/src/ci.ts +++ b/scripts/src/ci.ts @@ -29,6 +29,14 @@ const ciRuntimeImages = [ "alpine/git:2.45.2", ciCodeQueueImage, ]; +const publishUserServiceArtifactAllowedServiceIds = new Set([ + "baidu-netdisk", + "code-queue-mgr", + "decision-center", + "frontend", + "oa-event-flow", + "project-manager", +]); interface CiOptions { repoUrl: string; @@ -213,7 +221,10 @@ function requireSupportedUserService(config: UniDeskConfig, serviceId: string): const isMainServerDirectService = service.providerId === "main-server" && service.development.providerId === "main-server" && 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}`); } return service; @@ -1539,8 +1550,8 @@ export async function runCiCommand(config: UniDeskConfig, args: string[]): Promi const waitMs = numberOption(args, "--wait-ms", 0); const dryRun = boolFlag(args, "--dry-run"); const dockerfile = requireRepoRelativePath(target.dockerfile, serviceId === "frontend" ? "frontend.dockerfile" : `microservices.${serviceId}.repository.dockerfile`); - if (!["baidu-netdisk", "decision-center", "frontend"].includes(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"); + if (!publishUserServiceArtifactAllowedServiceIds.has(serviceId)) { + 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, { repoUrl: target.repoUrl, diff --git a/scripts/src/deploy.ts b/scripts/src/deploy.ts index 77d15313..dd7caae9 100644 --- a/scripts/src/deploy.ts +++ b/scripts/src/deploy.ts @@ -136,9 +136,13 @@ const nativeK3sCtrAddress = "/run/k3s/containerd/containerd.sock"; const unideskRepoUrl = "https://github.com/pikasTech/unidesk"; const d601MaintenanceDeployAllowedServiceIds = new Set(["backend-core", "k3sctl-adapter", "code-queue"]); const devApplySupportedServiceIds = new Set(["backend-core"]); -const devArtifactConsumerServiceIds = new Set(["baidu-netdisk", "decision-center", "frontend"]); -const prodArtifactConsumerServiceIds = new Set(["backend-core", "baidu-netdisk", "decision-center", "frontend"]); -const prodForbiddenTargetSideBuildServiceIds = prodArtifactConsumerServiceIds; +const devArtifactConsumerServiceIds = new Set(["baidu-netdisk", "code-queue-mgr", "decision-center", "frontend", "oa-event-flow", "project-manager", "todo-note"]); +const devArtifactConsumerProdDesiredFallbackServiceIds = new Set(["code-queue-mgr", "oa-event-flow", "project-manager", "todo-note"]); +const prodArtifactConsumerServiceIds = new Set(["backend-core", "baidu-netdisk", "code-queue-mgr", "decision-center", "frontend", "oa-event-flow", "project-manager", "todo-note"]); +const prodArtifactLiveApplyBlockedServiceIds = new Map([ + ["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 = { dev: { environment: "dev", @@ -201,7 +205,7 @@ export function deployHelp(action: string | undefined = undefined): Record", 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 ", 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 ", 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 ", description: "Limit reconcile to one service from the manifest." }, { name: "--commit ", 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." }, @@ -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 { const path = resolve(repoRoot, file); 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.", } : 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, }; @@ -2615,6 +2633,7 @@ function environmentDryRunPlan( function unsupportedDevApplyServices(manifest: DeployManifest, serviceId: string | null): string[] { if (manifest.environment !== "dev") return []; 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)); } @@ -2636,6 +2655,7 @@ function selectedDevArtifactServices(manifest: DeployManifest, serviceId: string function selectedDevTargetServices(manifest: DeployManifest, serviceId: string | null): DeployManifestService[] { if (manifest.environment !== "dev") return []; + if (serviceId !== null && devArtifactConsumerProdDesiredFallbackServiceIds.has(serviceId) && !manifest.services.some((service) => service.id === serviceId)) return []; return selectedEnvironmentServices(manifest, serviceId) .filter((service) => devApplySupportedServiceIds.has(service.id) && !devArtifactConsumerServiceIds.has(service.id)); } @@ -2646,6 +2666,14 @@ function selectedEnvironmentServices(manifest: DeployManifest, serviceId: string 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[] { if (manifest.environment !== "dev") return []; 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 { +function prodArtifactLiveApplyBlockedResult(services: DeployManifestService[]): Record { return { ok: false, - supported: false, - error: "prod-artifact-consumer-local-manifest-blocked", + supported: true, + error: "live-prod-apply-blocked", services: services.map((service) => ({ id: service.id, repo: service.repo, commitId: service.commitId, 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", - commandShape: "bun scripts/cli.ts deploy apply --env prod --service --commit ", + policy: "prod dry-run/plan is available, but worker automation must not run live production apply for these services", + dryRunCommandShape: "bun scripts/cli.ts deploy apply --env prod --service --dry-run", }; } @@ -2705,7 +2734,7 @@ async function runArtifactConsumerApplyNow( "--commit", commit, "--source-repo", service.repo, "--deploy-ref", `${deployEnvironmentTargets[environment].gitRef}:deploy.json#environments.${environment}.services.${service.id}`, - ...(environment === "prod" || service.id === "frontend" || service.id === "decision-center" ? ["--env", environment] : []), + "--env", environment, "--timeout-ms", String(options.timeoutMs), "--run-now", ...(options.dryRun ? ["--dry-run"] : []), @@ -2731,6 +2760,10 @@ async function runProdArtifactApplyNow(manifest: DeployManifest, options: Deploy const selected = selectedEnvironmentServices(manifest, options.serviceId); const unsupported = selected.filter((service) => !prodArtifactConsumerServiceIds.has(service.id)); 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); } @@ -2739,6 +2772,16 @@ function prodArtifactApplyJob(args: string[], options: DeployOptions): Record 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); 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"); diff --git a/scripts/src/docker.ts b/scripts/src/docker.ts index f000e96a..d0e7c1b3 100644 --- a/scripts/src/docker.ts +++ b/scripts/src/docker.ts @@ -116,6 +116,26 @@ export function writeComposeEnv(config: UniDeskConfig, freshLogPrefix: boolean): UNIDESK_FRONTEND_DEPLOY_REPO: runtimeSecret("UNIDESK_FRONTEND_DEPLOY_REPO"), UNIDESK_FRONTEND_DEPLOY_COMMIT: runtimeSecret("UNIDESK_FRONTEND_DEPLOY_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_PASSWORD: config.auth.password, UNIDESK_SESSION_SECRET: config.auth.sessionSecret, diff --git a/scripts/src/help.ts b/scripts/src/help.ts index e23d64af..a04a631b 100644 --- a/scripts/src/help.ts +++ b/scripts/src/help.ts @@ -270,8 +270,8 @@ function artifactRegistryHelp(): unknown { "registry endpoint is D601 loopback 127.0.0.1:5000 only", "service is host-managed by systemd + Docker Compose, not k3s-managed", "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-service currently supports backend-core, baidu-netdisk, prod/dev frontend, and decision-center as standardized consumers", + "deploy-backend-core only pulls commit-pinned backend-core artifacts and does not build backend-core on the master server", + "deploy-service currently supports backend-core, baidu-netdisk, prod/dev frontend, decision-center, 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", ], legacyEntrypoints: { diff --git a/src/components/microservices/code-queue-mgr/src-rs/main.rs b/src/components/microservices/code-queue-mgr/src-rs/main.rs index 868416bb..4e781466 100644 --- a/src/components/microservices/code-queue-mgr/src-rs/main.rs +++ b/src/components/microservices/code-queue-mgr/src-rs/main.rs @@ -2681,6 +2681,9 @@ fn main() { if env::args().any(|arg| arg == "--healthcheck") { 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 { config, 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") } +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)] mod tests { use super::*; diff --git a/src/components/microservices/oa-event-flow/src/index.ts b/src/components/microservices/oa-event-flow/src/index.ts index 12d347df..10c4dbee 100644 --- a/src/components/microservices/oa-event-flow/src/index.ts +++ b/src/components/microservices/oa-event-flow/src/index.ts @@ -16,6 +16,11 @@ interface RuntimeConfig { pipelineBridgeBaseUrl: string; pipelineBridgeIntervalMs: number; pipelineBridgeRunLimit: number; + deployServiceId: string; + deployRef: string; + deployRepo: string; + deployCommit: string; + deployRequestedCommit: string; } interface OaEventInput { @@ -184,6 +189,11 @@ function configFromEnv(): RuntimeConfig { pipelineBridgeBaseUrl: envString("PIPELINE_OA_BRIDGE_BASE_URL", "").replace(/\/+$/u, ""), pipelineBridgeIntervalMs: envNumber("PIPELINE_OA_BRIDGE_INTERVAL_MS", 15_000), 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 { insertedCount: pipelineBridgeState.insertedCount, duplicateCount: pipelineBridgeState.duplicateCount, }, + deploy: { + serviceId: config.deployServiceId, + ref: config.deployRef, + repo: config.deployRepo, + commit: config.deployCommit, + requestedCommit: config.deployRequestedCommit, + }, }, databaseReady ? 200 : 503); } diff --git a/src/components/microservices/project-manager/src/index.ts b/src/components/microservices/project-manager/src/index.ts index 7b3b057c..716bd444 100644 --- a/src/components/microservices/project-manager/src/index.ts +++ b/src/components/microservices/project-manager/src/index.ts @@ -14,6 +14,11 @@ interface RuntimeConfig { databaseUrl: string; logFile: string; databasePoolMax: number; + deployServiceId: string; + deployRef: string; + deployRepo: string; + deployCommit: string; + deployRequestedCommit: string; } interface ProjectRow { @@ -92,6 +97,11 @@ function configFromEnv(): RuntimeConfig { databaseUrl, logFile: process.env.LOG_FILE || "", 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 { service: "project-manager", storage: { primary: "postgres", table: "project_manager_projects", projects: Number(rows[0]?.count ?? 0) }, capabilities: ["crud", "excel-import", "excel-export"], + deploy: { + serviceId: config.deployServiceId, + ref: config.deployRef, + repo: config.deployRepo, + commit: config.deployCommit, + requestedCommit: config.deployRequestedCommit, + }, startedAt: serviceStartedAt, }); }