From edfddd2445b981474fe6c48abb2859ae950e8af5 Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Fri, 26 Jun 2026 01:14:38 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20YAML-first=20=E6=B2=BB=E7=90=86=20CI/CD?= =?UTF-8?q?=20target=20(#919)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: specify cicd yaml target governance * fix: resolve cicd targets from yaml --------- Co-authored-by: Codex --- config/artifact-registry.yaml | 63 +++++ config/cicd/targets.yaml | 35 +++ config/hwlab-node-lanes.yaml | 1 + config/platform-infra/observability.yaml | 21 +- .../specs/PJ2026-010603-yaml-first-ops.md | 13 + ...60308-cicd-yaml-first-target-governance.md | 234 +++++++++++++++++ .../src/artifact-registry/compose-deploy.ts | 4 +- scripts/src/artifact-registry/entry.ts | 55 ++-- scripts/src/artifact-registry/options.ts | 80 ++++-- scripts/src/artifact-registry/readonly.ts | 10 +- scripts/src/artifact-registry/remote.ts | 5 +- scripts/src/artifact-registry/status.ts | 4 +- scripts/src/artifact-registry/types.ts | 88 +++++-- scripts/src/ci/cleanup.ts | 12 +- scripts/src/ci/entry.ts | 24 +- scripts/src/ci/help.ts | 16 +- scripts/src/ci/install.ts | 5 +- scripts/src/ci/logs.ts | 8 +- scripts/src/ci/publish-preflight.ts | 7 +- scripts/src/ci/publish.ts | 5 +- scripts/src/ci/types.ts | 79 +++--- scripts/src/code-queue-liveness-fixtures.ts | 4 +- scripts/src/deploy/entry.ts | 8 +- scripts/src/deploy/options.ts | 9 +- scripts/src/deploy/service-plan.ts | 11 +- scripts/src/deploy/types.ts | 12 +- scripts/src/hwlab-node-lanes.ts | 20 +- scripts/src/ops/config-refs.ts | 125 ++++++++++ scripts/src/ops/targets.ts | 236 ++++++++++++++++++ .../platform-infra-observability/actions.ts | 3 + .../platform-infra-observability/config.ts | 29 ++- .../platform-infra-observability/summary.ts | 12 + .../src/platform-infra-observability/types.ts | 3 + scripts/src/platform-infra-ops-library.ts | 13 +- scripts/src/platform-infra-wechat-archive.ts | 6 +- 35 files changed, 1079 insertions(+), 181 deletions(-) create mode 100644 config/artifact-registry.yaml create mode 100644 config/cicd/targets.yaml create mode 100644 project-management/PJ2026-01/specs/PJ2026-01060308-cicd-yaml-first-target-governance.md create mode 100644 scripts/src/ops/config-refs.ts create mode 100644 scripts/src/ops/targets.ts diff --git a/config/artifact-registry.yaml b/config/artifact-registry.yaml new file mode 100644 index 00000000..12151328 --- /dev/null +++ b/config/artifact-registry.yaml @@ -0,0 +1,63 @@ +# SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. +version: 1 +kind: UnideskArtifactRegistry +metadata: + name: unidesk-artifact-registry + owner: unidesk + +defaults: + targetId: D601 + +targets: + D601: + providerId: D601 + mode: d601-host-managed + registry: + host: 127.0.0.1 + port: 5000 + endpoint: http://127.0.0.1:5000 + image: registry:2.8.3 + repositoryPrefix: 127.0.0.1:5000/unidesk + runtime: + baseDir: /home/ubuntu/.unidesk/artifact-registry + storageDir: /home/ubuntu/.unidesk/registry-storage + unitName: unidesk-artifact-registry.service + composeProject: unidesk-artifact-registry + serviceName: registry + containerName: unidesk-artifact-registry + timeoutMs: 30000 + source: + repo: https://github.com/pikasTech/unidesk + consumers: + target: + composePullMode: provider-gateway-image-stream + k3sImportMode: d601-native-containerd + defaultEnvironment: prod + catalogRef: scripts/src/artifact-registry/catalog.ts + note: catalogRef is a legacy adapter boundary until service manifests own each consumer. + local: + providerId: local + mode: d601-host-managed-local + registry: + host: 127.0.0.1 + port: 5000 + endpoint: http://127.0.0.1:5000 + image: registry:2.8.3 + repositoryPrefix: 127.0.0.1:5000/unidesk + runtime: + baseDir: /home/ubuntu/.unidesk/artifact-registry + storageDir: /home/ubuntu/.unidesk/registry-storage + unitName: unidesk-artifact-registry.service + composeProject: unidesk-artifact-registry + serviceName: registry + containerName: unidesk-artifact-registry + timeoutMs: 30000 + source: + repo: https://github.com/pikasTech/unidesk + consumers: + target: + composePullMode: provider-gateway-image-stream + k3sImportMode: d601-native-containerd + defaultEnvironment: prod + catalogRef: scripts/src/artifact-registry/catalog.ts + note: local target reuses the D601 host registry facts when the CLI runs on the target host. diff --git a/config/cicd/targets.yaml b/config/cicd/targets.yaml new file mode 100644 index 00000000..8f8bd035 --- /dev/null +++ b/config/cicd/targets.yaml @@ -0,0 +1,35 @@ +# SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. +version: 1 +kind: UnideskCicdTargets +metadata: + name: unidesk-cicd-targets + owner: unidesk + +defaults: + targetId: D601 + +targets: + D601: + providerId: D601 + kubeRoute: D601:k3s + kubeconfig: /etc/rancher/k3s/k3s.yaml + hostCwd: /home/ubuntu + homeDir: /home/ubuntu + pipelineManifest: src/components/microservices/k3sctl-adapter/k3s/ci/unidesk-ci.pipeline.yaml + codeQueueImage: unidesk-code-queue:dev + guardName: d601_native_k3s_guard + requiredNodeName: d601 + artifactRegistry: + configRef: config/artifact-registry.yaml#targets.D601 + G14: + providerId: G14 + kubeRoute: G14:k3s + kubeconfig: /etc/rancher/k3s/k3s.yaml + hostCwd: /root + homeDir: /root + pipelineManifest: src/components/microservices/k3sctl-adapter/k3s/ci/unidesk-ci.pipeline.g14.yaml + codeQueueImage: unidesk-code-queue:dev + guardName: g14_native_k3s_guard + requiredNodeLabel: + key: unidesk.ai/node-id + value: G14 diff --git a/config/hwlab-node-lanes.yaml b/config/hwlab-node-lanes.yaml index 12515a2a..4f88241e 100644 --- a/config/hwlab-node-lanes.yaml +++ b/config/hwlab-node-lanes.yaml @@ -120,6 +120,7 @@ lanes: deployment: hwlab-cloud-api targets: D601: + node: D601 workspace: /home/ubuntu/workspace/hwlab-v03 cicdRepo: /home/ubuntu/workspace/hwlab-v03-cicd.git cicdRepoLock: /tmp/hwlab-v03-cicd-repo.lock diff --git a/config/platform-infra/observability.yaml b/config/platform-infra/observability.yaml index e03b8fed..aed32c0b 100644 --- a/config/platform-infra/observability.yaml +++ b/config/platform-infra/observability.yaml @@ -64,9 +64,10 @@ instrumentation: serviceConnections: - serviceName: hwlab-cloud-api owningRepo: pikasTech/HWLAB - targetNode: D601 - lane: v0.3 - namespace: hwlab-v03 + configRefs: + targetNode: config/hwlab-node-lanes.yaml#lanes.v03.targets.D601.node + lane: config/hwlab-node-lanes.yaml#lanes.v03.version + namespace: config/hwlab-node-lanes.yaml#lanes.v03.targets.D601.runtime.namespace requiredSpans: - POST /v1/agent/chat - durable_admission @@ -77,16 +78,18 @@ instrumentation: - turn_status_read - serviceName: user-billing owningRepo: pikasTech/HWLAB - targetNode: D601 - lane: v0.3 - namespace: hwlab-v03 + configRefs: + targetNode: config/hwlab-node-lanes.yaml#lanes.v03.targets.D601.node + lane: config/hwlab-node-lanes.yaml#lanes.v03.version + namespace: config/hwlab-node-lanes.yaml#lanes.v03.targets.D601.runtime.namespace requiredSpans: - billing_preflight - serviceName: agentrun-manager owningRepo: pikasTech/agentrun - targetNode: D601 - lane: v0.2 - namespace: agentrun-v02 + configRefs: + targetNode: config/agentrun.yaml#controlPlane.lanes.v02.node + lane: config/agentrun.yaml#controlPlane.lanes.v02.version + namespace: config/agentrun.yaml#controlPlane.lanes.v02.runtime.namespace requiredSpans: - agentrun_dispatch - run_created diff --git a/project-management/PJ2026-01/specs/PJ2026-010603-yaml-first-ops.md b/project-management/PJ2026-01/specs/PJ2026-010603-yaml-first-ops.md index a7174138..6c97c554 100644 --- a/project-management/PJ2026-01/specs/PJ2026-010603-yaml-first-ops.md +++ b/project-management/PJ2026-01/specs/PJ2026-010603-yaml-first-ops.md @@ -92,6 +92,7 @@ YAML运维负责 HWLAB/UniDesk 自有平台配置的真相源、解析、渲染 | PJ2026-01060305 | 执行策略 | 本规格 6.5 | AgentRun control-plane default、session policy、provider profile 和 workspace 策略配置 | Agent编排语义、lane 配置 | Agent编排、平台发布 | | PJ2026-01060306 | 公共原语 | 本规格 6.6 | 字段解析、fingerprint、摘要输出、Secret 引用和 YAML path 捕获复用 | 各平台 CLI 实现 | 全部平台运维 CLI | | PJ2026-01060307 | 控制面模块化 | [PJ2026-01060307 控制面模块化](PJ2026-01060307-control-plane-modularity.md) | CI/CD、YAML-first 和平台运维 CLI 源码入口的职责拆分、兼容入口和模块边界 | 发布流水、源码同步、公共原语 | 全部平台运维 CLI | +| PJ2026-01060308 | YAML目标治理 | [PJ2026-01060308 CI/CD YAML目标治理](PJ2026-01060308-cicd-yaml-first-target-governance.md) | CI/CD、HWLAB lane、AgentRun、platform-infra、Secret 和 public exposure 的 target/configRef/sourceRef 解析治理 | 控制面模块化、发布流水、源码同步 | CI、deploy、artifact-registry、HWLAB、AgentRun、platform-infra、secrets | ## 6. 原子需求 @@ -180,3 +181,15 @@ YAML运维应沉淀公共 ops primitive,使字段解析、YAML path 捕获、f YAML运维应约束 CI/CD、YAML-first 和平台基础设施 CLI 的源码入口,使超过 3000 行的控制面文件先按职责拆入领域子目录,再继续沉淀公共 ops primitive。 原 `scripts/src/*.ts` 同名入口应只保留兼容 re-export、命令路由或极薄 adapter。配置解析、manifest 渲染、远端脚本、Secret/public exposure、git-mirror、Tekton/Argo、status summary 和 bounded output 等职责应进入领域模块或共享 helper。新增平台运维逻辑不得继续追加到已超限入口文件。 + +### 6.8 OPS-YAML-REQ-008 CI/CD YAML目标治理 + +| 编号 | 短名 | 主责模块 | 关联模块 | +| --- | --- | --- | --- | +| OPS-YAML-REQ-008 | YAML目标治理 | [PJ2026-01060308 CI/CD YAML目标治理](PJ2026-01060308-cicd-yaml-first-target-governance.md) | [发布流水](PJ2026-010601-controlled-release.md)、[源码同步](PJ2026-010602-source-sync.md)、[控制面模块化](PJ2026-01060307-control-plane-modularity.md) | + +YAML运维应约束 CI、deploy、artifact-registry、HWLAB node/lane、AgentRun、platform-infra、Secret 分发和 public exposure 的目标解析,使 node、lane、target、namespace、route、workspace、registry endpoint、public URL、SecretRef、gitops path 和 CI/CD 参数来自 owning YAML 或显式参数。 + +公共 option parser 不得内置 domain target 默认。未显式传入目标时,调用 domain 可以从自身 YAML default 解析,并必须输出默认来源路径;已显式传入 node/lane/target 时,解析器只校验和解释该目标,不得用全局 default 覆盖。 + +跨 YAML 的运行目标事实应使用 `path/to/file.yaml#object.path` configRef,CLI `plan/status --full` 应显示引用链、resolved target、presence、摘要 hash 和缺失字段,不打印 Secret 值。代码中保留的 `G14`、`D601`、`v02`、`v03` 等目标名必须被 hardcode inventory 分类为 legacy adapter、help example、test fixture 或真实领域特例;隐藏默认必须迁移到 YAML。 diff --git a/project-management/PJ2026-01/specs/PJ2026-01060308-cicd-yaml-first-target-governance.md b/project-management/PJ2026-01/specs/PJ2026-01060308-cicd-yaml-first-target-governance.md new file mode 100644 index 00000000..2a0741a6 --- /dev/null +++ b/project-management/PJ2026-01/specs/PJ2026-01060308-cicd-yaml-first-target-governance.md @@ -0,0 +1,234 @@ +# PJ2026-01060308 CI/CD YAML目标治理 + +## 修改历史 + +| 版本 | 对应 commit id | 更新日期 | 变更说明 | +| --- | --- | --- | --- | + +当前正文仍在规格治理草稿中;未定稿前不新增版本号,不为单次编辑追加 `待提交` 版本。 + +## 正文 + +## PJ2026-01060308 CI/CD YAML目标治理需求规格 + +## 1. 文档控制 + +| 字段 | 内容 | +| --- | --- | +| 编号 | PJ2026-01060308 | +| 短名 | YAML目标治理 | +| 层级 | L3 子课题 | +| 状态 | 已生效 | +| 需求规格模板 | [ISO/IEC/IEEE 29148 需求规格模板](../../templates/iso-iec-ieee-29148-requirements-spec-template.md) | +| 上级规格 | [PJ2026-010603 YAML运维](PJ2026-010603-yaml-first-ops.md) | +| 关联规格 | [PJ2026-010601 发布流水](PJ2026-010601-controlled-release.md)、[PJ2026-010602 源码同步](PJ2026-010602-source-sync.md)、[PJ2026-01060307 控制面模块化](PJ2026-01060307-control-plane-modularity.md)、[PJ2026-010604 公开入口](PJ2026-010604-public-entry.md)、[PJ2026-010605 运维监控](PJ2026-010605-observability-monitoring.md) | +| 实现引用版本 | draft-2026-06-25-cicd-yaml-targets | +| 规格治理索引 | [规格治理](spec-governance.md) | + +本文采用 ISO/IEC/IEEE 29148 需求规格模板的项目裁剪版:正文只保留 CI/CD、YAML-first 和平台运维控制面中 target、lane、Secret、public exposure、PipelineRun/Argo/git-mirror 与公共 ops primitive 的稳定治理口径。 + +## 2. 目的和范围 + +### 2.1 目的 + +YAML目标治理负责把 CI、deploy、artifact-registry、HWLAB node/lane、AgentRun、platform-infra、Secret 分发和 public exposure 中的运行目标事实收敛到 owning YAML 或显式参数,使新增 node/lane/target 时不需要发布 TypeScript 代码来改变默认目标、namespace、route、registry endpoint 或 SecretRef。 + +本规格建立在控制面模块化完成后的领域目录边界上。代码可以保留 legacy adapter、测试夹具、错误消息和帮助示例中的历史目标名,但主路径不得继续通过隐藏默认值把 `G14`、`D601`、`v02`、`v03`、namespace、route、registry endpoint 或工作目录写死在解析逻辑中。 + +### 2.2 范围内 + +- `config/hwlab-node-lanes.yaml`、`config/agentrun.yaml`、`config/platform-infra/*.yaml`、`config/secrets-distribution.yaml`、`config/cicd/targets.yaml` 和 `config/artifact-registry.yaml` 的 target/lane/configRef/sourceRef 职责。 +- `scripts/src/ops/` 中的 configRef、target resolver、Secret redaction、public exposure、K8s/CI-CD 和 bounded output 公共原语。 +- `ci`、`deploy`、`artifact-registry`、`hwlab g14`、`hwlab nodes`、`agentrun`、`platform-infra observability` 和 `secrets` CLI 对目标来源、配置引用图和脱敏状态的解释输出。 +- hardcode inventory 的分类:`remove-code-default`、`keep-domain-special`、`legacy-adapter`、`test-fixture` 和 `help-example`。 + +### 2.3 范围外 + +- 不把所有配置合并成单一超级 YAML。 +- 不删除必要的 legacy 安全阻断;legacy 入口必须隔离并在输出中标注。 +- 不把运行面 Secret、pod env、日志或数据库状态反向写成本地配置真相。 +- 不绕过 UniDesk 受控 CLI 使用原生 `kubectl`、`argo`、`tkn`、`curl` 或临时 shell 作为正式控制入口。 +- 不为业务策略数值新增合同测试、硬编码范围或 schema 数值限制。 + +## 3. 术语表 + +| 术语 | 定义 | +| --- | --- | +| target | YAML 中描述部署目标、运行节点、route、kubeRoute、namespace、workspace、registry、public URL 或 CI/CD 参数的对象。 | +| lane | YAML 中描述一条 runtime 或 control-plane 运行线的对象,通常包含 node、source truth、runtime namespace、gitops、CI、Secret 和 publicExposure。 | +| configRef | `path/to/file.yaml#object.path` 形式的跨 YAML 引用,解析器只校验引用存在性、类型和摘要,不把合并结果写成第二真相。 | +| sourceRef | Secret 来源声明。输出只能显示来源标识、key 名、presence、fingerprint、字节数和 `valuesPrinted=false`。 | +| hidden default | 代码在未显式参数或未读取 YAML 时自动选择固定目标、namespace、route、registry、lane 或工作目录。 | +| legacy adapter | 为历史入口保留的隔离兼容路径。它可以拒绝 mutation 或提示替代命令,但不得污染主 runtime lane 解析。 | +| ops primitive | 跨领域复用的配置引用、目标解析、Secret redaction、K8s apply/status、public exposure 和 bounded output helper。 | + +## 4. 系统边界和接口 + +| 边界项 | 内容 | +| --- | --- | +| 外部使用者 | 平台维护者、发布操作人员、AgentRun/HWLAB lane 运维者、自动化任务和后续代码维护者。 | +| 外部输入 | CLI 参数、YAML 配置、configRef/sourceRef、node/lane/target、desired-state manifest、Git commit 和运行面只读状态。 | +| 受控资源 | target resolver、配置引用图、CI/CD manifest 渲染、Secret 同步摘要、public exposure 渲染、PipelineRun/Argo/git-mirror 状态和 CLI 输出。 | +| 外部输出 | 有界 plan/status/dry-run、配置来源路径、resolved target、redacted Secret evidence、drill-down 命令和 legacy adapter 提示。 | +| 用户接口 | `bun scripts/cli.ts ci ...`、`artifact-registry ...`、`deploy ...`、`hwlab ...`、`agentrun ...`、`platform-infra observability ...` 和 `secrets ...`。 | +| 系统边界 | 本规格定义配置如何成为运行目标真相;不定义业务能力语义、不替代发布/源码/公开入口/监控规格,也不让运行面状态成为配置来源。 | + +## 5. 内部分工与架构 + +### 5.1 配置引用图 + +```mermaid +flowchart LR + CICD[config/cicd/targets.yaml] --> OpsTarget[ops/targets.ts] + Artifact[config/artifact-registry.yaml] --> OpsTarget + Hwlab[config/hwlab-node-lanes.yaml] --> OpsTarget + AgentRun[config/agentrun.yaml] --> OpsTarget + Observability[config/platform-infra/observability.yaml] --> ConfigRef[ops/config-refs.ts] + Secrets[config/secrets-distribution.yaml] --> OpsSecret[ops/secrets.ts] + ConfigRef --> Hwlab + ConfigRef --> AgentRun + OpsTarget --> DomainCLI[domain CLI plan/status/dry-run] + OpsSecret --> DomainCLI + DomainCLI --> Runtime[controlled runtime or bounded output] +``` + +### 5.2 target/lane 解析数据流 + +```mermaid +flowchart TD + Args[explicit --target/--node/--lane] --> Select[selection] + Defaults[YAML defaults] --> Select + Select --> Resolver[ops target resolver] + Resolver --> Validate[shape and reference validation] + Validate --> Plan[resolved target summary] + Plan --> Domain[CI/deploy/artifact/HWLAB/AgentRun/platform-infra] + Domain --> Output[bounded summary + drill-down] +``` + +命令行或 issue 已明确 node/lane/target 时,resolver 只校验和解释该目标,不得用全局 default 覆盖。未显式传入目标时,resolver 可以读取 domain YAML default,并在输出中说明默认来源路径。 + +### 5.3 Secret sourceRef 同步链路 + +```mermaid +sequenceDiagram + participant YAML as owning YAML + participant CLI as controlled CLI + participant Source as local sourceRef + participant Runtime as runtime Secret + YAML->>CLI: sourceRef + sourceKey + targetKey + CLI->>Source: inspect presence and fingerprint + Source-->>CLI: presence/fingerprint only + CLI-->>CLI: valuesPrinted=false + CLI->>Runtime: sync only when --confirm + Runtime-->>CLI: object/key presence + CLI-->>YAML: no reverse write-back +``` + +缺少 sourceRef、targetKey、providerCredential 或 tool credential 时,CLI 必须暴露 YAML/AipodSpec binding 缺口,不得通过复制其他 lane Secret、手工创建 legacy Secret 或 patch runtime namespace 规避。 + +### 5.4 publicExposure/Caddy/FRPC 链路 + +```mermaid +flowchart LR + PublicYaml[publicExposure YAML] --> PublicHelper[ops/public-exposure.ts] + PublicHelper --> Frpc[FRPC Secret/render] + PublicHelper --> Caddy[PK01 Caddy managed block] + PublicHelper --> Probe[HTTPS public health probe] + Probe --> Summary[public URL + diagnostic endpoint summary] +``` + +正式用户入口来自 YAML 中的 public URL 和 publicExposure;FRP remote port、Caddy backend、本地 service DNS、direct port 或历史 nip.io 域名只能作为实现细节、诊断入口或 legacy 对照。 + +### 5.5 CI PipelineRun/Argo/git-mirror 状态链路 + +```mermaid +sequenceDiagram + participant CLI + participant Target as YAML target + participant Mirror as git-mirror + participant Tekton + participant Argo + participant Runtime + CLI->>Target: resolve CI/CD target + CLI->>Mirror: read/sync/flush via controlled entry + Mirror-->>CLI: refs and pendingFlush summary + CLI->>Tekton: PipelineRun status or dry-run manifest + Tekton-->>CLI: PipelineRun/TaskRun bounded summary + CLI->>Argo: Application sync/health status + Argo-->>Runtime: desired runtime state + Runtime-->>CLI: readiness/public probe summary +``` + +状态输出必须区分 PR merge commit、current source head、PipelineRun、Argo revision、git-mirror pendingFlush 和 runtime readiness;不得只用一个当前 head 推断所有阶段完成。 + +## 6. 原子需求 + +### 6.1 OPS-TARGET-REQ-001 YAML target 真相 + +| 编号 | 短名 | 主责模块 | 关联模块 | +| --- | --- | --- | --- | +| OPS-TARGET-REQ-001 | 目标真相 | PJ2026-01060308 YAML目标治理 | [YAML运维](PJ2026-010603-yaml-first-ops.md) | + +CI/CD、deploy、artifact-registry、HWLAB node/lane、AgentRun 和 platform-infra 的 target、lane、namespace、route、workspace、registry endpoint、public URL、SecretRef、gitops path 和 CI/CD 参数必须来自 owning YAML 或显式参数。 + +代码不得在主路径中新增隐藏默认 `G14`、`D601`、`v02`、`v03`、namespace、route、registry endpoint 或工作目录。缺少配置时应报出 YAML 路径、字段名和下一步配置入口。 + +### 6.2 OPS-TARGET-REQ-002 configRef 解析 + +| 编号 | 短名 | 主责模块 | 关联模块 | +| --- | --- | --- | --- | +| OPS-TARGET-REQ-002 | 配置引用 | PJ2026-01060308 YAML目标治理 | [公共原语](PJ2026-010603-yaml-first-ops.md#66-ops-yaml-req-006-公共-ops-primitive) | + +跨 YAML 目标事实必须通过 `path/to/file.yaml#object.path` configRef 表达。configRef parser 只负责读取、校验存在性、生成摘要 hash、展示引用链和暴露缺失字段;不得把解析后的大对象写回另一个 YAML 成为第二真相。 + +`platform-infra observability` 的 serviceConnection 不得复制 HWLAB/AgentRun 的 node/lane/namespace 事实,必须通过 configRef 指向对应 lane 配置,并在 `plan/status --full` 中展示解析链。 + +### 6.3 OPS-TARGET-REQ-003 公共 resolver 和 domain 注入默认 + +| 编号 | 短名 | 主责模块 | 关联模块 | +| --- | --- | --- | --- | +| OPS-TARGET-REQ-003 | resolver | PJ2026-01060308 YAML目标治理 | [控制面模块化](PJ2026-01060307-control-plane-modularity.md) | + +公共 option parser 不允许内置 domain target 默认。公共 parser 可以返回 `targetId: null`,由调用 domain 把 YAML default 注入并输出 default 来源路径。`ciTarget()`、deploy provider resolver、artifact-registry options 和 platform-infra common options 都必须遵循该规则。 + +### 6.4 OPS-TARGET-REQ-004 Secret 与 credential 脱敏一致性 + +| 编号 | 短名 | 主责模块 | 关联模块 | +| --- | --- | --- | --- | +| OPS-TARGET-REQ-004 | Secret脱敏 | PJ2026-01060308 YAML目标治理 | [用户管理](PJ2026-0105-user-management.md)、[Agent编排](PJ2026-0102-agent-orchestration.md) | + +Secret plan/status 只允许输出对象名、key 名、sourceRef、targetKey、presence、fingerprint、字节数、缺失项和 `valuesPrinted=false`。AgentRun provider credential、tool credential、HWLAB bootstrap Secret、platform-infra Secret 和通用 `secrets` 命令必须使用同一类 redacted sourceRef/fingerprint 语义。 + +### 6.5 OPS-TARGET-REQ-005 legacy adapter 隔离 + +| 编号 | 短名 | 主责模块 | 关联模块 | +| --- | --- | --- | --- | +| OPS-TARGET-REQ-005 | legacy隔离 | PJ2026-01060308 YAML目标治理 | [发布流水](PJ2026-010601-controlled-release.md) | + +D601 maintenance deploy、G14 DEV/PROD retirement、v02-only runtime migration、Code Queue compat path 等历史入口可以保留为 legacy adapter,但必须在代码和输出中标注原因、替代命令和是否允许 mutation。legacy adapter 不得成为主 runtime lane 的 fallback,也不得补齐缺失 YAML/Secret。 + +### 6.6 OPS-TARGET-REQ-006 hardcode inventory 与验收 + +| 编号 | 短名 | 主责模块 | 关联模块 | +| --- | --- | --- | --- | +| OPS-TARGET-REQ-006 | inventory | PJ2026-01060308 YAML目标治理 | [平台运维](PJ2026-0106-platform-ops.md) | + +每轮治理必须维护 hardcode inventory,并把命中分类为 `remove-code-default`、`keep-domain-special`、`legacy-adapter`、`test-fixture` 或 `help-example`。代码中保留的目标名必须属于后四类之一;`remove-code-default` 必须迁移到 YAML 或公共 helper。 + +验收至少覆盖 `ci plan/status --target `、`artifact-registry plan/status --target `、`deploy apply --dry-run` 目标来源说明、`hwlab nodes control-plane status --node --lane ` 的 YAML-declared lane、`platform-infra observability plan/status --full` 的 configRef 解析链和 `secrets status --config ... --full` 的脱敏输出。 + +## 7. 过程控制 + +本规格的执行 issue 为 [#911](https://github.com/pikasTech/unidesk/issues/911)。源码文件头部应标注 `SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets`;自动生成文件、纯配置、锁文件和无法承载注释头的二进制产物可例外。 + +首轮 hardcode inventory: + +| 命中 | 分类 | 治理动作 | +| --- | --- | --- | +| `ciTarget()`、`d601ProviderId`、CI pipeline manifest、node label、host/root/workspace | remove-code-default | 迁入 `config/cicd/targets.yaml`,CI resolver 读取 YAML。 | +| `artifact-registry` 默认 D601、`127.0.0.1:5000`、runtime image、storageDir | remove-code-default | 迁入 `config/artifact-registry.yaml`。 | +| `deploy` artifact consumer provider 默认 D601 | remove-code-default | 从 artifact-registry YAML default 解析;compat path 显式 legacy。 | +| `HwlabRuntimeLane = "v02" | "v03"` 和 `isSupportedLaneId()` | remove-code-default | lane id 改为 YAML 声明 string,新增 lane 不要求改 TypeScript union。 | +| `platform-infra observability` 复制 `targetNode/lane/namespace` | remove-code-default | 改为 serviceConnection configRef 并输出解析链。 | +| D601 maintenance deploy、Code Queue compat path、v02 runtime migration | legacy-adapter | 保留安全阻断和替代命令,不作为主路径默认。 | +| 帮助示例、错误消息、历史说明中的 G14/D601/v02/v03 | help-example | 允许保留,必要时补 `configTruth` 或 legacy 标注。 | diff --git a/scripts/src/artifact-registry/compose-deploy.ts b/scripts/src/artifact-registry/compose-deploy.ts index de0ef8fa..26a5955a 100644 --- a/scripts/src/artifact-registry/compose-deploy.ts +++ b/scripts/src/artifact-registry/compose-deploy.ts @@ -1,4 +1,5 @@ // SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. compose-deploy module for scripts/src/artifact-registry.ts. +// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. // Moved mechanically from scripts/src/artifact-registry.ts:2579-3153 for #903. @@ -23,7 +24,7 @@ import { import { d601K3sGuardShellLines } from "../d601-k3s-guard"; import { composeRuntimeEnvValue } from "../runtime-env"; -import type { ArtifactConsumerSpec, ArtifactConsumerTarget, ArtifactRegistryOptions } from "./types"; +import { artifactRegistryOptionsTargetSummary, type ArtifactConsumerSpec, type ArtifactConsumerTarget, type ArtifactRegistryOptions } from "./types"; import { composeArtifactEnvValues, registryArtifactMissingMessage, registryArtifactProbeScript, syntheticComposeHealthDeployScript, verifyLocalArtifactLabels } from "./artifact-probe"; import { base64, safeName, shellQuote } from "./bundle"; import { artifactConsumerSpecs } from "./catalog"; @@ -439,6 +440,7 @@ export function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spe error: verificationBlocked ? "runtime-verification-blocked" : undefined, environment, providerId: effectiveOptions.providerId, + artifactRegistryTarget: artifactRegistryOptionsTargetSummary(effectiveOptions), serviceId: spec.serviceId, commit, sourceRepo: sourceRepoFor(effectiveOptions, spec), diff --git a/scripts/src/artifact-registry/entry.ts b/scripts/src/artifact-registry/entry.ts index c0aa4f68..eb854e25 100644 --- a/scripts/src/artifact-registry/entry.ts +++ b/scripts/src/artifact-registry/entry.ts @@ -1,4 +1,5 @@ // SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. entry module for scripts/src/artifact-registry.ts. +// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. // Moved mechanically from scripts/src/artifact-registry.ts:3448-3700 for #903. @@ -85,33 +86,33 @@ export function localHelp(): Record { command: "artifact-registry plan|render|status|health|install|deploy-backend-core|deploy-service", output: "json", usage: [ - "bun scripts/cli.ts artifact-registry plan [--provider-id D601]", - "bun scripts/cli.ts artifact-registry render [--provider-id D601]", - "bun scripts/cli.ts artifact-registry status [--provider-id D601]", - "bun scripts/cli.ts artifact-registry health [--provider-id D601]", - "bun scripts/cli.ts artifact-registry install [--provider-id D601]", - "bun scripts/cli.ts artifact-registry deploy-service --service baidu-netdisk --commit [--dry-run] [--run-now] [--provider-id D601]", - "bun scripts/cli.ts artifact-registry deploy-service --service frontend --env prod --commit [--dry-run] [--run-now] [--provider-id D601]", - "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] [--run-now] [--provider-id D601]", - "bun scripts/cli.ts artifact-registry deploy-service --env dev --service todo-note --commit [--dry-run] [--run-now] [--provider-id D601]", - "bun scripts/cli.ts artifact-registry deploy-service --env dev --service findjob --commit [--dry-run] [--run-now] [--provider-id D601]", - "bun scripts/cli.ts artifact-registry deploy-service --env prod --service findjob --commit [--dry-run] [--run-now] [--provider-id D601]", - "bun scripts/cli.ts artifact-registry deploy-service --env dev --service pipeline --commit [--dry-run] [--run-now] [--provider-id D601]", - "bun scripts/cli.ts artifact-registry deploy-service --env prod --service pipeline --commit [--dry-run] [--run-now] [--provider-id D601]", - "bun scripts/cli.ts artifact-registry deploy-service --env dev --service met-nonlinear --commit --dry-run [--provider-id D601]", - "bun scripts/cli.ts artifact-registry deploy-service --env prod --service met-nonlinear --commit --dry-run [--provider-id D601]", - "bun scripts/cli.ts artifact-registry deploy-service --env prod --service k3sctl-adapter --commit --dry-run [--provider-id D601]", - "bun scripts/cli.ts artifact-registry deploy-service --env dev --service mdtodo --commit [--dry-run] [--run-now] [--provider-id D601]", - "bun scripts/cli.ts artifact-registry deploy-service --env prod --service mdtodo --commit [--dry-run] [--run-now] [--provider-id D601]", - "bun scripts/cli.ts artifact-registry deploy-service --env dev --service claudeqq --commit [--dry-run] [--run-now] [--provider-id D601]", - "bun scripts/cli.ts artifact-registry deploy-service --env prod --service claudeqq --commit [--dry-run] [--run-now] [--provider-id D601]", - "bun scripts/cli.ts artifact-registry deploy-service --env dev --service code-queue --commit --dry-run [--provider-id D601]", + "bun scripts/cli.ts artifact-registry plan [--target D601]", + "bun scripts/cli.ts artifact-registry render [--target D601]", + "bun scripts/cli.ts artifact-registry status [--target D601]", + "bun scripts/cli.ts artifact-registry health [--target D601]", + "bun scripts/cli.ts artifact-registry install [--target D601]", + "bun scripts/cli.ts artifact-registry deploy-service --service baidu-netdisk --commit [--dry-run] [--run-now] [--target D601]", + "bun scripts/cli.ts artifact-registry deploy-service --service frontend --env prod --commit [--dry-run] [--run-now] [--target D601]", + "bun scripts/cli.ts artifact-registry deploy-service --service frontend --env dev --commit [--dry-run] [--run-now] [--target D601]", + "bun scripts/cli.ts artifact-registry deploy-service --env dev --service decision-center --commit [--dry-run] [--run-now] [--target D601]", + "bun scripts/cli.ts artifact-registry deploy-service --env prod --service decision-center --commit [--dry-run] [--run-now] [--target D601]", + "bun scripts/cli.ts artifact-registry deploy-service --env prod --service project-manager --commit [--dry-run] [--run-now] [--target D601]", + "bun scripts/cli.ts artifact-registry deploy-service --env prod --service oa-event-flow --commit [--dry-run] [--run-now] [--target D601]", + "bun scripts/cli.ts artifact-registry deploy-service --env prod --service code-queue-mgr --commit --dry-run [--target D601]", + "bun scripts/cli.ts artifact-registry deploy-service --env prod --service todo-note --commit [--dry-run] [--run-now] [--target D601]", + "bun scripts/cli.ts artifact-registry deploy-service --env dev --service todo-note --commit [--dry-run] [--run-now] [--target D601]", + "bun scripts/cli.ts artifact-registry deploy-service --env dev --service findjob --commit [--dry-run] [--run-now] [--target D601]", + "bun scripts/cli.ts artifact-registry deploy-service --env prod --service findjob --commit [--dry-run] [--run-now] [--target D601]", + "bun scripts/cli.ts artifact-registry deploy-service --env dev --service pipeline --commit [--dry-run] [--run-now] [--target D601]", + "bun scripts/cli.ts artifact-registry deploy-service --env prod --service pipeline --commit [--dry-run] [--run-now] [--target D601]", + "bun scripts/cli.ts artifact-registry deploy-service --env dev --service met-nonlinear --commit --dry-run [--target D601]", + "bun scripts/cli.ts artifact-registry deploy-service --env prod --service met-nonlinear --commit --dry-run [--target D601]", + "bun scripts/cli.ts artifact-registry deploy-service --env prod --service k3sctl-adapter --commit --dry-run [--target D601]", + "bun scripts/cli.ts artifact-registry deploy-service --env dev --service mdtodo --commit [--dry-run] [--run-now] [--target D601]", + "bun scripts/cli.ts artifact-registry deploy-service --env prod --service mdtodo --commit [--dry-run] [--run-now] [--target D601]", + "bun scripts/cli.ts artifact-registry deploy-service --env dev --service claudeqq --commit [--dry-run] [--run-now] [--target D601]", + "bun scripts/cli.ts artifact-registry deploy-service --env prod --service claudeqq --commit [--dry-run] [--run-now] [--target D601]", + "bun scripts/cli.ts artifact-registry deploy-service --env dev --service code-queue --commit --dry-run [--target D601]", ], firstStage: "install now writes the rendered systemd/Compose/config files and starts the registry", artifactConsumers: { diff --git a/scripts/src/artifact-registry/options.ts b/scripts/src/artifact-registry/options.ts index a7fda341..8773ee15 100644 --- a/scripts/src/artifact-registry/options.ts +++ b/scripts/src/artifact-registry/options.ts @@ -1,4 +1,5 @@ // SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. options module for scripts/src/artifact-registry.ts. +// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. // Moved mechanically from scripts/src/artifact-registry.ts:881-978 for #903. @@ -24,7 +25,8 @@ import { d601K3sGuardShellLines } from "../d601-k3s-guard"; import { composeRuntimeEnvValue } from "../runtime-env"; import type { ArtifactDeployEnvironment, ArtifactRegistryOptions } from "./types"; -import { defaultOptions } from "./types"; +import { artifactRegistryOptionsFromTarget } from "./types"; +import { resolveArtifactRegistryTarget } from "../ops/targets"; export function isHelpArg(value: string | undefined): boolean { return value === undefined || value === "help" || value === "--help" || value === "-h"; @@ -60,65 +62,93 @@ export function environmentValue(value: string, option: string): ArtifactDeployE } export function parseArtifactRegistryOptions(args: string[]): ArtifactRegistryOptions { - const options = { ...defaultOptions }; + let targetSelection: string | null = null; + let environment: ArtifactDeployEnvironment | null = null; + let host: string | null = null; + let port: number | null = null; + let image: string | null = null; + let baseDir: string | null = null; + let storageDir: string | null = null; + let timeoutMs: number | null = null; + let dryRun = false; + let runNow = false; + let commit: string | null = null; + let targetImage: string | null = null; + let serviceId: string | null = null; + let sourceRepo: string | null = null; + let sourceRepoExplicit = false; + let deployRef: string | null = null; + let deployJsonService: ArtifactRegistryOptions["deployJsonService"] = null; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (arg === "--dry-run") { - options.dryRun = true; + dryRun = true; } else if (arg === "--run-now") { - options.runNow = true; + runNow = true; } else if (arg === "--env" || arg === "--environment") { - const environment = requireValue(args, index, arg); - if (environment !== "dev" && environment !== "prod") throw new Error(`${arg} must be dev or prod`); - options.environment = environment; + environment = environmentValue(requireValue(args, index, arg), arg); index += 1; - } else if (arg === "--provider-id") { - options.providerId = requireValue(args, index, arg); + } else if (arg === "--target" || arg === "--provider-id") { + targetSelection = requireValue(args, index, arg); index += 1; } else if (arg === "--host") { - options.host = requireValue(args, index, arg); + host = requireValue(args, index, arg); index += 1; } else if (arg === "--port") { - options.port = positiveInt(requireValue(args, index, arg), arg); + port = positiveInt(requireValue(args, index, arg), arg); index += 1; } else if (arg === "--image") { - options.image = requireValue(args, index, arg); + image = requireValue(args, index, arg); index += 1; } else if (arg === "--base-dir") { - options.baseDir = absolutePath(requireValue(args, index, arg), arg); + baseDir = absolutePath(requireValue(args, index, arg), arg); index += 1; } else if (arg === "--storage-dir") { - options.storageDir = absolutePath(requireValue(args, index, arg), arg); + storageDir = absolutePath(requireValue(args, index, arg), arg); index += 1; } else if (arg === "--timeout-ms") { - options.timeoutMs = positiveInt(requireValue(args, index, arg), arg); + timeoutMs = positiveInt(requireValue(args, index, arg), arg); index += 1; } else if (arg === "--commit") { - options.commit = commitValue(requireValue(args, index, arg), arg); + commit = commitValue(requireValue(args, index, arg), arg); index += 1; } else if (arg === "--target-image") { - options.targetImage = requireValue(args, index, arg); + targetImage = requireValue(args, index, arg); index += 1; } else if (arg === "--service" || arg === "--service-id") { - options.serviceId = requireValue(args, index, arg); + serviceId = requireValue(args, index, arg); index += 1; } else if (arg === "--source-repo") { - options.sourceRepo = requireValue(args, index, arg); - options.sourceRepoExplicit = true; + sourceRepo = requireValue(args, index, arg); + sourceRepoExplicit = true; index += 1; } else if (arg === "--deploy-ref") { - options.deployRef = requireValue(args, index, arg); + deployRef = requireValue(args, index, arg); index += 1; } else if (arg === "--deploy-json-service") { - options.deployJsonService = parseDeployJsonServiceContractBase64(requireValue(args, index, arg)); - index += 1; - } else if (arg === "--env" || arg === "--environment") { - options.environment = environmentValue(requireValue(args, index, arg), arg); + deployJsonService = parseDeployJsonServiceContractBase64(requireValue(args, index, arg)); index += 1; } else { throw new Error(`unknown artifact-registry option: ${arg}`); } } + const options = artifactRegistryOptionsFromTarget(resolveArtifactRegistryTarget(targetSelection)); + options.environment = environment; + options.host = host ?? options.host; + options.port = port ?? options.port; + options.image = image ?? options.image; + options.baseDir = baseDir ?? options.baseDir; + options.storageDir = storageDir ?? options.storageDir; + options.timeoutMs = timeoutMs ?? options.timeoutMs; + options.dryRun = dryRun; + options.runNow = runNow; + options.commit = commit; + options.targetImage = targetImage; + options.serviceId = serviceId; + options.sourceRepo = sourceRepo ?? options.sourceRepo; + options.sourceRepoExplicit = sourceRepoExplicit; + options.deployRef = deployRef; + options.deployJsonService = deployJsonService; if (options.host !== "127.0.0.1") throw new Error("--host is first-stage restricted to 127.0.0.1"); if (options.port !== 5000) throw new Error("--port is first-stage restricted to 5000"); return options; diff --git a/scripts/src/artifact-registry/readonly.ts b/scripts/src/artifact-registry/readonly.ts index b3946f1e..188f7e8d 100644 --- a/scripts/src/artifact-registry/readonly.ts +++ b/scripts/src/artifact-registry/readonly.ts @@ -1,4 +1,5 @@ // SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. readonly module for scripts/src/artifact-registry.ts. +// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. // Moved mechanically from scripts/src/artifact-registry.ts:1934-2116 for #903. @@ -23,7 +24,7 @@ import { import { d601K3sGuardShellLines } from "../d601-k3s-guard"; import { composeRuntimeEnvValue } from "../runtime-env"; -import type { ArtifactRegistryCommandRuntime, ArtifactRegistryOptions, ArtifactRegistryReadonlyProbe } from "./types"; +import { artifactRegistryOptionsTargetSummary, type ArtifactRegistryCommandRuntime, type ArtifactRegistryOptions, type ArtifactRegistryReadonlyProbe } from "./types"; import { renderBundle } from "./bundle"; import { commandTail } from "./consumer"; import { annotateRemoteFirstReadonlyResult, annotateRemoteReadonlyResult, controlPlaneMissingResult, parsedCliData, readonlyCommandFailureResult, remoteFrontendHostFromEnv, runRemoteScript, unwrapRemoteArtifactRegistryResult } from "./remote"; @@ -41,7 +42,7 @@ export function artifactRegistryReadonlyAutoRemotePlan( providerId: options.providerId, host: remoteHost, transport: "frontend", - command: remoteHost === null ? null : `bun scripts/cli.ts --main-server-ip ${remoteHost} artifact-registry ${action} --provider-id ${options.providerId}`, + command: remoteHost === null ? null : `bun scripts/cli.ts --main-server-ip ${remoteHost} artifact-registry ${action} --target ${options.targetId}`, failureClassification: remoteHost === null ? "control-plane-missing" : null, }; } @@ -81,8 +82,8 @@ export function runReadonlyStatusViaRemoteFrontend( remoteHost, "artifact-registry", action, - "--provider-id", - options.providerId, + "--target", + options.targetId, "--timeout-ms", String(options.timeoutMs), ]; @@ -146,6 +147,7 @@ export function statusFromValues(options: ArtifactRegistryOptions, values: Recor installed, healthy, ...decision, + target: artifactRegistryOptionsTargetSummary(options), remoteCommandShape: readonlyRemoteCommandShape(healthMode ? "health" : "status", options), checks, observed: { diff --git a/scripts/src/artifact-registry/remote.ts b/scripts/src/artifact-registry/remote.ts index 6cd5de3c..e55d6ee0 100644 --- a/scripts/src/artifact-registry/remote.ts +++ b/scripts/src/artifact-registry/remote.ts @@ -1,4 +1,5 @@ // SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. remote module for scripts/src/artifact-registry.ts. +// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. // Moved mechanically from scripts/src/artifact-registry.ts:1659-1933 for #903. @@ -228,8 +229,8 @@ export function controlPlaneMissingResult( localBackendCoreMissing: true, classification: "control-plane-missing", retryCommand: remoteHost === null - ? `bun scripts/cli.ts --main-server-ip artifact-registry ${action} --provider-id ${options.providerId}` - : `bun scripts/cli.ts --main-server-ip ${remoteHost} artifact-registry ${action} --provider-id ${options.providerId}`, + ? `bun scripts/cli.ts --main-server-ip artifact-registry ${action} --target ${options.targetId}` + : `bun scripts/cli.ts --main-server-ip ${remoteHost} artifact-registry ${action} --target ${options.targetId}`, }, localObservation: localResult, remoteObservation: remoteResult, diff --git a/scripts/src/artifact-registry/status.ts b/scripts/src/artifact-registry/status.ts index 7c98eb84..73ce55a5 100644 --- a/scripts/src/artifact-registry/status.ts +++ b/scripts/src/artifact-registry/status.ts @@ -1,4 +1,5 @@ // SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. status module for scripts/src/artifact-registry.ts. +// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. // Moved mechanically from scripts/src/artifact-registry.ts:1081-1318 for #903. @@ -23,13 +24,14 @@ import { import { d601K3sGuardShellLines } from "../d601-k3s-guard"; import { composeRuntimeEnvValue } from "../runtime-env"; -import type { ArtifactRegistryFailureClassification, ArtifactRegistryOptions, RenderedBundle } from "./types"; +import { artifactRegistryOptionsTargetSummary, type ArtifactRegistryFailureClassification, type ArtifactRegistryOptions, type RenderedBundle } from "./types"; import { renderBundle, shellQuote } from "./bundle"; export function plan(options: ArtifactRegistryOptions): Record { const bundle = renderBundle(options); return { ok: true, + target: artifactRegistryOptionsTargetSummary(options), providerId: options.providerId, mode: "d601-host-managed", firstStage: true, diff --git a/scripts/src/artifact-registry/types.ts b/scripts/src/artifact-registry/types.ts index 27f0a361..5b7e9685 100644 --- a/scripts/src/artifact-registry/types.ts +++ b/scripts/src/artifact-registry/types.ts @@ -1,4 +1,5 @@ // SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. types module for scripts/src/artifact-registry.ts. +// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. // Moved mechanically from scripts/src/artifact-registry.ts:1-248 for #903. @@ -22,6 +23,7 @@ import { } from "../deploy-json-contract"; import { d601K3sGuardShellLines } from "../d601-k3s-guard"; import { composeRuntimeEnvValue } from "../runtime-env"; +import { artifactRegistryTargetSummary, resolveArtifactRegistryTarget, type TargetConfigSource } from "../ops/targets"; import { composeFile, sha256 } from "./bundle"; @@ -30,11 +32,14 @@ export type ArtifactRegistryAction = "plan" | "render" | "status" | "health" | " export type ArtifactDeployEnvironment = "prod" | "dev"; export interface ArtifactRegistryOptions { + targetId: string; environment: ArtifactDeployEnvironment | null; providerId: string; + mode: string; host: string; port: number; image: string; + repositoryPrefix: string; baseDir: string; storageDir: string; unitName: string; @@ -51,6 +56,9 @@ export interface ArtifactRegistryOptions { sourceRepoExplicit: boolean; deployRef: string | null; deployJsonService: DeployJsonServiceContract | null; + consumerTarget: Record; + consumerCatalogRef: string; + configSource: TargetConfigSource; } export interface ArtifactRegistryCommandRuntime { @@ -96,29 +104,63 @@ export type ArtifactRegistryFailureClassification = | "registry-not-installed" | "registry-unhealthy"; -export const defaultOptions: ArtifactRegistryOptions = { - environment: null, - providerId: "D601", - host: "127.0.0.1", - port: 5000, - image: "registry:2.8.3", - baseDir: "/home/ubuntu/.unidesk/artifact-registry", - storageDir: "/home/ubuntu/.unidesk/registry-storage", - unitName: "unidesk-artifact-registry.service", - composeProject: "unidesk-artifact-registry", - serviceName: "registry", - containerName: "unidesk-artifact-registry", - timeoutMs: 30_000, - dryRun: false, - runNow: false, - commit: null, - targetImage: null, - serviceId: null, - sourceRepo: "https://github.com/pikasTech/unidesk", - sourceRepoExplicit: false, - deployRef: null, - deployJsonService: null, -}; +export function artifactRegistryOptionsFromTarget(target = resolveArtifactRegistryTarget(null)): ArtifactRegistryOptions { + return { + targetId: target.targetId, + environment: null, + providerId: target.providerId, + mode: target.mode, + host: target.host, + port: target.port, + image: target.image, + repositoryPrefix: target.repositoryPrefix, + baseDir: target.baseDir, + storageDir: target.storageDir, + unitName: target.unitName, + composeProject: target.composeProject, + serviceName: target.serviceName, + containerName: target.containerName, + timeoutMs: target.timeoutMs, + dryRun: false, + runNow: false, + commit: null, + targetImage: null, + serviceId: null, + sourceRepo: target.sourceRepo, + sourceRepoExplicit: false, + deployRef: null, + deployJsonService: null, + consumerTarget: target.consumerTarget, + consumerCatalogRef: target.consumerCatalogRef, + configSource: target.configSource, + }; +} + +export const defaultOptions: ArtifactRegistryOptions = artifactRegistryOptionsFromTarget(); + +export function artifactRegistryOptionsTargetSummary(options: ArtifactRegistryOptions): Record { + return artifactRegistryTargetSummary({ + targetId: options.targetId, + providerId: options.providerId, + mode: options.mode, + host: options.host, + port: options.port, + endpoint: `http://${options.host}:${options.port}`, + image: options.image, + repositoryPrefix: options.repositoryPrefix, + baseDir: options.baseDir, + storageDir: options.storageDir, + unitName: options.unitName, + composeProject: options.composeProject, + serviceName: options.serviceName, + containerName: options.containerName, + timeoutMs: options.timeoutMs, + sourceRepo: options.sourceRepo, + consumerTarget: options.consumerTarget, + consumerCatalogRef: options.consumerCatalogRef, + configSource: options.configSource, + }); +} export const supportedArtifactConsumerServices = [ "auth-broker", diff --git a/scripts/src/ci/cleanup.ts b/scripts/src/ci/cleanup.ts index af02b028..7211b422 100644 --- a/scripts/src/ci/cleanup.ts +++ b/scripts/src/ci/cleanup.ts @@ -1,4 +1,5 @@ // SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. cleanup module for scripts/src/ci.ts. +// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. // Moved mechanically from scripts/src/ci.ts:1206-1428 for #903. @@ -24,7 +25,7 @@ import { runSshCommandCapture } from "../ssh"; import type { CiCleanupFailedPodsOptions, CiCleanupRunsOptions, CiTarget } from "./types"; import { ciTargetGuardShellLines, shellQuote, tailTextLines } from "./options"; import { runRemoteKubectl } from "./remote"; -import { ciTarget, tektonPipelineVersion, tektonTriggersVersion } from "./types"; +import { ciTarget, ciTargetSourceSummary, tektonPipelineVersion, tektonTriggersVersion } from "./types"; export async function status(target = ciTarget(null)): Promise> { const summary = await runRemoteKubectl([ @@ -40,6 +41,7 @@ export async function status(target = ciTarget(null)): Promise> { const [action = "status", nameArg] = args; @@ -44,6 +45,21 @@ export async function runCiCommand(config: UniDeskConfig, args: string[]): Promi return options.wait ? install(config, options) : installAsync(options); } if (action === "install-status") return installStatus(nameArg ?? "latest"); + if (action === "plan") { + const target = ciTarget(providerIdOption(args)); + return { + ok: true, + action: "ci-plan", + mutation: false, + target: ciTargetSourceSummary(target), + configTruth: target.configSource.configPath, + next: { + status: `bun scripts/cli.ts ci status --target ${target.targetId}`, + installDryRun: `bun scripts/cli.ts ci install --target ${target.targetId} --skip-prewarm --skip-tekton-install`, + run: `bun scripts/cli.ts ci run --target ${target.targetId} --revision `, + }, + }; + } if (action === "status") return status(ciTarget(providerIdOption(args))); if (action === "run") { const target = ciTarget(providerIdOption(args)); @@ -130,12 +146,12 @@ export async function runCiCommand(config: UniDeskConfig, args: string[]): Promi if (action === "logs") return logs(config, nameArg ?? "", ciTarget(providerIdOption(args)), ciLogsOptions(args)); if (action === "cleanup-runs") return cleanupRuns(config, ciCleanupRunsOptions(args)); if (action === "cleanup-failed-pods") return cleanupFailedPods(config, ciCleanupFailedPodsOptions(args)); - throw new Error("ci command must be one of: install, status, run, publish-backend-core, publish-user-service, run-dev-e2e, logs, cleanup-runs, cleanup-failed-pods"); + throw new Error("ci command must be one of: install, plan, status, run, publish-backend-core, publish-user-service, run-dev-e2e, logs, cleanup-runs, cleanup-failed-pods"); } -export function startCiInstallJob(providerId = d601ProviderId): Record { +export function startCiInstallJob(targetId?: string): Record { return installAsync({ - target: ciTarget(providerId), + target: ciTarget(targetId ?? null), skipPrewarm: false, skipTektonInstall: false, wait: false, diff --git a/scripts/src/ci/help.ts b/scripts/src/ci/help.ts index 8840b2c6..60aa5a43 100644 --- a/scripts/src/ci/help.ts +++ b/scripts/src/ci/help.ts @@ -1,4 +1,5 @@ // SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. help module for scripts/src/ci.ts. +// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. // Moved mechanically from scripts/src/ci.ts:3273-3409 for #903. @@ -58,9 +59,10 @@ export function ciHelp(): Record { "bun scripts/cli.ts ci install --skip-prewarm --skip-tekton-install", "bun scripts/cli.ts ci install-status latest", "bun scripts/cli.ts ci install --wait --skip-prewarm --skip-tekton-install", - "bun scripts/cli.ts ci install --provider-id G14", + "bun scripts/cli.ts ci plan --target D601", + "bun scripts/cli.ts ci install --target G14", "bun scripts/cli.ts ci run --revision ", - "bun scripts/cli.ts ci run --provider-id G14 --revision ", + "bun scripts/cli.ts ci run --target G14 --revision ", "bun scripts/cli.ts ci publish-backend-core --commit ", "bun scripts/cli.ts ci publish-user-service --service baidu-netdisk --commit ", "bun scripts/cli.ts ci publish-user-service --service mdtodo --commit ", @@ -69,11 +71,11 @@ export function ciHelp(): Record { "bun scripts/cli.ts ci publish-user-service --service decision-center --commit ", "bun scripts/cli.ts ci publish-user-service --service frontend --commit ", "bun scripts/cli.ts ci run-dev-e2e --wait-ms 600000", - "bun scripts/cli.ts ci logs [--provider-id G14] [--tail-lines 80]", - "bun scripts/cli.ts ci cleanup-runs --provider-id D601 --min-age-minutes 60 --limit 50 --dry-run", - "bun scripts/cli.ts ci cleanup-runs --provider-id D601 --min-age-minutes 60 --limit 50 --confirm", - "bun scripts/cli.ts ci cleanup-failed-pods --provider-id D601 --namespace unidesk-ci --min-age-minutes 60 --dry-run", - "bun scripts/cli.ts ci cleanup-failed-pods --provider-id D601 --namespace unidesk-ci --min-age-minutes 60 --confirm", + "bun scripts/cli.ts ci logs [--target G14] [--tail-lines 80]", + "bun scripts/cli.ts ci cleanup-runs --target D601 --min-age-minutes 60 --limit 50 --dry-run", + "bun scripts/cli.ts ci cleanup-runs --target D601 --min-age-minutes 60 --limit 50 --confirm", + "bun scripts/cli.ts ci cleanup-failed-pods --target D601 --namespace unidesk-ci --min-age-minutes 60 --dry-run", + "bun scripts/cli.ts ci cleanup-failed-pods --target D601 --namespace unidesk-ci --min-age-minutes 60 --confirm", ], tekton: { pipelineVersion: tektonPipelineVersion, diff --git a/scripts/src/ci/install.ts b/scripts/src/ci/install.ts index 07f5b463..6b46bbfc 100644 --- a/scripts/src/ci/install.ts +++ b/scripts/src/ci/install.ts @@ -1,4 +1,5 @@ // SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. install module for scripts/src/ci.ts. +// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. // Moved mechanically from scripts/src/ci.ts:1429-1559 for #903. @@ -99,8 +100,8 @@ export function ciInstallCommand(options: CiInstallOptions): string[] { "scripts/cli.ts", "ci", "install", - "--provider-id", - options.target.providerId, + "--target", + options.target.targetId, "--wait", ...(options.skipPrewarm ? ["--skip-prewarm"] : []), ...(options.skipTektonInstall ? ["--skip-tekton-install"] : []), diff --git a/scripts/src/ci/logs.ts b/scripts/src/ci/logs.ts index bac332fb..5f7372d4 100644 --- a/scripts/src/ci/logs.ts +++ b/scripts/src/ci/logs.ts @@ -1,4 +1,5 @@ // SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. logs module for scripts/src/ci.ts. +// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. // Moved mechanically from scripts/src/ci.ts:3168-3272 for #903. @@ -61,6 +62,7 @@ export function pipelineRunLogsScript(name: string, options: CiLogsOptions): str export function ciLogsResultFromCapture(args: { ok: boolean; providerId: string; + targetId: string; kind: "run" | "pipelinerun"; name: string; options: CiLogsOptions; @@ -86,8 +88,8 @@ export function ciLogsResultFromCapture(args: { ...(args.dispatchTaskId === undefined ? {} : { dispatchTaskId: args.dispatchTaskId }), fullOutputAvailable: args.options.capture === "ssh-stream", next: [ - `bun scripts/cli.ts ci logs ${args.name} --provider-id ${args.providerId} --tail-lines ${Math.min(args.options.tailLines * 2, 2000)}`, - `bun scripts/cli.ts ci status --provider-id ${args.providerId}`, + `bun scripts/cli.ts ci logs ${args.name} --target ${args.targetId} --tail-lines ${Math.min(args.options.tailLines * 2, 2000)}`, + `bun scripts/cli.ts ci status --target ${args.targetId}`, ], }; } @@ -104,6 +106,7 @@ export async function logs(config: UniDeskConfig, name: string, target = ciTarge return ciLogsResultFromCapture({ ok: resultOk, providerId: target.providerId, + targetId: target.targetId, kind: "run", name, options, @@ -121,6 +124,7 @@ export async function logs(config: UniDeskConfig, name: string, target = ciTarge return ciLogsResultFromCapture({ ok: result.exitCode === 0, providerId: target.providerId, + targetId: target.targetId, kind: "pipelinerun", name, options, diff --git a/scripts/src/ci/publish-preflight.ts b/scripts/src/ci/publish-preflight.ts index ace59ab6..135166f2 100644 --- a/scripts/src/ci/publish-preflight.ts +++ b/scripts/src/ci/publish-preflight.ts @@ -1,4 +1,5 @@ // SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. publish-preflight module for scripts/src/ci.ts. +// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. // Moved mechanically from scripts/src/ci.ts:2317-2567 for #903. @@ -89,7 +90,7 @@ export async function publishUserServicePreflight( raw: sshProbe.ok ? undefined : dispatchPreflightFailure("host.ssh readonly probe", sshProbe).raw, })); - const registryOptions = parseArtifactRegistryOptions(["--provider-id", providerId]); + const registryOptions = parseArtifactRegistryOptions(["--target", providerId]); const registryProbe = buildArtifactRegistryReadonlyProbe("health", registryOptions); const registryDispatch = await transport.dispatchHostSsh(registryProbe.script, Math.max(registryProbe.timeoutMs, 30_000), registryProbe.timeoutMs); const registryCommand = commandResultFromDispatch(transport.artifactRegistryCommand(registryProbe), transport.commandCwd, registryDispatch); @@ -131,7 +132,7 @@ export async function publishUserServicePreflight( ? "From a Code Queue runner, rerun this read-only preflight through the existing remote frontend transport: bun scripts/cli.ts --main-server-ip ci publish-user-service --service --commit --dry-run." : "Run from the main-server CLI or use remote frontend transport against a healthy frontend/backend-core path.", "Restore backend-core/database/provider-gateway/Host SSH connectivity before retrying artifact publication.", - "Use bun scripts/cli.ts artifact-registry health --provider-id D601 to recheck registry reachability after the control bridge is restored.", + "Use bun scripts/cli.ts artifact-registry health --target D601 to recheck registry reachability after the control bridge is restored.", ], boundary: "preflight is read-only: no D601 source export, no Tekton PipelineRun, no image push, no deploy apply, no service restart", }; @@ -182,7 +183,7 @@ export async function publishBackendCorePreflight( raw: ciRunnerReady ? undefined : dispatchPreflightFailure("backend-core ci runner readonly probe", ciProbe).raw, })); - const registryOptions = parseArtifactRegistryOptions(["--provider-id", providerId]); + const registryOptions = parseArtifactRegistryOptions(["--target", providerId]); const registryProbe = buildArtifactRegistryReadonlyProbe("health", registryOptions); const registryDispatch = await transport.dispatchHostSsh(registryProbe.script, Math.max(registryProbe.timeoutMs, 30_000), registryProbe.timeoutMs); const registryCommand = commandResultFromDispatch(transport.artifactRegistryCommand(registryProbe), transport.commandCwd, registryDispatch); diff --git a/scripts/src/ci/publish.ts b/scripts/src/ci/publish.ts index 0c4c0d57..41309d26 100644 --- a/scripts/src/ci/publish.ts +++ b/scripts/src/ci/publish.ts @@ -1,4 +1,5 @@ // SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. publish module for scripts/src/ci.ts. +// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. // Moved mechanically from scripts/src/ci.ts:2568-2959 for #903. @@ -56,8 +57,8 @@ export async function run(options: CiOptions): Promise> }, condition, next: [ - `bun scripts/cli.ts ci logs ${name} --provider-id ${options.target.providerId}`, - `bun scripts/cli.ts ci status --provider-id ${options.target.providerId}`, + `bun scripts/cli.ts ci logs ${name} --target ${options.target.targetId}`, + `bun scripts/cli.ts ci status --target ${options.target.targetId}`, ], }; } diff --git a/scripts/src/ci/types.ts b/scripts/src/ci/types.ts index a3b0f01e..5659d65a 100644 --- a/scripts/src/ci/types.ts +++ b/scripts/src/ci/types.ts @@ -1,4 +1,5 @@ // SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. types module for scripts/src/ci.ts. +// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. // Moved mechanically from scripts/src/ci.ts:1-267 for #903. @@ -20,13 +21,17 @@ import { } from "../artifact-registry"; import { d601K3sGuardShellLines, d601NativeKubeconfig } from "../d601-k3s-guard"; import { runSshCommandCapture } from "../ssh"; +import { cicdTargetSummary, resolveCicdTarget, type TargetConfigSource } from "../ops/targets"; import { status } from "./cleanup"; import { stringOption } from "./options"; -export const d601ProviderId = "D601"; +const d601YamlTarget = resolveCicdTarget("D601"); +const g14YamlTarget = resolveCicdTarget("G14"); -export const d601Kubeconfig = d601NativeKubeconfig; +export const d601ProviderId = d601YamlTarget.providerId; + +export const d601Kubeconfig = d601YamlTarget.kubeconfig; export const tektonPipelineVersion = "v1.12.0"; @@ -46,9 +51,9 @@ export const codeQueueDirectDockerBaseImage = "unidesk-code-queue:d601"; export const providerDispatchCompletionLagMs = 45_000; -export const defaultCiPipelineManifest = "src/components/microservices/k3sctl-adapter/k3s/ci/unidesk-ci.pipeline.yaml"; +export const defaultCiPipelineManifest = d601YamlTarget.pipelineManifest; -export const g14CiPipelineManifest = "src/components/microservices/k3sctl-adapter/k3s/ci/unidesk-ci.pipeline.g14.yaml"; +export const g14CiPipelineManifest = g14YamlTarget.pipelineManifest; export const ciRuntimeImages = [ "rancher/mirrored-pause:3.6", @@ -65,36 +70,44 @@ export const ciRuntimeImages = [ ]; export function ciTarget(providerId: string | null): CiTarget { - const normalized = providerId ?? d601ProviderId; - if (normalized === d601ProviderId) { - return { - providerId: d601ProviderId, - kubeconfig: d601Kubeconfig, - hostCwd: "/home/ubuntu", - homeDir: "/home/ubuntu", - pipelineManifest: defaultCiPipelineManifest, - codeQueueImage: ciCodeQueueImage, - guardName: "d601_native_k3s_guard", - requiredNodeName: "d601", - }; - } - if (normalized === "G14") { - return { - providerId: "G14", - kubeconfig: d601Kubeconfig, - hostCwd: "/root", - homeDir: "/root", - pipelineManifest: g14CiPipelineManifest, - codeQueueImage: ciCodeQueueImage, - guardName: "g14_native_k3s_guard", - requiredNodeLabel: { key: "unidesk.ai/node-id", value: "G14" }, - }; - } - throw new Error(`ci --provider-id currently supports D601 or G14, got ${normalized}`); + const target = resolveCicdTarget(providerId); + return { + targetId: target.targetId, + providerId: target.providerId, + kubeRoute: target.kubeRoute, + kubeconfig: target.kubeconfig, + hostCwd: target.hostCwd, + homeDir: target.homeDir, + pipelineManifest: target.pipelineManifest, + codeQueueImage: target.codeQueueImage, + guardName: target.guardName, + requiredNodeName: target.requiredNodeName, + requiredNodeLabel: target.requiredNodeLabel, + artifactRegistryConfigRef: target.artifactRegistryConfigRef, + configSource: target.configSource, + }; } export function providerIdOption(args: string[]): string | null { - return stringOption(args, "--provider-id") ?? stringOption(args, "--provider"); + return stringOption(args, "--target") ?? stringOption(args, "--provider-id") ?? stringOption(args, "--provider"); +} + +export function ciTargetSourceSummary(target: CiTarget): Record { + return cicdTargetSummary({ + targetId: target.targetId, + providerId: target.providerId, + kubeRoute: target.kubeRoute, + kubeconfig: target.kubeconfig, + hostCwd: target.hostCwd, + homeDir: target.homeDir, + pipelineManifest: target.pipelineManifest, + codeQueueImage: target.codeQueueImage, + guardName: target.guardName, + requiredNodeName: target.requiredNodeName, + requiredNodeLabel: target.requiredNodeLabel, + artifactRegistryConfigRef: target.artifactRegistryConfigRef, + configSource: target.configSource, + }); } export interface CiOptions { @@ -112,7 +125,9 @@ export interface CiInstallOptions { } export interface CiTarget { + targetId: string; providerId: string; + kubeRoute: string; kubeconfig: string; hostCwd: string; homeDir: string; @@ -121,6 +136,8 @@ export interface CiTarget { guardName: string; requiredNodeName?: string; requiredNodeLabel?: { key: string; value: string }; + artifactRegistryConfigRef?: string; + configSource: TargetConfigSource; } export interface CiPublishBackendCoreOptions { diff --git a/scripts/src/code-queue-liveness-fixtures.ts b/scripts/src/code-queue-liveness-fixtures.ts index b51b8984..0f3621c1 100644 --- a/scripts/src/code-queue-liveness-fixtures.ts +++ b/scripts/src/code-queue-liveness-fixtures.ts @@ -1,3 +1,4 @@ +// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. import { buildExecutionDiagnostics, buildSchedulerHeartbeat, schedulerHeartbeatStaleMs, staleRecoveryCandidate, taskHasTraceGapButFreshHeartbeat } from "../../src/components/microservices/code-queue/src/execution-diagnostics"; import type { ActiveRun } from "../../src/components/microservices/code-queue/src/code-agent/common"; import type { CodeQueueExecutionDiagnostics, QueueTask, SchedulerActiveRunHeartbeat, TaskStatus } from "../../src/components/microservices/code-queue/src/types"; @@ -24,6 +25,7 @@ const now = "2026-05-19T00:10:00.000Z"; const freshAt = "2026-05-19T00:09:50.000Z"; const oldTraceAt = "2026-05-18T23:40:00.000Z"; const expiredAt = "2026-05-18T23:50:00.000Z"; +const fixtureProviderId = "D601"; function assertCondition(condition: unknown, message: string, detail: Record = {}): void { if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`); @@ -38,7 +40,7 @@ function fixtureTask(id: string, status: TaskStatus, heartbeat: SchedulerActiveR basePrompt: `${id} prompt`, referenceTaskIds: [], referenceInjection: null, - providerId: "D601", + providerId: fixtureProviderId, cwd: "/workspace", model: "gpt-5.5", reasoningEffort: null, diff --git a/scripts/src/deploy/entry.ts b/scripts/src/deploy/entry.ts index 6c7f4f8f..9d3636f0 100644 --- a/scripts/src/deploy/entry.ts +++ b/scripts/src/deploy/entry.ts @@ -1,4 +1,5 @@ // SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. entry module for scripts/src/deploy.ts. +// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. // Moved mechanically from scripts/src/deploy.ts:3528-3700 for #903. @@ -27,6 +28,7 @@ import { type DeployJsonExecutorMirror, type DeployJsonServiceContract, } from "../deploy-json-contract"; +import { resolveArtifactRegistryTarget } from "../ops/targets"; import type { DeployAction } from "./types"; import { applyJob, devArtifactApplyJob, prodArtifactApplyJob, runApplyNow, runArtifactConsumerApplyNow, runProdArtifactApplyNow } from "./artifact-jobs"; @@ -108,7 +110,9 @@ export async function runDeployCommand(config: UniDeskConfig | null, args: strin export async function runCodeQueueDeployCompatCommand(_config: UniDeskConfig, args: string[]): Promise { if (args.includes("--skip-build")) throw new Error("codex deploy is disabled; --skip-build is not supported"); - const providerId = optionValue(args, ["--provider-id", "--provider"]) ?? "D601"; - if (providerId !== "D601") throw new Error(`codex deploy compatibility path only supports D601; got ${providerId}`); + const selectedTarget = optionValue(args, ["--target", "--provider-id", "--provider"]); + if (selectedTarget === undefined) throw new Error("codex deploy compatibility path is a legacy adapter and requires explicit --target D601; it no longer falls back to a hidden provider default"); + const target = resolveArtifactRegistryTarget(selectedTarget); + if (target.providerId !== "D601") throw new Error(`codex deploy compatibility path only supports D601; got ${target.providerId}`); throw new Error("codex deploy is disabled because D601 maintenance-channel direct deployment must not deploy Code Queue. Use the dev-only artifact consumer with deploy apply --env dev --service code-queue or artifact-registry deploy-service --env dev --service code-queue; production Code Queue artifact deployment is unsupported."); } diff --git a/scripts/src/deploy/options.ts b/scripts/src/deploy/options.ts index fabf1beb..127c7000 100644 --- a/scripts/src/deploy/options.ts +++ b/scripts/src/deploy/options.ts @@ -1,4 +1,5 @@ // SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. options module for scripts/src/deploy.ts. +// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. // Moved mechanically from scripts/src/deploy.ts:213-535 for #903. @@ -27,6 +28,7 @@ import { type DeployJsonExecutorMirror, type DeployJsonServiceContract, } from "../deploy-json-contract"; +import { resolveArtifactRegistryTarget } from "../ops/targets"; import type { DeployEnvironment, DeployManifest, DeployManifestService, DeployOptions } from "./types"; import { step } from "./remote"; @@ -44,7 +46,7 @@ export function deployHelp(action: string | undefined = undefined): Record", default: defaultTimeoutMs, description: "Per-step timeout budget where supported." }, - { name: "--provider-id ", default: "D601", description: "Provider used by artifact-registry consumers; use local only from the D601 host CLI." }, + { name: "--target ", default: "config/artifact-registry.yaml#defaults.targetId", description: "Artifact-registry consumer target. --provider-id is accepted as a legacy alias." }, { name: "--run-now", description: "Run apply in the foreground worker process; omit it for fire-and-forget async job mode." }, { name: "guard code-queue-source --root ", description: "Validate Code Queue hostPath source relative imports before any scheduler rollout; failures report degradedReason and missing import targets." }, ], @@ -342,12 +344,13 @@ export function parseOptions(args: string[]): DeployOptions { if (commitOverride !== undefined && environment === null) throw new Error("deploy --commit is only supported for --env dev|prod artifact consumer apply"); if (commitOverride !== undefined && !isFullGitSha(commitOverride)) throw new Error("deploy --commit must be a full 40-character commit SHA"); if (commitOverride !== undefined && serviceId === null) throw new Error("deploy --commit requires --service so artifact consumer apply is unambiguous"); + const artifactTarget = resolveArtifactRegistryTarget(optionValue(args, ["--target", "--provider-id", "--provider"]) ?? null); return { file: optionValue(args, ["--file"]) ?? defaultDeployFile, environment, serviceId, commitOverride: commitOverride?.toLowerCase() ?? null, - providerId: optionValue(args, ["--provider-id", "--provider"]) ?? "D601", + providerId: artifactTarget.providerId, runNow: args.includes("--run-now"), dryRun: args.includes("--dry-run"), force: args.includes("--force"), diff --git a/scripts/src/deploy/service-plan.ts b/scripts/src/deploy/service-plan.ts index 6b1ee665..e40c891a 100644 --- a/scripts/src/deploy/service-plan.ts +++ b/scripts/src/deploy/service-plan.ts @@ -1,4 +1,5 @@ // SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. service-plan module for scripts/src/deploy.ts. +// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. // Moved mechanically from scripts/src/deploy.ts:634-1236 for #903. @@ -30,7 +31,7 @@ import { import type { ArtifactConsumerPlanKind, DeployEnvironment, DeployEnvironmentTarget, DeployManifest, DeployManifestService } from "./types"; import { targetIsMain, targetWorkDir } from "./paths"; -import { artifactConsumerDryRunBlockedServiceIds, d601MaintenanceDeployAllowedServiceIds, devApplySupportedServiceIds, devArtifactConsumerServiceIds, k8sNamespace, prodArtifactConsumerServiceIds, prodArtifactLiveApplyBlockedServiceIds, unideskRepoUrl } from "./types"; +import { artifactConsumerDryRunBlockedServiceIds, d601DeployNodeId, d601DeployProviderId, d601MaintenanceDeployAllowedServiceIds, devApplySupportedServiceIds, devArtifactConsumerServiceIds, k8sNamespace, prodArtifactConsumerServiceIds, prodArtifactLiveApplyBlockedServiceIds, unideskRepoUrl } from "./types"; export function frontendCoreDeployService(config: UniDeskConfig): UniDeskMicroserviceConfig { return { @@ -178,7 +179,7 @@ export function devK3sDeployService(id: string): UniDeskMicroserviceConfig | und return { id, name: spec.name, - providerId: "D601", + providerId: d601DeployProviderId, description: spec.description, repository: { url: spec.repoUrl ?? unideskRepoUrl, @@ -205,11 +206,11 @@ export function devK3sDeployService(id: string): UniDeskMicroserviceConfig | und adapterServiceId: "k3sctl-adapter", k3sServiceId: spec.composeService, namespace: "unidesk-dev", - expectedNodeIds: ["D601"], - activeNodeId: "D601", + expectedNodeIds: [d601DeployNodeId], + activeNodeId: d601DeployNodeId, }, development: { - providerId: "D601", + providerId: d601DeployProviderId, sshPassthrough: true, worktreePath: id === "code-queue" ? "/home/ubuntu/unidesk-dev-code-queue-deploy/code-queue" diff --git a/scripts/src/deploy/types.ts b/scripts/src/deploy/types.ts index 0912f1b4..864f8cd7 100644 --- a/scripts/src/deploy/types.ts +++ b/scripts/src/deploy/types.ts @@ -1,4 +1,5 @@ // SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. types module for scripts/src/deploy.ts. +// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. // Moved mechanically from scripts/src/deploy.ts:1-212 for #903. @@ -15,6 +16,7 @@ import { coreInternalFetch } from "../microservices"; import { codeQueueSourceImportPreflight, codeQueueSourceSubdir } from "../code-queue-source-guard"; import { d601K3sGuardShellLines, d601NativeKubeconfig } from "../d601-k3s-guard"; import { composeRuntimeEnvValue } from "../runtime-env"; +import { resolveArtifactRegistryTarget } from "../ops/targets"; import { compareDeployJsonExecutorMirrors, deployJsonCommitImage, @@ -151,6 +153,12 @@ export const shortRemoteTimeoutMs = 20_000; export const providerDispatchCompletionLagMs = 45_000; +const d601ArtifactRegistryTarget = resolveArtifactRegistryTarget("D601"); + +export const d601DeployProviderId = d601ArtifactRegistryTarget.providerId; + +export const d601DeployNodeId = d601ArtifactRegistryTarget.targetId; + export const pollIntervalMs = 5_000; export const remoteDeployRoot = "/home/ubuntu/.unidesk/deploy"; @@ -235,8 +243,8 @@ export const deployEnvironmentTargets: Record): HwlabRuntimeNodeS }; } -function isSupportedLaneId(id: string): id is HwlabRuntimeLane { - return id === "v02" || id === "v03"; +function assertYamlDeclaredLaneId(id: string, path: string): asserts id is HwlabRuntimeLane { + if (!/^[A-Za-z0-9._-]+$/u.test(id)) throw new Error(`${path} has an unsupported lane id; lane ids must be YAML-declared simple ids`); } function laneConfig(id: HwlabRuntimeLane, raw: Record): HwlabLaneConfig { @@ -1043,12 +1044,13 @@ function readHwlabNodeLaneConfig(): HwlabNodeLaneConfig { ); const laneEntries = sortedRecordEntries(parsed.lanes, "lanes"); const lanes = Object.fromEntries(laneEntries.map(([id, item]) => { - if (!isSupportedLaneId(id)) throw new Error(`lanes.${id} is not supported by this CLI build`); + assertYamlDeclaredLaneId(id, `lanes.${id}`); return [id, laneConfig(id, item)]; - })) as Record; - const laneTargets: Partial>> = {}; + })) as Record; + const laneTargets: Partial>> = {}; for (const [id, item] of laneEntries) { - if (!isSupportedLaneId(id) || item.targets === undefined) continue; + assertYamlDeclaredLaneId(id, `lanes.${id}`); + if (item.targets === undefined) continue; laneTargets[id] = Object.fromEntries(sortedRecordEntries(item.targets, `lanes.${id}.targets`).map(([nodeId, target]) => [ nodeId, laneTargetConfig(id, nodeId, item, target), @@ -1080,7 +1082,7 @@ function validateConfigEnvelope(parsed: Record): void { function parseDefaultTarget(raw: Record): { readonly node: string; readonly lane: HwlabRuntimeLane } { const lane = stringField(raw, "lane", `${HWLAB_NODE_LANE_CONFIG_PATH}.defaults`); - if (!isSupportedLaneId(lane)) throw new Error(`${HWLAB_NODE_LANE_CONFIG_PATH}.defaults.lane is not supported by this CLI build`); + assertYamlDeclaredLaneId(lane, `${HWLAB_NODE_LANE_CONFIG_PATH}.defaults.lane`); return { node: stringField(raw, "node", `${HWLAB_NODE_LANE_CONFIG_PATH}.defaults`), lane, @@ -1145,7 +1147,7 @@ function buildRuntimeLaneSpec(config: HwlabLaneConfig): HwlabRuntimeLaneSpec { const RUNTIME_LANE_SPECS = Object.fromEntries( Object.values(HWLAB_NODE_LANE_CONFIG.lanes).map((config) => [config.id, buildRuntimeLaneSpec(config)]), -) as Record; +) as Record; export function isHwlabRuntimeLane(value: string): value is HwlabRuntimeLane { return Object.prototype.hasOwnProperty.call(RUNTIME_LANE_SPECS, value); diff --git a/scripts/src/ops/config-refs.ts b/scripts/src/ops/config-refs.ts new file mode 100644 index 00000000..b6e09c06 --- /dev/null +++ b/scripts/src/ops/config-refs.ts @@ -0,0 +1,125 @@ +// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. +// Responsibility: shared YAML configRef parsing and redacted reference summaries. +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import { rootPath } from "../config"; + +export interface ParsedConfigRef { + ref: string; + file: string; + fragment: string; +} + +export interface ResolvedConfigRef { + ref: string; + file: string; + fragment: string; + value: unknown; + valueType: string; + present: true; + sha256: string; + summary: unknown; +} + +export function parseConfigRef(ref: string, label = "configRef"): ParsedConfigRef { + const [file, fragment, extra] = ref.split("#"); + if (extra !== undefined || file === undefined || fragment === undefined || file.length === 0 || fragment.length === 0) { + throw new Error(`${label} must use path/to/file.yaml#object.path syntax`); + } + if (file.startsWith("/") || file.includes("\0") || file.includes("..") || !file.startsWith("config/") || !file.endsWith(".yaml")) { + throw new Error(`${label} must reference a repo-relative config/*.yaml file without ..`); + } + if (!/^[A-Za-z0-9_.\-[\]]+$/u.test(fragment)) throw new Error(`${label} has an unsupported YAML path fragment`); + return { ref, file, fragment }; +} + +export function resolveConfigRef(ref: string, label = "configRef"): ResolvedConfigRef { + const parsed = parseConfigRef(ref, label); + const yaml = Bun.YAML.parse(readFileSync(rootPath(parsed.file), "utf8")) as unknown; + const value = valueAtPath(yaml, parsed.fragment, `${parsed.file}#${parsed.fragment}`); + const serialized = stableSerialize(value); + return { + ...parsed, + value, + valueType: valueType(value), + present: true, + sha256: createHash("sha256").update(serialized).digest("hex"), + summary: summarizeConfigValue(value), + }; +} + +export function resolveConfigRefString(ref: string, label = "configRef"): string { + const resolved = resolveConfigRef(ref, label); + if (typeof resolved.value !== "string" || resolved.value.length === 0) throw new Error(`${resolved.file}#${resolved.fragment} must resolve to a non-empty string`); + return resolved.value; +} + +export function configRefSummary(ref: string, label = "configRef"): Record { + const resolved = resolveConfigRef(ref, label); + return { + ref: resolved.ref, + file: resolved.file, + path: resolved.fragment, + present: resolved.present, + valueType: resolved.valueType, + sha256: resolved.sha256, + summary: resolved.summary, + }; +} + +export function configRefGraph(refs: Array<{ id: string; ref: string }>): Array> { + return refs.map((item) => ({ id: item.id, ...configRefSummary(item.ref, item.id) })); +} + +function valueAtPath(value: unknown, fragment: string, label: string): unknown { + let current = value; + for (const segment of fragment.split(".")) { + if (segment.length === 0) throw new Error(`${label} has an empty path segment`); + const parts = [...segment.matchAll(/([A-Za-z0-9_-]+)|\[(\d+)\]/gu)]; + if (parts.length === 0 || parts.map((match) => match[0]).join("") !== segment) throw new Error(`${label} has unsupported segment ${segment}`); + for (const part of parts) { + const key = part[1]; + const index = part[2]; + if (key !== undefined) { + if (typeof current !== "object" || current === null || Array.isArray(current)) throw new Error(`${label} segment ${key} does not resolve through an object`); + const record = current as Record; + if (!Object.prototype.hasOwnProperty.call(record, key)) throw new Error(`${label} is missing segment ${key}`); + current = record[key]; + } else if (index !== undefined) { + if (!Array.isArray(current)) throw new Error(`${label} segment [${index}] does not resolve through an array`); + const parsed = Number(index); + if (current[parsed] === undefined) throw new Error(`${label} is missing array item [${index}]`); + current = current[parsed]; + } + } + } + return current; +} + +function valueType(value: unknown): string { + if (Array.isArray(value)) return "array"; + if (value === null) return "null"; + return typeof value; +} + +function summarizeConfigValue(value: unknown): unknown { + if (typeof value === "string") return value.length > 160 ? `${value.slice(0, 157)}...` : value; + if (typeof value === "number" || typeof value === "boolean" || value === null) return value; + if (Array.isArray(value)) return { kind: "array", count: value.length }; + if (typeof value === "object") { + const keys = Object.keys(value as Record); + return { kind: "object", keys: keys.slice(0, 12), omittedKeys: Math.max(0, keys.length - 12) }; + } + return String(value); +} + +function stableSerialize(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(stableSerialize).join(",")}]`; + if (typeof value === "object" && value !== null) { + return `{${Object.keys(value as Record) + .sort() + .map((key) => `${JSON.stringify(key)}:${stableSerialize((value as Record)[key])}`) + .join(",")}}`; + } + return JSON.stringify(value); +} diff --git a/scripts/src/ops/targets.ts b/scripts/src/ops/targets.ts new file mode 100644 index 00000000..a468e3f5 --- /dev/null +++ b/scripts/src/ops/targets.ts @@ -0,0 +1,236 @@ +// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. +// Responsibility: YAML-backed target resolvers shared by CI/CD control-plane commands. +import { readFileSync } from "node:fs"; +import { rootPath } from "../config"; +import { configRefSummary } from "./config-refs"; + +export const CICD_TARGETS_CONFIG_PATH = "config/cicd/targets.yaml"; +export const ARTIFACT_REGISTRY_CONFIG_PATH = "config/artifact-registry.yaml"; + +export interface TargetConfigSource { + configPath: string; + targetPath: string; + defaultPath: string; + defaulted: boolean; +} + +export interface ResolvedCicdTarget { + targetId: string; + providerId: string; + kubeRoute: string; + kubeconfig: string; + hostCwd: string; + homeDir: string; + pipelineManifest: string; + codeQueueImage: string; + guardName: string; + requiredNodeName?: string; + requiredNodeLabel?: { key: string; value: string }; + artifactRegistryConfigRef?: string; + configSource: TargetConfigSource; +} + +export interface ResolvedArtifactRegistryTarget { + targetId: string; + providerId: string; + mode: string; + host: string; + port: number; + endpoint: string; + image: string; + repositoryPrefix: string; + baseDir: string; + storageDir: string; + unitName: string; + composeProject: string; + serviceName: string; + containerName: string; + timeoutMs: number; + sourceRepo: string; + consumerTarget: Record; + consumerCatalogRef: string; + configSource: TargetConfigSource; +} + +interface CodedTargetConfig { + defaults: { targetId: string }; + targets: Record; +} + +export function resolveCicdTarget(selection: string | null | undefined): ResolvedCicdTarget { + const config = readTargetConfig(CICD_TARGETS_CONFIG_PATH, "UnideskCicdTargets"); + const { targetId, record, defaulted } = selectTarget(config, selection, CICD_TARGETS_CONFIG_PATH); + const label = `targets.${targetId}`; + const requiredNodeLabel = optionalRecord(record.requiredNodeLabel, `${label}.requiredNodeLabel`); + return { + targetId, + providerId: stringField(record, "providerId", label), + kubeRoute: stringField(record, "kubeRoute", label), + kubeconfig: absolutePathField(record, "kubeconfig", label), + hostCwd: absolutePathField(record, "hostCwd", label), + homeDir: absolutePathField(record, "homeDir", label), + pipelineManifest: stringField(record, "pipelineManifest", label), + codeQueueImage: stringField(record, "codeQueueImage", label), + guardName: stringField(record, "guardName", label), + ...(record.requiredNodeName === undefined ? {} : { requiredNodeName: stringField(record, "requiredNodeName", label) }), + ...(requiredNodeLabel === undefined + ? {} + : { requiredNodeLabel: { key: stringField(requiredNodeLabel, "key", `${label}.requiredNodeLabel`), value: stringField(requiredNodeLabel, "value", `${label}.requiredNodeLabel`) } }), + ...(record.artifactRegistry === undefined + ? {} + : { artifactRegistryConfigRef: stringField(asRecord(record.artifactRegistry, `${label}.artifactRegistry`), "configRef", `${label}.artifactRegistry`) }), + configSource: configSource(CICD_TARGETS_CONFIG_PATH, targetId, defaulted), + }; +} + +export function resolveArtifactRegistryTarget(selection: string | null | undefined): ResolvedArtifactRegistryTarget { + const config = readTargetConfig(ARTIFACT_REGISTRY_CONFIG_PATH, "UnideskArtifactRegistry"); + const { targetId, record, defaulted } = selectTarget(config, selection, ARTIFACT_REGISTRY_CONFIG_PATH); + const label = `targets.${targetId}`; + const registry = asRecord(record.registry, `${label}.registry`); + const runtime = asRecord(record.runtime, `${label}.runtime`); + const source = asRecord(record.source, `${label}.source`); + const consumers = asRecord(record.consumers, `${label}.consumers`); + return { + targetId, + providerId: stringField(record, "providerId", label), + mode: stringField(record, "mode", label), + host: stringField(registry, "host", `${label}.registry`), + port: portField(registry, "port", `${label}.registry`), + endpoint: stringField(registry, "endpoint", `${label}.registry`), + image: stringField(registry, "image", `${label}.registry`), + repositoryPrefix: stringField(registry, "repositoryPrefix", `${label}.registry`), + baseDir: absolutePathField(runtime, "baseDir", `${label}.runtime`), + storageDir: absolutePathField(runtime, "storageDir", `${label}.runtime`), + unitName: stringField(runtime, "unitName", `${label}.runtime`), + composeProject: stringField(runtime, "composeProject", `${label}.runtime`), + serviceName: stringField(runtime, "serviceName", `${label}.runtime`), + containerName: stringField(runtime, "containerName", `${label}.runtime`), + timeoutMs: positiveIntegerField(runtime, "timeoutMs", `${label}.runtime`), + sourceRepo: stringField(source, "repo", `${label}.source`), + consumerTarget: asRecord(consumers.target, `${label}.consumers.target`), + consumerCatalogRef: stringField(consumers, "catalogRef", `${label}.consumers`), + configSource: configSource(ARTIFACT_REGISTRY_CONFIG_PATH, targetId, defaulted), + }; +} + +export function targetSourceSummary(target: { configSource: TargetConfigSource }): Record { + return { + configPath: target.configSource.configPath, + targetPath: target.configSource.targetPath, + defaultPath: target.configSource.defaultPath, + defaulted: target.configSource.defaulted, + }; +} + +export function cicdTargetSummary(target: ResolvedCicdTarget): Record { + return { + targetId: target.targetId, + providerId: target.providerId, + kubeRoute: target.kubeRoute, + kubeconfig: target.kubeconfig, + hostCwd: target.hostCwd, + homeDir: target.homeDir, + pipelineManifest: target.pipelineManifest, + codeQueueImage: target.codeQueueImage, + guardName: target.guardName, + requiredNodeName: target.requiredNodeName ?? null, + requiredNodeLabel: target.requiredNodeLabel ?? null, + artifactRegistryConfigRef: target.artifactRegistryConfigRef === undefined ? null : configRefSummary(target.artifactRegistryConfigRef, `${target.configSource.targetPath}.artifactRegistry.configRef`), + source: targetSourceSummary(target), + }; +} + +export function artifactRegistryTargetSummary(target: ResolvedArtifactRegistryTarget): Record { + return { + targetId: target.targetId, + providerId: target.providerId, + mode: target.mode, + registry: { + image: target.image, + host: target.host, + port: target.port, + endpoint: target.endpoint, + repositoryPrefix: target.repositoryPrefix, + }, + runtime: { + baseDir: target.baseDir, + storageDir: target.storageDir, + unitName: target.unitName, + composeProject: target.composeProject, + serviceName: target.serviceName, + containerName: target.containerName, + timeoutMs: target.timeoutMs, + }, + consumerTarget: target.consumerTarget, + consumerCatalogRef: target.consumerCatalogRef, + source: targetSourceSummary(target), + }; +} + +function readTargetConfig(path: string, expectedKind: string): CodedTargetConfig> { + const parsed = Bun.YAML.parse(readFileSync(rootPath(path), "utf8")) as unknown; + const root = asRecord(parsed, path); + const version = root.version; + if (version !== 1) throw new Error(`${path}.version must be 1`); + if (root.kind !== expectedKind) throw new Error(`${path}.kind must be ${expectedKind}`); + const defaults = asRecord(root.defaults, `${path}.defaults`); + const targets = asRecord(root.targets, `${path}.targets`); + const config = { + defaults: { targetId: stringField(defaults, "targetId", `${path}.defaults`) }, + targets: Object.fromEntries(Object.entries(targets).map(([key, value]) => [key, asRecord(value, `${path}.targets.${key}`)])), + }; + if (config.targets[config.defaults.targetId] === undefined) throw new Error(`${path}.defaults.targetId references missing target ${config.defaults.targetId}`); + return config; +} + +function selectTarget(config: CodedTargetConfig>, selection: string | null | undefined, path: string): { targetId: string; record: Record; defaulted: boolean } { + const raw = selection === undefined || selection === null || selection.length === 0 ? config.defaults.targetId : selection; + const targetId = Object.keys(config.targets).find((candidate) => candidate.toLowerCase() === raw.toLowerCase()) + ?? Object.entries(config.targets).find(([, target]) => typeof target.providerId === "string" && target.providerId.toLowerCase() === raw.toLowerCase())?.[0]; + if (targetId === undefined) throw new Error(`${path} has no target ${raw}; known targets: ${Object.keys(config.targets).join(", ")}`); + return { targetId, record: config.targets[targetId], defaulted: raw === config.defaults.targetId && (selection === undefined || selection === null || selection.length === 0) }; +} + +function configSource(path: string, targetId: string, defaulted: boolean): TargetConfigSource { + return { + configPath: path, + targetPath: `${path}#targets.${targetId}`, + defaultPath: `${path}#defaults.targetId`, + defaulted, + }; +} + +function asRecord(value: unknown, path: string): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be an object`); + return value as Record; +} + +function optionalRecord(value: unknown, path: string): Record | undefined { + if (value === undefined || value === null) return undefined; + return asRecord(value, path); +} + +function stringField(record: Record, key: string, path: string): string { + const value = record[key]; + if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${path}.${key} must be a non-empty string`); + return value.trim(); +} + +function absolutePathField(record: Record, key: string, path: string): string { + const value = stringField(record, key, path); + if (!value.startsWith("/")) throw new Error(`${path}.${key} must be an absolute path`); + return value.replace(/\/+$/u, ""); +} + +function positiveIntegerField(record: Record, key: string, path: string): number { + const value = record[key]; + if (!Number.isInteger(value) || Number(value) <= 0) throw new Error(`${path}.${key} must be a positive integer`); + return Number(value); +} + +function portField(record: Record, key: string, path: string): number { + const value = positiveIntegerField(record, key, path); + if (value > 65535) throw new Error(`${path}.${key} must be a TCP port`); + return value; +} diff --git a/scripts/src/platform-infra-observability/actions.ts b/scripts/src/platform-infra-observability/actions.ts index 16cc01dd..6218c01c 100644 --- a/scripts/src/platform-infra-observability/actions.ts +++ b/scripts/src/platform-infra-observability/actions.ts @@ -1,4 +1,5 @@ // SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. actions module for scripts/src/platform-infra-observability.ts. +// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. // Moved mechanically from scripts/src/platform-infra-observability.ts:515-623 for #903. @@ -119,6 +120,7 @@ export async function status(config: UniDeskConfig, options: CommonOptions): Pro action: "platform-infra-observability-status", mutation: false, target: targetSummary(target), + config: configSummary(observability, target), summary, remote: options.raw ? parsed : compactStatus(parsed, options.full) ?? compactCapture(result, { full: true }), next: { @@ -146,6 +148,7 @@ export async function validate(config: UniDeskConfig, options: CommonOptions): P action: "platform-infra-observability-validate", mutation: false, target: targetSummary(target), + config: configSummary(observability, target), summary, validation: { readiness: ready ? "passed" : "failed", diff --git a/scripts/src/platform-infra-observability/config.ts b/scripts/src/platform-infra-observability/config.ts index 90779cf0..119f4b19 100644 --- a/scripts/src/platform-infra-observability/config.ts +++ b/scripts/src/platform-infra-observability/config.ts @@ -1,4 +1,5 @@ // SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. config module for scripts/src/platform-infra-observability.ts. +// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. // Moved mechanically from scripts/src/platform-infra-observability.ts:370-514 for #903. @@ -22,6 +23,7 @@ import { import type { ImageSpec, ObservabilityConfig, ObservabilityTarget, OtlpPorts, ServiceConnection } from "./types"; import { assertKnownEnabledTarget, parseStatusEndpoint } from "./actions"; import { apiPathField, arrayOfRecords, asRecord, booleanField, configFile, configLabel, enumField, integerField, kubernetesNameField, numberArrayField, objectField, portField, stringArrayField, stringField } from "./types"; +import { configRefGraph, resolveConfigRefString } from "../ops/config-refs"; export function readObservabilityConfig(): ObservabilityConfig { const parsed = Bun.YAML.parse(readFileSync(configFile, "utf8")) as unknown; @@ -134,12 +136,33 @@ export function parseOtlpPorts(record: Record, path: string): O export function parseServiceConnection(record: Record, index: number): ServiceConnection { const path = `instrumentation.serviceConnections[${index}]`; + const refs = record.configRefs === undefined ? null : objectField(record, "configRefs", path); + const configRefs = refs === null + ? {} + : { + targetNode: stringField(refs, "targetNode", `${path}.configRefs`), + lane: stringField(refs, "lane", `${path}.configRefs`), + namespace: stringField(refs, "namespace", `${path}.configRefs`), + }; return { serviceName: stringField(record, "serviceName", path), owningRepo: stringField(record, "owningRepo", path), - targetNode: stringField(record, "targetNode", path), - lane: stringField(record, "lane", path), - namespace: kubernetesNameField(record, "namespace", path), + targetNode: refs === null ? stringField(record, "targetNode", path) : resolveConfigRefString(configRefs.targetNode, `${path}.configRefs.targetNode`), + lane: refs === null ? stringField(record, "lane", path) : resolveConfigRefString(configRefs.lane, `${path}.configRefs.lane`), + namespace: refs === null ? kubernetesNameField(record, "namespace", path) : kubernetesNameValue(resolveConfigRefString(configRefs.namespace, `${path}.configRefs.namespace`), `${path}.configRefs.namespace`), + configRefs, + configRefGraph: refs === null + ? [] + : configRefGraph([ + { id: `${path}.targetNode`, ref: configRefs.targetNode }, + { id: `${path}.lane`, ref: configRefs.lane }, + { id: `${path}.namespace`, ref: configRefs.namespace }, + ]), requiredSpans: stringArrayField(record, "requiredSpans", path), }; } + +function kubernetesNameValue(value: string, path: string): string { + if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/u.test(value)) throw new Error(`${configLabel}.${path} must resolve to a Kubernetes name`); + return value; +} diff --git a/scripts/src/platform-infra-observability/summary.ts b/scripts/src/platform-infra-observability/summary.ts index 378786f6..3f530e1d 100644 --- a/scripts/src/platform-infra-observability/summary.ts +++ b/scripts/src/platform-infra-observability/summary.ts @@ -1,4 +1,5 @@ // SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. summary module for scripts/src/platform-infra-observability.ts. +// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. // Moved mechanically from scripts/src/platform-infra-observability.ts:3031-4400 for #903. @@ -35,6 +36,17 @@ export function configSummary(observability: ObservabilityConfig, target: Observ collector: observability.collector, traceBackend: observability.traceBackend, sampling: observability.sampling, + serviceConnections: observability.instrumentation.serviceConnections.map((item) => ({ + serviceName: item.serviceName, + owningRepo: item.owningRepo, + resolved: { + targetNode: item.targetNode, + lane: item.lane, + namespace: item.namespace, + }, + configRefs: item.configRefGraph, + requiredSpans: item.requiredSpans, + })), }; } diff --git a/scripts/src/platform-infra-observability/types.ts b/scripts/src/platform-infra-observability/types.ts index 1ed3793b..d2ced1e8 100644 --- a/scripts/src/platform-infra-observability/types.ts +++ b/scripts/src/platform-infra-observability/types.ts @@ -1,4 +1,5 @@ // SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. types module for scripts/src/platform-infra-observability.ts. +// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. // Moved mechanically from scripts/src/platform-infra-observability.ts:1-149 for #903. @@ -112,6 +113,8 @@ export interface ServiceConnection { targetNode: string; lane: string; namespace: string; + configRefs: Record; + configRefGraph: Array>; requiredSpans: string[]; } diff --git a/scripts/src/platform-infra-ops-library.ts b/scripts/src/platform-infra-ops-library.ts index 6de7039a..b721da04 100644 --- a/scripts/src/platform-infra-ops-library.ts +++ b/scripts/src/platform-infra-ops-library.ts @@ -1,3 +1,4 @@ +// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. import { createHash } from "node:crypto"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { basename, dirname, join, normalize, relative } from "node:path"; @@ -8,7 +9,7 @@ import { coreInternalFetch } from "./microservices"; import { runSshCommandCapture, type SshCaptureResult } from "./ssh"; export interface OpsCommonOptions { - targetId: string; + targetId: string | null; full: boolean; raw: boolean; } @@ -120,10 +121,10 @@ export function shQuote(value: string): string { return `'${value.replaceAll("'", "'\"'\"'")}'`; } -export function parseOpsCommonOptions(args: string[], spec: OpsCommandOptionSpec = {}): OpsCommonOptions & Record { +export function parseOpsCommonOptions(args: string[], spec: OpsCommandOptionSpec = {}): OpsCommonOptions & Record { const stringOptions = new Set(["--target", ...(spec.stringOptions ?? [])]); const flagOptions = new Set(["--full", "--raw", ...(spec.flagOptions ?? [])]); - const values: Record = { targetId: "G14", full: false, raw: false }; + const values: Record = { full: false, raw: false }; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (stringOptions.has(arg)) { @@ -144,9 +145,9 @@ export function parseOpsCommonOptions(args: string[], spec: OpsCommandOptionSpec throw new Error(`unsupported option: ${arg}`); } } - const targetId = String(values.targetId); - if (!/^[A-Za-z0-9._-]+$/u.test(targetId)) throw new Error("--target must be a simple target id"); - return values as OpsCommonOptions & Record; + const targetId = values.targetId === undefined ? null : String(values.targetId); + if (targetId !== null && !/^[A-Za-z0-9._-]+$/u.test(targetId)) throw new Error("--target must be a simple target id"); + return { ...values, targetId } as OpsCommonOptions & Record; } export function parseOpsApplyOptions(args: string[]): OpsApplyOptions { diff --git a/scripts/src/platform-infra-wechat-archive.ts b/scripts/src/platform-infra-wechat-archive.ts index 55eab574..f8377a9a 100644 --- a/scripts/src/platform-infra-wechat-archive.ts +++ b/scripts/src/platform-infra-wechat-archive.ts @@ -1,3 +1,4 @@ +// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. import { randomUUID } from "node:crypto"; import { Buffer } from "node:buffer"; import { existsSync, readFileSync } from "node:fs"; @@ -1348,8 +1349,9 @@ function validateConfig(config: WechatArchiveConfig): void { validatePersonalWechatIngress(config.personalWechatIngress); } -function assertTarget(config: WechatArchiveConfig, targetId: string): void { - if (targetId !== config.target.id) throw new Error(`wechat archive target ${targetId} is not declared in ${configLabel}`); +function assertTarget(config: WechatArchiveConfig, targetId: string | null): void { + const resolved = targetId ?? config.target.id; + if (resolved !== config.target.id) throw new Error(`wechat archive target ${resolved} is not declared in ${configLabel}`); } function configSummary(config: WechatArchiveConfig): Record {