fix: expose baidu netdisk secret source contract
This commit is contained in:
@@ -108,7 +108,7 @@ bun scripts/cli.ts artifact-registry deploy-service --env dev --service code-que
|
||||
|
||||
`code-queue-mgr` 是主 server Compose sidecar,不是 D601 Code Queue scheduler/runner。`artifact-registry deploy-service --env prod --service code-queue-mgr --commit <full-sha> --dry-run` 必须显示 target 仅为 `composeService=code-queue-mgr` / `containerName=code-queue-mgr-backend`,并在 `excludedTargets` 中说明不会触碰 `code-queue` scheduler、runner、任务、interrupt 或 cancel 状态。真实 prod apply 仍受 supervisor-only gate 保护;未经单服务授权不得执行非 dry-run apply。
|
||||
|
||||
dry-run 输出会暴露 registry probe URL、required labels、目标 image、部署形态、目标 Deployment 列表、回滚信息,以及结构化 `registry`、`source` 和 `build` 字段;`registry.digest` 在 dry-run 中为 `null`,`digestSource` 指明真实 digest 来自 live registry manifest HEAD,`build.willCompile`、`build.willRunCargoBuild`、`build.willRunDockerBuild` 和 `build.willRunDockerComposeBuild` 必须为 false。dry-run 不得读取或打印运行时密钥。`backend-core --env dev` 使用 `ci publish-backend-core` 产出的 `127.0.0.1:5000/unidesk/backend-core:<commit>`,导入 D601 native k3s containerd,更新 `unidesk-dev/backend-core-dev` Deployment,设置 image/env/annotations,并通过 Kubernetes API service proxy 验证 `/health.deploy.commit` 和 `deploy.requestedCommit`;CD 阶段不得运行 Rust 编译或 Docker build。`baidu-netdisk` 是 PGDATA 备份链路依赖服务;它的 Compose artifact 路径会通过 provider-gateway Host SSH 把 `unidesk/baidu-netdisk:<commit>` 流式拉到 master server,retag 为 `baidu-netdisk` 和 `baidu-netdisk:<commit>`,在 canonical UniDesk 根目录使用 `providerGateway.upgrade.composeEnvFile` 指向的受控 env 文件写入 `UNIDESK_BAIDU_NETDISK_DEPLOY_*`,只 recreate `baidu-netdisk` service,并验证容器 image label 与 `/health.deploy.commit`。live apply 在 recreate 前必须确认受控 env 文件中存在 `UNIDESK_BAIDU_NETDISK_CLIENT_ID`、`UNIDESK_BAIDU_NETDISK_CLIENT_SECRET` 和 `UNIDESK_BAIDU_NETDISK_TOKEN_KEY`,输出只能包含 present/length/boolean;recreate 后必须验收 `/health.auth.configured`、`clientIdConfigured`、`clientSecretConfigured`、`tokenKeyConfigured` 和 `loggedIn` 全部为 true,否则返回失败或 degraded,并提示先恢复 env、单服务 recreate、再验证 `microservice health baidu-netdisk`。`findjob`、`pipeline` 和 `met-nonlinear` 的 D601 direct Compose 路径在 D601 本机验证 registry manifest、pull image、retag stable image、写入 `UNIDESK_*_DEPLOY_*` labels/env,并用 `docker compose up -d --no-build --no-deps --force-recreate <service>` 重新拉起对应 compose service;其中 `met-nonlinear` 当前因为 registered Dockerfile 和 long-running service contract 不一致而 live deploy blocked。`k3sctl-adapter` 是基础设施控制桥,只做 plan/dry-run,真实生产部署需要 supervisor 单独确认。`frontend --env prod` 使用同一 Compose artifact consumer,流式拉取 `unidesk/frontend:<commit>`,retag 为 `unidesk-frontend` 和 `unidesk-frontend:<commit>`,写入 `UNIDESK_FRONTEND_DEPLOY_*`,只 recreate `frontend` service,并验证 image label 与 `/health.deploy.commit`。`frontend --env dev`、`decision-center`、`mdtodo`、`claudeqq` 和 dev `code-queue` 的 k3s 路径会在 D601 上验证 commit image、导入 native k3s containerd、更新 Deployment image/env/annotations,并通过 Kubernetes API service proxy 验证 `/health` 中的 live commit 与 requested commit;dev frontend 还会在 rollout 前把主 server `config.json.auth` 同步到 `unidesk-dev` Secret/ConfigMap。`decision-center --env dev` 落到 `unidesk-dev/decision-center-dev`,prod 落到 `unidesk/decision-center`;`mdtodo` 和 `claudeqq` 使用同样的 dev 后 prod k3s consumer 结构。`code-queue --env dev` 只更新 `unidesk-dev` 中的 scheduler/read/write/provider-egress-proxy dev Deployments,prod 没有 consumer target。D601 direct Compose consumer 与 k3s-managed consumer 的区别是:前者只接触 D601 Docker/Compose 项目和私有 backend health,不创建 Kubernetes 对象;后者只通过 native k3s Deployment/Service、containerd import 和 Kubernetes API service proxy 验证 live commit。回滚信息通过同一 artifact consumer 的 `rollback` 字段暴露,提示操作者重新对一个旧 commit 运行相同命令,而不是切回 legacy maintenance-channel 构建。
|
||||
dry-run 输出会暴露 registry probe URL、required labels、目标 image、部署形态、目标 Deployment 列表、回滚信息,以及结构化 `registry`、`source` 和 `build` 字段;`registry.digest` 在 dry-run 中为 `null`,`digestSource` 指明真实 digest 来自 live registry manifest HEAD,`build.willCompile`、`build.willRunCargoBuild`、`build.willRunDockerBuild` 和 `build.willRunDockerComposeBuild` 必须为 false。dry-run 不得打印运行时密钥。`backend-core --env dev` 使用 `ci publish-backend-core` 产出的 `127.0.0.1:5000/unidesk/backend-core:<commit>`,导入 D601 native k3s containerd,更新 `unidesk-dev/backend-core-dev` Deployment,设置 image/env/annotations,并通过 Kubernetes API service proxy 验证 `/health.deploy.commit` 和 `deploy.requestedCommit`;CD 阶段不得运行 Rust 编译或 Docker build。`baidu-netdisk` 是 PGDATA 备份链路依赖服务;它的 Compose artifact 路径会通过 provider-gateway Host SSH 把 `unidesk/baidu-netdisk:<commit>` 流式拉到 master server,retag 为 `baidu-netdisk` 和 `baidu-netdisk:<commit>`,在 canonical UniDesk 根目录使用 `providerGateway.upgrade.composeEnvFile` 指向的受控 env 文件写入 `UNIDESK_BAIDU_NETDISK_DEPLOY_*`,只 recreate `baidu-netdisk` service,并验证容器 image label 与 `/health.deploy.commit`。live apply 在 recreate 前必须确认受控 env 文件中存在 `UNIDESK_BAIDU_NETDISK_CLIENT_ID`、`UNIDESK_BAIDU_NETDISK_CLIENT_SECRET` 和 `UNIDESK_BAIDU_NETDISK_TOKEN_KEY`,输出只能包含 present/length/boolean;dry-run 输出 `runtimeSecrets.secretSource`、`requiredSecretsPresent`、`missingSecretKeys` 和 `recommendedAction`,让 dev secret source blocker 可诊断但不打印值;recreate 后必须验收 `/health.auth.configured`、`clientIdConfigured`、`clientSecretConfigured`、`tokenKeyConfigured` 和 `loggedIn` 全部为 true,否则返回失败或 degraded,并提示先恢复 env、单服务 recreate、再验证 `microservice health baidu-netdisk`。`findjob`、`pipeline` 和 `met-nonlinear` 的 D601 direct Compose 路径在 D601 本机验证 registry manifest、pull image、retag stable image、写入 `UNIDESK_*_DEPLOY_*` labels/env,并用 `docker compose up -d --no-build --no-deps --force-recreate <service>` 重新拉起对应 compose service;其中 `met-nonlinear` 当前因为 registered Dockerfile 和 long-running service contract 不一致而 live deploy blocked。`k3sctl-adapter` 是基础设施控制桥,只做 plan/dry-run,真实生产部署需要 supervisor 单独确认。`frontend --env prod` 使用同一 Compose artifact consumer,流式拉取 `unidesk/frontend:<commit>`,retag 为 `unidesk-frontend` 和 `unidesk-frontend:<commit>`,写入 `UNIDESK_FRONTEND_DEPLOY_*`,只 recreate `frontend` service,并验证 image label 与 `/health.deploy.commit`。`frontend --env dev`、`decision-center`、`mdtodo`、`claudeqq` 和 dev `code-queue` 的 k3s 路径会在 D601 上验证 commit image、导入 native k3s containerd、更新 Deployment image/env/annotations,并通过 Kubernetes API service proxy 验证 `/health` 中的 live commit 与 requested commit;dev frontend 还会在 rollout 前把主 server `config.json.auth` 同步到 `unidesk-dev` Secret/ConfigMap。`decision-center --env dev` 落到 `unidesk-dev/decision-center-dev`,prod 落到 `unidesk/decision-center`;`mdtodo` 和 `claudeqq` 使用同样的 dev 后 prod k3s consumer 结构。`code-queue --env dev` 只更新 `unidesk-dev` 中的 scheduler/read/write/provider-egress-proxy dev Deployments,prod 没有 consumer target。D601 direct Compose consumer 与 k3s-managed consumer 的区别是:前者只接触 D601 Docker/Compose 项目和私有 backend health,不创建 Kubernetes 对象;后者只通过 native k3s Deployment/Service、containerd import 和 Kubernetes API service proxy 验证 live commit。回滚信息通过同一 artifact consumer 的 `rollback` 字段暴露,提示操作者重新对一个旧 commit 运行相同命令,而不是切回 legacy maintenance-channel 构建。
|
||||
|
||||
`status` 和 `health` 通过:
|
||||
|
||||
@@ -165,7 +165,7 @@ Main-server direct production dry-runs for the user-service matrix must be enoug
|
||||
| `project-manager` | `ci publish-user-service`, UniDesk repo `src/components/microservices/project-manager/Dockerfile` | `127.0.0.1:5000/unidesk/project-manager:<commit>` | `project-manager` / `project-manager-backend` | `docker compose up -d --no-build --no-deps --force-recreate project-manager` | image labels plus `/health.deploy.commit` and `deploy.requestedCommit` |
|
||||
| `oa-event-flow` | `ci publish-user-service`, UniDesk repo `src/components/microservices/oa-event-flow/Dockerfile` | `127.0.0.1:5000/unidesk/oa-event-flow:<commit>` | `oa-event-flow` / `oa-event-flow-backend` | `docker compose up -d --no-build --no-deps --force-recreate oa-event-flow` | image labels plus `/health.deploy.commit` and `deploy.requestedCommit` |
|
||||
| `todo-note` | `ci publish-user-service`, external `https://gitee.com/Lyon1998/todo_note` `Dockerfile` | `127.0.0.1:5000/unidesk/todo-note:<commit>` | `todo-note` / `todo-note-backend` | `docker compose up -d --no-build --no-deps --force-recreate todo-note` | image labels plus synthetic health deploy metadata from `/api/health` and `UNIDESK_TODO_NOTE_DEPLOY_*` |
|
||||
| `baidu-netdisk` | `ci publish-user-service`, UniDesk repo `src/components/microservices/baidu-netdisk/Dockerfile` | `127.0.0.1:5000/unidesk/baidu-netdisk:<commit>` | `baidu-netdisk` / `baidu-netdisk-backend` | `docker compose up -d --no-build --no-deps --force-recreate baidu-netdisk` | image labels, `/health.deploy.commit`, `deploy.requestedCommit`, secret presence preflight and auth health gate on live apply |
|
||||
| `baidu-netdisk` | `ci publish-user-service`, UniDesk repo `src/components/microservices/baidu-netdisk/Dockerfile` | `127.0.0.1:5000/unidesk/baidu-netdisk:<commit>` | `baidu-netdisk` / `baidu-netdisk-backend` | `docker compose up -d --no-build --no-deps --force-recreate baidu-netdisk` | image labels, `/health.deploy.commit`, `deploy.requestedCommit`, redacted `runtimeSecrets` source contract, secret presence preflight and auth health gate on live apply |
|
||||
| `frontend` | `ci publish-user-service`, UniDesk repo `src/components/frontend/Dockerfile` | `127.0.0.1:5000/unidesk/frontend:<commit>` | `frontend` / `unidesk-frontend` | `docker compose up -d --no-build --no-deps --force-recreate frontend` | image labels plus `/health.deploy.commit` and `deploy.requestedCommit` |
|
||||
|
||||
The dry-run matrix intentionally excludes production backend-core and Code Queue execution-plane mutation. `code-queue-mgr` may be used as a read-only reference for the same dry-run output style, but its prod live apply remains supervisor-gated and its plan must state that scheduler, runner, queued task, interrupt and cancellation state are outside the target set.
|
||||
|
||||
@@ -70,7 +70,7 @@ Main-server Compose user services are normal source-build artifacts even though
|
||||
|
||||
| Service | Producer | Artifact | Consumer | Dev validation | Prod validation | Blocker |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| `baidu-netdisk` | `ci publish-user-service --service baidu-netdisk` from `src/components/microservices/baidu-netdisk/Dockerfile` | `127.0.0.1:5000/unidesk/baidu-netdisk:<commit>` | master-server Compose service `baidu-netdisk`, container `baidu-netdisk-backend` | `deploy plan/apply --env dev --service baidu-netdisk` consumes the registry artifact and verifies labels plus health; dry-run is acceptable while the artifact or registry is absent | `deploy plan/apply --env prod --service baidu-netdisk` recreates only `baidu-netdisk` with `--no-build --no-deps --force-recreate`, then verifies image labels, `/health.deploy.commit`, requested commit and auth health | D601 registry artifact must exist; live apply also requires non-empty Baidu client id, client secret and token key plus logged-in auth health |
|
||||
| `baidu-netdisk` | `ci publish-user-service --service baidu-netdisk` from `src/components/microservices/baidu-netdisk/Dockerfile` | `127.0.0.1:5000/unidesk/baidu-netdisk:<commit>` | master-server Compose service `baidu-netdisk`, container `baidu-netdisk-backend` | `deploy plan/apply --env dev --service baidu-netdisk` consumes the registry artifact and must expose the redacted `runtimeSecrets` source contract; dry-run is acceptable while the artifact, registry or secret source condition is absent | `deploy plan/apply --env prod --service baidu-netdisk` recreates only `baidu-netdisk` with `--no-build --no-deps --force-recreate`, then verifies image labels, `/health.deploy.commit`, requested commit and auth health; current prod artifact/health evidence is aligned | D601 registry artifact must exist; dev live apply remains blocked until canonical Compose env source has non-empty Baidu client id, client secret and token key plus logged-in auth health |
|
||||
| `project-manager` | `ci publish-user-service --service project-manager` from `src/components/microservices/project-manager/Dockerfile` | `127.0.0.1:5000/unidesk/project-manager:<commit>` | master-server Compose service `project-manager`, container `project-manager-backend` | `deploy plan/apply --env dev --service project-manager` consumes the registry artifact and verifies labels plus health; dry-run is acceptable while the artifact or registry is absent | `deploy plan/apply --env prod --service project-manager` recreates only `project-manager` with `--no-build --no-deps --force-recreate`, then verifies image labels, `/health.deploy.commit` and requested commit | D601 registry artifact must exist before live dev or prod apply |
|
||||
|
||||
Focused smoke for this class is intentionally narrow: health, running image labels/digest, live `deploy.commit` / `deploy.requestedCommit`, and one private proxy API check such as `baidu-netdisk /api/transfers?limit=20` or `project-manager /api/projects`. Full e2e, Playwright, broad `check`, public-port probing and unrelated service restarts are outside this lane.
|
||||
@@ -111,6 +111,7 @@ This matrix is the single review surface for the remaining D601 service lane. It
|
||||
| `met-nonlinear` | D601 `unidesk-direct` GPU/business execution service in Docker Compose; control path is backend-core -> provider-gateway private HTTP proxy -> D601 loopback `/health` and `/api/`. | `CI.json` source-build supported through `ci publish-user-service --service met-nonlinear`; cataloged artifact uses `docker/unidesk/Dockerfile.ml` from `https://github.com/pikasTech/met_nonlinear`. | D601 direct Compose consumer is plan/dry-run only for service/container `met-nonlinear-ts`; dry-run exposes the no-build pull-only shape but returns `runtime-verification-blocked`. | Dry-run/read-only only. `deploy apply --env dev --service met-nonlinear --dry-run` must remain blocked until the running service image contract matches the published artifact. | Not authorized. Prod dry-run must remain `runtime-verification-blocked`; live prod apply is unsupported. | Published artifact is the ML image contract while the long-running service is `met-nonlinear-ts`, so CD cannot prove the running container image label equals the requested commit. | Split the TS server artifact from the ML image or publish a labeled artifact that exactly matches `met-nonlinear-ts`; then add live commit proof before enabling apply. |
|
||||
| `k3sctl-adapter` | UniDesk-managed D601 direct Compose control bridge, outside the native k3s fault domain; it is the control path for k3s-managed services and must not be moved into k3s. | `CI.json` source-build supported through `ci publish-user-service --service k3sctl-adapter`; artifact is `127.0.0.1:5000/unidesk/k3sctl-adapter:<commit>` from the UniDesk Dockerfile. | Artifact consumer exposes plan/dry-run only for service/container `k3sctl-adapter`; live replacement is supervisor-only because replacing the bridge can remove the repair path for k3s. | No normal dev target. DEV acceptance is read-only bridge health, service catalog/proxy checks and dry-run contract review only. | Dry-run/read-only only in this lane. Real prod replacement requires explicit supervisor confirmation, rollback proof and out-of-band recovery access. | Must remain recoverable while k3s may be broken; worker automation must not self-replace or k3s-manage the bridge. | Write a supervised bridge-upgrade runbook with rollback and out-of-band access checks; keep CLI dry-run as the standard preflight. |
|
||||
| `code-queue` | Production execution plane is D601 native k3s (`unidesk` namespace) behind `k3sctl-adapter`; dev execution plane is `unidesk-dev` scheduler/read/write/provider-egress-proxy. Main-server `code-queue-mgr` is a separate control-plane sidecar. | `CI.json` source-build supported through `ci publish-user-service --service code-queue` for dev image validation only; artifact is `127.0.0.1:5000/unidesk/code-queue:<commit>`. | Reviewed dev-only k3s artifact consumer updates only `unidesk-dev` Code Queue objects. `deploy plan --env prod --service code-queue` and `artifact-registry deploy-service --env prod --service code-queue` must stay unsupported. | Allowed only as dry-run/source/contract evidence here; a later human-approved dev live apply may consume the artifact into `unidesk-dev` outside the running Code Queue task. | Not implemented and not authorized. No production artifact deploy, manifest mutation, scheduler/runner restart, interrupt or cancel is allowed. | Production still has hostPath/source and active-run safety boundaries; self-deploy would couple the deployment actor to the target being replaced. | Keep contract tests and dev dry-run coverage; design a separate supervisor-approved production CD consumer before any prod mutation is considered. |
|
||||
| `decision-center` | D601 native k3s user service; dev runs `unidesk-dev/decision-center-dev`, prod runs `unidesk/decision-center`, both behind backend-core -> provider-gateway -> k3sctl-adapter -> Kubernetes API service proxy. | `CI.json` source-build supported through `ci publish-user-service --service decision-center`; artifact is `127.0.0.1:5000/unidesk/decision-center:<commit>` from the UniDesk Dockerfile. | Dev and prod are reviewed D601 k3s artifact consumers. Desired state, live health, and registry artifact must point at the same commit; drift where live is newer than `deploy.json` is corrected by repinning `deploy.json`, not by redeploying. | Closed for artifact CD when `deploy plan --env dev --service decision-center` is no-build and health reports matching `deploy.commit` / `deploy.requestedCommit`. Focused product gates remain record CRUD, diary lifecycle and frontend Decision Center visibility. | Closed for artifact CD when `deploy plan --env prod --service decision-center` is no-build and health reports matching `deploy.commit` / `deploy.requestedCommit`. Remaining acceptance is manual UI/product verification: health, records, diary editor, frontend page, no public business ports and live commit/artifact information. | Product completeness and manual UI acceptance can remain open, but they are not deployment drift. Registry artifact digest and health commit are the release evidence. | Keep `scripts/decision-center-desired-state-contract-test.ts` in the lightweight script gate so future desired-state edits cannot reintroduce source-build or stale-commit drift. |
|
||||
|
||||
Minimum evidence for this lane is:
|
||||
|
||||
@@ -123,6 +124,7 @@ Minimum evidence for this lane is:
|
||||
| MET Nonlinear blocked dry-run | `bun scripts/cli.ts deploy apply --env dev --service met-nonlinear --dry-run` |
|
||||
| k3s control bridge dry-run | `bun scripts/cli.ts deploy apply --env prod --service k3sctl-adapter --dry-run` |
|
||||
| CI producer preflight | `bun scripts/cli.ts ci publish-user-service --service <service> --commit <full-sha> --dry-run` |
|
||||
| Decision Center desired/live no-build drift guard | `bun scripts/decision-center-desired-state-contract-test.ts` |
|
||||
|
||||
### Upstream Image Evidence
|
||||
|
||||
@@ -255,11 +257,11 @@ This matrix describes the next promotion stage after dry-run coverage is in plac
|
||||
| `backend-core` | `master` | source-build supported through `ci publish-backend-core` | dev + prod artifact consumer | dev artifact rollout to `unidesk-dev/backend-core-dev` with no CD compile | prod artifact recreate with live commit proof | CI resource, registry artifact, and runtime health evidence | `GPT-5.5` |
|
||||
| `code-queue` | `master` | source-build supported, dev-only | dev-only k3s consumer | dev artifact validation for `unidesk-dev` scheduler/read/write/provider-egress-proxy | not implemented; must remain unsupported | production boundary, hostPath/source contract, scheduler/egress dependency health | `GPT-5.5` |
|
||||
| `frontend` | `master` | source-build supported | dev + prod artifact consumer | commit-pinned dev rollout and `/health.deploy.commit` | commit-pinned prod recreate and UI route verification | none beyond standard artifact/CD checks | `GPT-5.5` |
|
||||
| `baidu-netdisk` | `master` | source-build supported | dev + prod artifact consumer | pull-only dev validation plus auth and proxy checks | pull-only prod recreate plus live commit and proxy checks | secret presence and `/health.auth` gate | `GPT-5.5` |
|
||||
| `baidu-netdisk` | `master` | source-build supported | dev + prod artifact consumer | pull-only dev validation is diagnosable through `runtimeSecrets.secretSource`, `requiredSecretsPresent`, `missingSecretKeys` and `recommendedAction`; live dev apply waits for the secret source condition | prod artifact and health are aligned; further prod action is only focused post-apply proxy/auth verification after operator approval | canonical Compose env secret source and `/health.auth` gate; no manual secret operation in worker tasks | `GPT-5.5` |
|
||||
| `project-manager` | `master` | source-build supported | dev + prod artifact consumer | dev artifact validation with `/api/projects` | prod artifact validation with live commit proof | none beyond standard artifact/CD checks | `MiniMax` for dry-run/reporting, `GPT-5.5` for release sign-off |
|
||||
| `oa-event-flow` | `master` | source-build supported | dev + prod artifact consumer | dev artifact validation with `/api/diagnostics` | prod artifact validation with live commit proof | none beyond standard artifact/CD checks | `MiniMax` for dry-run/reporting, `GPT-5.5` for release sign-off |
|
||||
| `todo-note` | `master` | external source-build supported | dev + prod artifact consumer | dev recreate with PostgreSQL-backed deploy metadata | prod recreate with matching `deploy.commit` and `deploy.requestedCommit` | external repo fetch and runtime metadata consistency | `DeepSeek` for digesting external-source evidence, `GPT-5.5` for final gate |
|
||||
| `decision-center` | `master` | source-build supported | dev + prod k3s consumer | dev gate with record CRUD, diary lifecycle, doc-number uniqueness and frontend visibility | manual prod acceptance after dev gate; verify health, records, diary editor and live commit | doc-management completeness, PostgreSQL truth, no public business ports | `GPT-5.5` |
|
||||
| `decision-center` | `master` | source-build supported | dev + prod k3s consumer closed when desired/live/artifact commit match and dry-run stays no-build | dev artifact CD closed; remaining dev acceptance is focused record CRUD, diary lifecycle, doc-number uniqueness and frontend visibility | prod artifact CD closed; remaining prod acceptance is manual UI/product verification after health/live commit proof | doc-management completeness, PostgreSQL truth and UI acceptance; no deployment drift when desired/live/artifact are aligned | `GPT-5.5` |
|
||||
| `mdtodo` | `master` | source-build supported | dev + prod k3s consumer | dev rollout with deployment metadata and `/health` or `/live` proof | prod rollout with service proxy verification and live commit proof | no NodePort/hostPort/public backend exposure | `MiniMax` for prompt prep, `GPT-5.5` for approval |
|
||||
| `claudeqq` | `master` | source-build supported | dev + prod k3s consumer | dev rollout with Deployment metadata and health via Kubernetes API proxy | prod rollout with same commit-pinned artifact contract | NapCat/backend port exposure must stay private | `MiniMax` for prompt prep, `GPT-5.5` for approval |
|
||||
| `findjob` | `master` | source-build supported | dev + prod direct Compose consumer | pull-only dev validation on D601 with image labels and `/api/health` | pull-only prod recreate with live commit proof | target-side compose health/labels only, no public business ports | `DeepSeek` for dry-run matrix drafting, `GPT-5.5` for final gate |
|
||||
|
||||
@@ -112,7 +112,7 @@ Production `code-queue-mgr` is a separate main-server Compose sidecar artifact c
|
||||
|
||||
`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`.
|
||||
|
||||
Environment plan output must be sufficient to review the artifact matrix without running a live apply. Each service item includes `deploymentPath`, `artifactConsumer.consumerKind`, `artifactConsumer.registryImage`, `artifactConsumer.registry`, `artifactConsumer.source`, `artifactConsumer.build`, `artifactConsumer.noRuntimeSourceBuild`, `artifactConsumer.dryRunOnly`, `target`, `validation` and `liveApply` where relevant. `consumerKind=d601-direct-compose` means the reviewed consumer touches only the D601 Docker/Compose service and private health path; `consumerKind=d601-k3s-managed` means the reviewed consumer imports the artifact into native k3s/containerd and verifies through the Kubernetes API service proxy; `consumerKind=main-server-compose` means the reviewed consumer streams or loads the D601 artifact into the main-server Compose service; `consumerKind=d601-dev-target-side-build` is retained only as a legacy classification and should not appear for backend-core. Artifact consumer plan items must explicitly report `noRuntimeSourceBuild=true`, expose registry/source/build boundaries including digest provenance, and list forbidden build/public exposure actions. Blocked or gated services must keep structured `dryRunOnly` / `blockedReason` output, for example `met-nonlinear` `runtime-verification-blocked` and `k3sctl-adapter` supervisor-only production apply.
|
||||
Environment plan output must be sufficient to review the artifact matrix without running a live apply. Each service item includes `deploymentPath`, `artifactConsumer.consumerKind`, `artifactConsumer.registryImage`, `artifactConsumer.registry`, `artifactConsumer.source`, `artifactConsumer.build`, `artifactConsumer.noRuntimeSourceBuild`, `artifactConsumer.dryRunOnly`, `target`, `validation` and `liveApply` where relevant. `consumerKind=d601-direct-compose` means the reviewed consumer touches only the D601 Docker/Compose service and private health path; `consumerKind=d601-k3s-managed` means the reviewed consumer imports the artifact into native k3s/containerd and verifies through the Kubernetes API service proxy; `consumerKind=main-server-compose` means the reviewed consumer streams or loads the D601 artifact into the main-server Compose service; `consumerKind=d601-dev-target-side-build` is retained only as a legacy classification and should not appear for backend-core. Artifact consumer plan items must explicitly report `noRuntimeSourceBuild=true`, expose registry/source/build boundaries including digest provenance, and list forbidden build/public exposure actions. Services with runtime secret gates, currently `baidu-netdisk`, must also expose a redacted `artifactConsumer.runtimeSecrets` contract with `secretSource`, `requiredSecretsPresent`, `missingSecretKeys` and `recommendedAction`; this contract may report key names, booleans and lengths only, never secret values. Blocked or gated services must keep structured `dryRunOnly` / `blockedReason` output, for example `met-nonlinear` `runtime-verification-blocked` and `k3sctl-adapter` supervisor-only production apply.
|
||||
|
||||
For `--env dev --service code-queue`, the environment plan must also expose a `boundary` block that separates the CI producer from the dev CD consumer. CI is allowed to publish only `127.0.0.1:5000/unidesk/code-queue:<commit>` plus digest/label evidence. DEV CD may consume that artifact only for `unidesk-dev` Code Queue scheduler/read/write/provider-egress-proxy objects after an operator reviews the dry-run. For `--env prod --service code-queue`, the service item must remain `deploymentPath=unsupported`, `artifactConsumer.consumerKind=unsupported`, `target.deployCommandShape=none` and `liveApply.allowed=false`; it must not expose production k3s as an executable target. The prod boundary must state that production Code Queue CD needs a future supervisor-approved design and that this runner cannot self-deploy, mutate the production namespace, restart scheduler/runner, or interrupt/cancel tasks.
|
||||
|
||||
@@ -145,7 +145,7 @@ The exception is narrow:
|
||||
- CD must not run Rust compilation, Docker build, Compose build or `server rebuild backend-core`.
|
||||
- The legacy `artifact-registry deploy-backend-core` compatibility entry is deprecated and disabled as a standard entrypoint; use `deploy apply --env prod --service backend-core --commit <full-sha>` so the common artifact-consumer guardrails execute first.
|
||||
- The pushed Git commit remains the version source of truth. The image registry is a content cache and transfer boundary, not a replacement for `deploy.json` or Git.
|
||||
- `baidu-netdisk` is the first main-server direct user-service sample for the same split: CI publishes `127.0.0.1:5000/unidesk/baidu-netdisk:<commit>` from `src/components/microservices/baidu-netdisk/Dockerfile`; dev validation and prod CD both pull that artifact, retag `baidu-netdisk`, recreate only `baidu-netdisk` with `--no-build --no-deps --force-recreate`, and verify image labels plus `/health.deploy.commit`.
|
||||
- `baidu-netdisk` is the first main-server direct user-service sample for the same split: CI publishes `127.0.0.1:5000/unidesk/baidu-netdisk:<commit>` from `src/components/microservices/baidu-netdisk/Dockerfile`; dev validation and prod CD both pull that artifact, retag `baidu-netdisk`, recreate only `baidu-netdisk` with `--no-build --no-deps --force-recreate`, and verify image labels plus `/health.deploy.commit`. The current prod lane is aligned when the artifact, running image and health commit match; dev apply remains gated until the canonical Compose env secret source reports the three Baidu keys present through the redacted `runtimeSecrets` contract.
|
||||
- `frontend` is the UniDesk UI artifact sample: CI publishes `127.0.0.1:5000/unidesk/frontend:<commit>` from `src/components/frontend/Dockerfile`; dev CD imports that artifact into native k3s `frontend-dev`, prod CD retags it as `unidesk-frontend` for the master-server Compose service, and both paths verify image labels plus `/health.deploy.commit`.
|
||||
- `findjob` and `pipeline` are D601 direct Docker/Compose artifact consumers: CD runs on D601 through the existing provider-gateway/SSH maintenance bridge, verifies `127.0.0.1:5000/unidesk/<service>:<commit>` labels, writes deploy env/labels, and recreates only the target Compose service with `--no-build --no-deps --force-recreate`.
|
||||
- `met-nonlinear` has a D601 direct dry-run/plan contract, but live artifact deploy is blocked until the long-running `met-nonlinear-ts` image contract is separated from the ML image Dockerfile contract or otherwise proves the running container image label matches the requested commit.
|
||||
@@ -200,7 +200,7 @@ Code Queue health and diagnostics must cover its k3s dependencies, not only sche
|
||||
|
||||
Existing service-specific commands such as Code Queue deploy are disabled as direct D601 deploy paths. Their build/import/rollout semantics should converge later into one controlled target-side deployment path instead of keeping parallel implementations.
|
||||
|
||||
Baidu Netdisk is the main-server `unidesk-direct` sample for artifact CD and a dependency of the PGDATA-to-Baidu-Netdisk backup path. Controlled dev validation and prod CD use the D601 registry artifact consumer: it verifies `unidesk/baidu-netdisk:<commit>` exists in the registry, streams the image to the main server through provider-gateway Host SSH, retags `baidu-netdisk` and `baidu-netdisk:<commit>`, stamps `UNIDESK_BAIDU_NETDISK_DEPLOY_*` in the canonical Compose env file, recreates only Compose service `baidu-netdisk`, and verifies container health, image labels, service id, `/health.deploy.commit`, and `/health.auth`. Live apply must fail or return degraded before success if `UNIDESK_BAIDU_NETDISK_CLIENT_ID`, `UNIDESK_BAIDU_NETDISK_CLIENT_SECRET`, or `UNIDESK_BAIDU_NETDISK_TOKEN_KEY` is absent from the controlled env source, or if `/health.auth.configured`, `clientIdConfigured`, `clientSecretConfigured`, `tokenKeyConfigured`, or `loggedIn` is not true after recreate. Dry-run only reports that these secret presences and auth fields are required and pending live check; it must not read or print secret values. It must not use `server rebuild baidu-netdisk`, mutable tags, dirty worktrees, hand-built images, or public `4244` exposure as deployment truth.
|
||||
Baidu Netdisk is the main-server `unidesk-direct` sample for artifact CD and a dependency of the PGDATA-to-Baidu-Netdisk backup path. Controlled dev validation and prod CD use the D601 registry artifact consumer: it verifies `unidesk/baidu-netdisk:<commit>` exists in the registry, streams the image to the main server through provider-gateway Host SSH, retags `baidu-netdisk` and `baidu-netdisk:<commit>`, stamps `UNIDESK_BAIDU_NETDISK_DEPLOY_*` in the canonical Compose env file, recreates only Compose service `baidu-netdisk`, and verifies container health, image labels, service id, `/health.deploy.commit`, and `/health.auth`. Live apply must fail or return degraded before success if `UNIDESK_BAIDU_NETDISK_CLIENT_ID`, `UNIDESK_BAIDU_NETDISK_CLIENT_SECRET`, or `UNIDESK_BAIDU_NETDISK_TOKEN_KEY` is absent from the controlled env source, or if `/health.auth.configured`, `clientIdConfigured`, `clientSecretConfigured`, `tokenKeyConfigured`, or `loggedIn` is not true after recreate. Dry-run reports `secretSource`, `requiredSecretsPresent`, `missingSecretKeys` and `recommendedAction` so a missing dev secret source is diagnosable before live apply; it must not print secret values. It must not use `server rebuild baidu-netdisk`, mutable tags, dirty worktrees, hand-built images, or public `4244` exposure as deployment truth.
|
||||
|
||||
For PGDATA-to-Baidu-Netdisk incident review, the no-authorization read-only boundary is limited to `server status`, `schedule list`, `schedule get`, `schedule runs`, `microservice status/health baidu-netdisk`, `microservice proxy baidu-netdisk /api/auth/status --raw`, and `microservice proxy baidu-netdisk '/api/transfers?limit=20' --raw`. These commands may report `failureKind=target-stack-not-running` when `unidesk-backend-core`, `unidesk-database`, or `baidu-netdisk-backend` is absent, especially when only `*.verify-*` containers are visible; that state is an infrastructure blocker, not a successful empty backup history. Recovery actions such as restoring non-empty Baidu secrets, `server start`, `server rebuild backend-core`, `server rebuild baidu-netdisk`, `deploy apply --env prod --service baidu-netdisk`, `schedule run`, or `schedule retry-run` can affect production or trigger a real backup and require explicit operator authorization.
|
||||
|
||||
|
||||
@@ -152,6 +152,19 @@ for (const item of serviceCases) {
|
||||
`${item.serviceId} dry-run must narrow Compose recreate command`,
|
||||
target,
|
||||
);
|
||||
if (item.serviceId === "baidu-netdisk") {
|
||||
const runtimeSecrets = asRecord(plan.runtimeSecrets, "baidu-netdisk dry-run runtimeSecrets");
|
||||
const secretSource = asRecord(runtimeSecrets.secretSource, "baidu-netdisk dry-run secretSource");
|
||||
const requirements = asArray(runtimeSecrets.requirements, "baidu-netdisk dry-run requirements").map((requirement, index) => asRecord(requirement, `baidu-netdisk dry-run requirement ${index}`));
|
||||
assertCondition(runtimeSecrets.check === "runtime-secret-presence", "baidu-netdisk dry-run should expose runtime secret contract", runtimeSecrets);
|
||||
assertCondition(secretSource.kind === "compose-env-file", "baidu-netdisk dry-run should expose canonical secret source kind", secretSource);
|
||||
assertCondition(secretSource.valuesPrinted === false && runtimeSecrets.valuesPrinted === false, "baidu-netdisk dry-run must not print secret values", runtimeSecrets);
|
||||
assertCondition(typeof runtimeSecrets.requiredSecretsPresent === "boolean", "baidu-netdisk requiredSecretsPresent should be boolean", runtimeSecrets);
|
||||
assertCondition(Array.isArray(runtimeSecrets.missingSecretKeys), "baidu-netdisk missingSecretKeys should be structured", runtimeSecrets);
|
||||
assertCondition(typeof runtimeSecrets.recommendedAction === "string" && runtimeSecrets.recommendedAction.length > 0, "baidu-netdisk recommendedAction should be explicit", runtimeSecrets);
|
||||
assertCondition(requirements.length === 3, "baidu-netdisk should list three required source secret keys", requirements);
|
||||
assertCondition(requirements.every((requirement) => requirement.valuePrinted === false), "baidu-netdisk should never mark values printed", requirements);
|
||||
}
|
||||
|
||||
assertCondition(validation.some((line) => line.includes("registry /v2 manifest")), `${item.serviceId} must plan registry manifest validation`, validation);
|
||||
assertCondition(validation.some((line) => line.includes("image labels match service id, source commit, and Dockerfile")), `${item.serviceId} must plan image label validation`, validation);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
baiduNetdiskAuthHealthGateStatus,
|
||||
baiduNetdiskRuntimeSecretRequirements,
|
||||
runtimeSecretContractFromEnvText,
|
||||
runtimeSecretPresenceFromEnvText,
|
||||
} from "./src/artifact-registry";
|
||||
|
||||
@@ -19,6 +20,21 @@ assertCondition(present.every((item) => item.present), "all baidu-netdisk secret
|
||||
assertCondition(present.map((item) => item.length).join(",") === "31,30,64", "presence reports only lengths", present);
|
||||
assertCondition(!JSON.stringify(present).includes("0123456789abcdef"), "secret values must not be exposed", present);
|
||||
|
||||
const presentContract = runtimeSecretContractFromEnvText(secretEnvText, baiduNetdiskRuntimeSecretRequirements, {
|
||||
path: "/root/unidesk/.state/docker-compose.env",
|
||||
exists: true,
|
||||
workDir: "/root/unidesk",
|
||||
composeEnvFile: ".state/docker-compose.env",
|
||||
composeService: "baidu-netdisk",
|
||||
containerName: "baidu-netdisk-backend",
|
||||
});
|
||||
assertCondition(presentContract.secretSource.kind === "compose-env-file", "secret source should name canonical compose env file", presentContract);
|
||||
assertCondition(presentContract.requiredSecretsPresent === true, "present contract should pass", presentContract);
|
||||
assertCondition(presentContract.missingSecretKeys.length === 0, "present contract should not list missing keys", presentContract);
|
||||
assertCondition(presentContract.recommendedAction === "none", "present contract should recommend no action", presentContract);
|
||||
assertCondition(presentContract.valuesPrinted === false, "present contract must not print values", presentContract);
|
||||
assertCondition(!JSON.stringify(presentContract).includes("clientsecret"), "contract must not expose fake secret values", presentContract);
|
||||
|
||||
const missing = runtimeSecretPresenceFromEnvText(
|
||||
"UNIDESK_BAIDU_NETDISK_CLIENT_ID=clientid-clientid-clientid-0000\n",
|
||||
baiduNetdiskRuntimeSecretRequirements,
|
||||
@@ -30,6 +46,30 @@ assertCondition(
|
||||
missing,
|
||||
);
|
||||
|
||||
const missingContract = runtimeSecretContractFromEnvText(
|
||||
"UNIDESK_BAIDU_NETDISK_CLIENT_ID=clientid-clientid-clientid-0000\n",
|
||||
baiduNetdiskRuntimeSecretRequirements,
|
||||
{
|
||||
path: "/root/unidesk/.state/docker-compose.env",
|
||||
exists: true,
|
||||
workDir: "/root/unidesk",
|
||||
composeEnvFile: ".state/docker-compose.env",
|
||||
composeService: "baidu-netdisk",
|
||||
containerName: "baidu-netdisk-backend",
|
||||
},
|
||||
);
|
||||
assertCondition(missingContract.requiredSecretsPresent === false, "missing contract should fail", missingContract);
|
||||
assertCondition(
|
||||
missingContract.missingSecretKeys.join(",") === "UNIDESK_BAIDU_NETDISK_CLIENT_SECRET,UNIDESK_BAIDU_NETDISK_TOKEN_KEY",
|
||||
"missing contract should expose missing source keys",
|
||||
missingContract,
|
||||
);
|
||||
assertCondition(
|
||||
String(missingContract.recommendedAction).includes("canonical Compose env file"),
|
||||
"missing contract should recommend the canonical source",
|
||||
missingContract,
|
||||
);
|
||||
|
||||
const healthy = baiduNetdiskAuthHealthGateStatus({
|
||||
auth: {
|
||||
configured: true,
|
||||
@@ -61,10 +101,13 @@ process.stdout.write(`${JSON.stringify({
|
||||
ok: true,
|
||||
checks: [
|
||||
"runtime secret presence reports booleans and lengths only",
|
||||
"runtime secret contract exposes secretSource/requiredSecretsPresent/missingSecretKeys/recommendedAction without values",
|
||||
"missing Baidu Netdisk env cannot pass the deploy contract",
|
||||
"auth health gate requires configured/clientId/clientSecret/tokenKey/loggedIn",
|
||||
],
|
||||
present,
|
||||
presentContract,
|
||||
missing: missing.map((item) => ({ ...item, length: item.length })),
|
||||
missingContract,
|
||||
degraded,
|
||||
}, null, 2)}\n`);
|
||||
|
||||
@@ -58,6 +58,20 @@ function assertMainServerComposeConsumer(
|
||||
assertCondition(target.containerName === expectedContainerName, `${serviceId} container mismatch`, target);
|
||||
assertCondition(listIncludes(target.forbiddenActions, "docker build"), `${serviceId} plan should forbid docker build`, target);
|
||||
assertCondition(listIncludes(target.forbiddenActions, "docker compose build"), `${serviceId} plan should forbid compose build`, target);
|
||||
if (serviceId === "baidu-netdisk") {
|
||||
const runtimeSecrets = asRecord(artifact.runtimeSecrets, `${serviceId} ${environment} runtimeSecrets`);
|
||||
const secretSource = asRecord(runtimeSecrets.secretSource, `${serviceId} ${environment} secretSource`);
|
||||
const requirements = Array.isArray(runtimeSecrets.requirements) ? runtimeSecrets.requirements.map((item, index) => asRecord(item, `${serviceId} ${environment} requirement ${index}`)) : [];
|
||||
assertCondition(runtimeSecrets.check === "runtime-secret-presence", `${serviceId} should expose secret presence check`, runtimeSecrets);
|
||||
assertCondition(secretSource.kind === "compose-env-file", `${serviceId} should name compose env secret source`, secretSource);
|
||||
assertCondition(secretSource.valuesPrinted === false && runtimeSecrets.valuesPrinted === false, `${serviceId} must not print secret values`, runtimeSecrets);
|
||||
assertCondition(typeof runtimeSecrets.requiredSecretsPresent === "boolean", `${serviceId} requiredSecretsPresent should be boolean`, runtimeSecrets);
|
||||
assertCondition(Array.isArray(runtimeSecrets.missingSecretKeys), `${serviceId} missingSecretKeys should be an array`, runtimeSecrets);
|
||||
assertCondition(typeof runtimeSecrets.recommendedAction === "string" && runtimeSecrets.recommendedAction.length > 0, `${serviceId} recommendedAction should be explicit`, runtimeSecrets);
|
||||
assertCondition(requirements.length === 3, `${serviceId} should list three required source secrets`, runtimeSecrets);
|
||||
assertCondition(requirements.every((item) => item.valuePrinted === false), `${serviceId} requirements must not print values`, requirements);
|
||||
assertCondition(!JSON.stringify(runtimeSecrets).includes("0123456789abcdef"), `${serviceId} must not leak secret-looking values`, runtimeSecrets);
|
||||
}
|
||||
}
|
||||
|
||||
assertMainServerComposeConsumer("dev", "baidu-netdisk", "baidu-netdisk", "baidu-netdisk-backend");
|
||||
|
||||
@@ -166,6 +166,7 @@ interface AuthHealthGate {
|
||||
interface ComposeArtifactRuntime {
|
||||
workDir: string;
|
||||
composeFile: string;
|
||||
composeEnvFile: string;
|
||||
envFile: string;
|
||||
project: string;
|
||||
command: string[];
|
||||
@@ -178,6 +179,36 @@ export interface RuntimeSecretPresence {
|
||||
length: number;
|
||||
}
|
||||
|
||||
export interface RuntimeSecretSource {
|
||||
kind: "compose-env-file";
|
||||
path: string;
|
||||
exists: boolean;
|
||||
configSource: "config.providerGateway.upgrade.hostProjectRoot + config.providerGateway.upgrade.composeEnvFile";
|
||||
workDir: string;
|
||||
composeEnvFile: string;
|
||||
composeService: string | null;
|
||||
containerName: string | null;
|
||||
valuesPrinted: false;
|
||||
}
|
||||
|
||||
export interface RuntimeSecretRequirementStatus {
|
||||
sourceEnvName: string;
|
||||
containerEnvName: string;
|
||||
present: boolean;
|
||||
valuePrinted: false;
|
||||
}
|
||||
|
||||
export interface RuntimeSecretContract {
|
||||
check: "runtime-secret-presence";
|
||||
secretSource: RuntimeSecretSource;
|
||||
requiredSecretsPresent: boolean;
|
||||
missingSecretKeys: string[];
|
||||
recommendedAction: string;
|
||||
valuesPrinted: false;
|
||||
requirements: RuntimeSecretRequirementStatus[];
|
||||
dryRunDisposition: "not-required" | "ready-for-live-apply" | "secret-source-blocked";
|
||||
}
|
||||
|
||||
function todoNoteHealthProbeCommand(): string {
|
||||
return "bun -e \"fetch('http://127.0.0.1:4211/api/health').then(async r=>{const text=await r.text(); let body; try{body=JSON.parse(text)}catch{body={ok:r.ok,raw:text}}; const deploy=body.deploy&&typeof body.deploy==='object'&&!Array.isArray(body.deploy)?body.deploy:{}; body.deploy={...deploy,serviceId:deploy.serviceId||process.env.UNIDESK_DEPLOY_SERVICE_ID||'todo-note',ref:deploy.ref||process.env.UNIDESK_DEPLOY_REF||'',repo:deploy.repo||process.env.UNIDESK_DEPLOY_REPO||'',commit:deploy.commit||process.env.UNIDESK_DEPLOY_COMMIT||'',requestedCommit:deploy.requestedCommit||process.env.UNIDESK_DEPLOY_REQUESTED_COMMIT||''}; console.log(JSON.stringify(body)); process.exit(r.ok?0:1)}).catch(e=>{console.error(e); process.exit(1)})\"";
|
||||
}
|
||||
@@ -1553,13 +1584,12 @@ function composeLockScript(innerScript: string): string {
|
||||
].join("; ");
|
||||
}
|
||||
|
||||
function parseEnvFile(raw: string): Record<string, string> {
|
||||
const values: Record<string, string> = {};
|
||||
function findEnvValueLength(raw: string, key: string): number {
|
||||
for (const line of raw.split(/\r?\n/u)) {
|
||||
if (!line.trim() || line.trimStart().startsWith("#")) continue;
|
||||
const index = line.indexOf("=");
|
||||
if (index <= 0) continue;
|
||||
const key = line.slice(0, index);
|
||||
if (line.slice(0, index) !== key) continue;
|
||||
let value = line.slice(index + 1);
|
||||
if (value.startsWith("\"") && value.endsWith("\"")) {
|
||||
try {
|
||||
@@ -1568,20 +1598,19 @@ function parseEnvFile(raw: string): Record<string, string> {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
}
|
||||
values[key] = value;
|
||||
return value.length;
|
||||
}
|
||||
return values;
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function runtimeSecretPresenceFromEnvText(envText: string, requirements: RuntimeSecretRequirement[]): RuntimeSecretPresence[] {
|
||||
const values = parseEnvFile(envText);
|
||||
return requirements.map((requirement) => {
|
||||
const value = values[requirement.sourceEnvName] ?? "";
|
||||
const length = findEnvValueLength(envText, requirement.sourceEnvName);
|
||||
return {
|
||||
sourceEnvName: requirement.sourceEnvName,
|
||||
containerEnvName: requirement.containerEnvName,
|
||||
present: value.length > 0,
|
||||
length: value.length,
|
||||
present: length > 0,
|
||||
length,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -1602,10 +1631,97 @@ function composeArtifactRuntime(config: UniDeskConfig, target: ArtifactConsumerT
|
||||
const compose = target.compose;
|
||||
const workDir = compose.workDir ?? config.providerGateway.upgrade.hostProjectRoot;
|
||||
const composeFile = compose.composeFile ?? config.providerGateway.upgrade.composeFile;
|
||||
const envFile = join(workDir, config.providerGateway.upgrade.composeEnvFile);
|
||||
const composeEnvFile = config.providerGateway.upgrade.composeEnvFile;
|
||||
const envFile = join(workDir, composeEnvFile);
|
||||
const project = compose.projectHint ?? (config.providerGateway.upgrade.composeProject || config.docker.projectName);
|
||||
const command = ["docker", "compose", "--env-file", envFile, "-f", join(workDir, composeFile), "-p", project];
|
||||
return { workDir, composeFile, envFile, project, command };
|
||||
return { workDir, composeFile, composeEnvFile, envFile, project, command };
|
||||
}
|
||||
|
||||
function runtimeSecretSourceForTarget(config: UniDeskConfig, target: ArtifactConsumerTarget): RuntimeSecretSource {
|
||||
const runtime = composeArtifactRuntime(config, target);
|
||||
return {
|
||||
kind: "compose-env-file",
|
||||
path: runtime.envFile,
|
||||
exists: existsSync(runtime.envFile),
|
||||
configSource: "config.providerGateway.upgrade.hostProjectRoot + config.providerGateway.upgrade.composeEnvFile",
|
||||
workDir: runtime.workDir,
|
||||
composeEnvFile: runtime.composeEnvFile,
|
||||
composeService: target.compose?.serviceName ?? null,
|
||||
containerName: target.compose?.containerName ?? null,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function runtimeSecretRecommendedAction(missing: string[]): string {
|
||||
if (missing.length === 0) return "none";
|
||||
return "Restore the missing source env keys in the canonical Compose env file without printing values, then rerun deploy apply --env <env> --service <service> --dry-run before any live apply.";
|
||||
}
|
||||
|
||||
function runtimeSecretRecommendedActionForService(contract: RuntimeSecretContract, environment: ArtifactDeployEnvironment, serviceId: string): RuntimeSecretContract {
|
||||
if (contract.requiredSecretsPresent) return { ...contract, recommendedAction: "none" };
|
||||
return {
|
||||
...contract,
|
||||
recommendedAction: `Restore ${contract.missingSecretKeys.join(", ")} in the canonical Compose env file without printing values, then rerun deploy apply --env ${environment} --service ${serviceId} --dry-run before any live apply.`,
|
||||
};
|
||||
}
|
||||
|
||||
function runtimeSecretContractFromPresence(
|
||||
source: RuntimeSecretSource,
|
||||
requirements: RuntimeSecretRequirement[],
|
||||
presence: RuntimeSecretPresence[],
|
||||
): RuntimeSecretContract {
|
||||
const missingSecretKeys = presence.filter((item) => !item.present).map((item) => item.sourceEnvName);
|
||||
const requiredSecretsPresent = requirements.length === 0 || (source.exists && missingSecretKeys.length === 0);
|
||||
return {
|
||||
check: "runtime-secret-presence",
|
||||
secretSource: source,
|
||||
requiredSecretsPresent,
|
||||
missingSecretKeys,
|
||||
recommendedAction: runtimeSecretRecommendedAction(missingSecretKeys),
|
||||
valuesPrinted: false,
|
||||
requirements: presence.map((item) => ({
|
||||
sourceEnvName: item.sourceEnvName,
|
||||
containerEnvName: item.containerEnvName,
|
||||
present: item.present,
|
||||
valuePrinted: false,
|
||||
})),
|
||||
dryRunDisposition: requirements.length === 0
|
||||
? "not-required"
|
||||
: requiredSecretsPresent
|
||||
? "ready-for-live-apply"
|
||||
: "secret-source-blocked",
|
||||
};
|
||||
}
|
||||
|
||||
export function runtimeSecretContractFromEnvText(
|
||||
envText: string,
|
||||
requirements: RuntimeSecretRequirement[],
|
||||
sourceOverrides: Partial<Omit<RuntimeSecretSource, "kind" | "valuesPrinted">> = {},
|
||||
): RuntimeSecretContract {
|
||||
const source: RuntimeSecretSource = {
|
||||
kind: "compose-env-file",
|
||||
path: sourceOverrides.path ?? "<env-text>",
|
||||
exists: sourceOverrides.exists ?? true,
|
||||
configSource: sourceOverrides.configSource ?? "config.providerGateway.upgrade.hostProjectRoot + config.providerGateway.upgrade.composeEnvFile",
|
||||
workDir: sourceOverrides.workDir ?? "",
|
||||
composeEnvFile: sourceOverrides.composeEnvFile ?? "",
|
||||
composeService: sourceOverrides.composeService ?? null,
|
||||
containerName: sourceOverrides.containerName ?? null,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
return runtimeSecretContractFromPresence(source, requirements, runtimeSecretPresenceFromEnvText(envText, requirements));
|
||||
}
|
||||
|
||||
export function runtimeSecretContractForComposeTarget(
|
||||
config: UniDeskConfig,
|
||||
target: ArtifactConsumerTarget,
|
||||
requirements: RuntimeSecretRequirement[] | undefined,
|
||||
): RuntimeSecretContract | undefined {
|
||||
if (requirements === undefined) return undefined;
|
||||
const source = runtimeSecretSourceForTarget(config, target);
|
||||
const envText = source.exists ? readFileSync(source.path, "utf8") : "";
|
||||
return runtimeSecretContractFromPresence(source, requirements, runtimeSecretPresenceFromEnvText(envText, requirements));
|
||||
}
|
||||
|
||||
function composeArtifactSecretPreflight(envFile: string, requirements: RuntimeSecretRequirement[] | undefined): { ok: boolean; envFile: string; requirements: RuntimeSecretPresence[]; missing: RuntimeSecretPresence[]; valuesLogged: false } {
|
||||
@@ -1767,6 +1883,10 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
|
||||
const config = readConfig();
|
||||
const runtime = composeArtifactRuntime(config, target);
|
||||
const secretPreflight = composeArtifactSecretPreflight(runtime.envFile, target.compose.requiredRuntimeSecrets);
|
||||
const runtimeSecretsRaw = runtimeSecretContractForComposeTarget(config, target, target.compose.requiredRuntimeSecrets);
|
||||
const runtimeSecrets = runtimeSecretsRaw === undefined
|
||||
? undefined
|
||||
: runtimeSecretRecommendedActionForService(runtimeSecretsRaw, options.environment ?? "prod", spec.serviceId);
|
||||
if (!secretPreflight.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -1778,6 +1898,11 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
|
||||
envFile: runtime.envFile,
|
||||
requirements: secretPreflight.requirements,
|
||||
missing: secretPreflight.missing,
|
||||
secretSource: runtimeSecrets?.secretSource,
|
||||
requiredSecretsPresent: runtimeSecrets?.requiredSecretsPresent ?? false,
|
||||
missingSecretKeys: runtimeSecrets?.missingSecretKeys ?? secretPreflight.missing.map((item) => item.sourceEnvName),
|
||||
recommendedAction: runtimeSecrets?.recommendedAction ?? "Restore the required runtime secrets in the canonical Compose env file, then rerun artifact deploy.",
|
||||
runtimeSecrets,
|
||||
valuesLogged: false,
|
||||
recoveryHint: target.compose.authHealthGate?.recoveryHint ?? "Restore the required runtime secrets in the canonical Compose env file, then rerun artifact deploy.",
|
||||
};
|
||||
@@ -1898,6 +2023,7 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
|
||||
serviceHealthCommit: target.compose.requireHealthCommit ? commit : "not-required",
|
||||
serviceHealthRequestedCommit: target.compose.requireHealthCommit ? commit : "not-required",
|
||||
requiredRuntimeSecrets: secretPreflight.requirements,
|
||||
runtimeSecrets,
|
||||
authHealthGate: target.compose.authHealthGate === undefined ? "not-required" : {
|
||||
requiredFields: target.compose.authHealthGate.requiredFields,
|
||||
passed: true,
|
||||
@@ -2119,6 +2245,7 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
|
||||
const livePolicy = environment === "prod" ? spec.prodLiveApply : "enabled";
|
||||
const sourceImage = artifactImageRef(options, spec, commit);
|
||||
const registryEndpoint = `http://127.0.0.1:${options.port}`;
|
||||
const config = readConfig();
|
||||
const k3sDeployments = target.k3s === undefined
|
||||
? []
|
||||
: [
|
||||
@@ -2180,6 +2307,8 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
|
||||
};
|
||||
if (spec.kind === "compose" || spec.kind === "d601-compose") {
|
||||
if (target.compose === undefined) throw new Error(`${spec.serviceId} missing compose artifact consumer config`);
|
||||
const runtimeSecretsRaw = runtimeSecretContractForComposeTarget(config, target, target.compose.requiredRuntimeSecrets);
|
||||
const runtimeSecrets = runtimeSecretsRaw === undefined ? undefined : runtimeSecretRecommendedActionForService(runtimeSecretsRaw, environment, spec.serviceId);
|
||||
return {
|
||||
...common,
|
||||
target: {
|
||||
@@ -2227,16 +2356,7 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
|
||||
`${spec.serviceId} live apply gates success on /health.auth fields: ${target.compose.authHealthGate.requiredFields.join(", ")}`,
|
||||
]),
|
||||
],
|
||||
runtimeSecrets: target.compose.requiredRuntimeSecrets === undefined ? undefined : {
|
||||
check: "live-apply-preflight",
|
||||
valuesPrinted: false,
|
||||
requirements: target.compose.requiredRuntimeSecrets.map((item) => ({
|
||||
sourceEnvName: item.sourceEnvName,
|
||||
containerEnvName: item.containerEnvName,
|
||||
presence: "not-read-during-dry-run",
|
||||
})),
|
||||
dryRunDisposition: "pending-live-check",
|
||||
},
|
||||
runtimeSecrets,
|
||||
authHealthGate: target.compose.authHealthGate === undefined ? undefined : {
|
||||
path: "/health",
|
||||
requiredAuthFields: target.compose.authHealthGate.requiredFields,
|
||||
|
||||
+24
-1
@@ -5,7 +5,7 @@ import { pathToFileURL } from "node:url";
|
||||
import { runCommand } from "./command";
|
||||
import { type UniDeskConfig, type UniDeskMicroserviceConfig, repoRoot, rootPath } from "./config";
|
||||
import { ensureGithubSshIdentityForProvider } from "./deploy-ssh-identity";
|
||||
import { runArtifactRegistryCommand } from "./artifact-registry";
|
||||
import { baiduNetdiskRuntimeSecretRequirements, runtimeSecretContractFromEnvText, type RuntimeSecretContract, runArtifactRegistryCommand } from "./artifact-registry";
|
||||
import { startJob } from "./jobs";
|
||||
import { coreInternalFetch } from "./microservices";
|
||||
import { codeQueueSourceImportPreflight, codeQueueSourceSubdir } from "./code-queue-source-guard";
|
||||
@@ -1216,6 +1216,27 @@ function directComposeEnvFile(service: UniDeskMicroserviceConfig): string {
|
||||
return targetIsMain(service) ? writeComposeEnvFallbackPath() : "";
|
||||
}
|
||||
|
||||
function redactedSecretContractForService(config: UniDeskConfig | null, service: UniDeskMicroserviceConfig, environment: DeployEnvironment): RuntimeSecretContract | undefined {
|
||||
if (config === null || service.id !== "baidu-netdisk" || !targetIsMain(service)) return undefined;
|
||||
const composeEnvFile = config.providerGateway.upgrade.composeEnvFile;
|
||||
const envFile = join(config.providerGateway.upgrade.hostProjectRoot, composeEnvFile);
|
||||
const envText = existsSync(envFile) ? readFileSync(envFile, "utf8") : "";
|
||||
const contract = runtimeSecretContractFromEnvText(envText, baiduNetdiskRuntimeSecretRequirements, {
|
||||
path: envFile,
|
||||
exists: existsSync(envFile),
|
||||
workDir: config.providerGateway.upgrade.hostProjectRoot,
|
||||
composeEnvFile,
|
||||
composeService: service.repository.composeService,
|
||||
containerName: service.repository.containerName,
|
||||
});
|
||||
return {
|
||||
...contract,
|
||||
recommendedAction: contract.requiredSecretsPresent
|
||||
? "none"
|
||||
: `Restore ${contract.missingSecretKeys.join(", ")} in the canonical Compose env file without printing values, then rerun deploy apply --env ${environment} --service ${service.id} --dry-run before any live apply.`,
|
||||
};
|
||||
}
|
||||
|
||||
function directBuildContextOverride(service: UniDeskMicroserviceConfig): string {
|
||||
if (targetIsMain(service) && isUnideskRepo(service.repository.url)) return targetWorkDir(service);
|
||||
return "";
|
||||
@@ -2946,6 +2967,7 @@ function environmentDryRunPlan(
|
||||
const dryRunBlockedReason = artifactConsumerDryRunBlockedServiceIds.get(service.id) ?? null;
|
||||
const planKind = serviceConfig === null ? "unsupported" : artifactConsumerPlanKind(serviceConfig, environment);
|
||||
const planTarget = serviceConfig === null ? null : artifactConsumerPlanTarget(serviceConfig, environment);
|
||||
const runtimeSecrets = serviceConfig === null ? undefined : redactedSecretContractForService(config, serviceConfig, environment);
|
||||
const unsupportedReason = unsupported ? unsupportedEnvironmentPlanReason(service.id, environment) : null;
|
||||
const effectiveTarget = unsupported
|
||||
? unsupportedPlanTarget(service.id, environment, unsupportedReason ?? "unsupported")
|
||||
@@ -2992,6 +3014,7 @@ function environmentDryRunPlan(
|
||||
noRuntimeSourceBuild: unsupported || planKind !== "d601-dev-target-side-build",
|
||||
dryRunOnly: unsupported || (environment === "prod" && prodArtifactLiveApplyBlockedServiceIds.has(service.id)) || dryRunBlockedReason !== null,
|
||||
blockedReason: unsupportedReason ?? dryRunBlockedReason ?? (environment === "prod" ? prodArtifactLiveApplyBlockedServiceIds.get(service.id) ?? null : null),
|
||||
runtimeSecrets,
|
||||
},
|
||||
target: effectiveTarget,
|
||||
validation: unsupported
|
||||
|
||||
Reference in New Issue
Block a user