fix: YAML-first 治理 CI/CD target (#919)

* docs: specify cicd yaml target governance

* fix: resolve cicd targets from yaml

---------

Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
Lyon
2026-06-26 01:14:38 +08:00
committed by GitHub
parent 3777577df4
commit edfddd2445
35 changed files with 1079 additions and 181 deletions
+63
View File
@@ -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.
+35
View File
@@ -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
+1
View File
@@ -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
+12 -9
View File
@@ -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
@@ -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` configRefCLI `plan/status --full` 应显示引用链、resolved target、presence、摘要 hash 和缺失字段,不打印 Secret 值。代码中保留的 `G14``D601``v02``v03` 等目标名必须被 hardcode inventory 分类为 legacy adapter、help example、test fixture 或真实领域特例;隐藏默认必须迁移到 YAML。
@@ -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 和 publicExposureFRP 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 <id>``artifact-registry plan/status --target <id>``deploy apply --dry-run` 目标来源说明、`hwlab nodes control-plane status --node <node> --lane <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 标注。 |
@@ -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),
+28 -27
View File
@@ -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<string, unknown> {
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 <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --service frontend --env prod --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --service frontend --env dev --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env dev --service decision-center --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service decision-center --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service project-manager --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service oa-event-flow --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service code-queue-mgr --commit <full-sha> --dry-run [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service todo-note --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env dev --service todo-note --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env dev --service findjob --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service findjob --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env dev --service pipeline --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service pipeline --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env dev --service met-nonlinear --commit <full-sha> --dry-run [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service met-nonlinear --commit <full-sha> --dry-run [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service k3sctl-adapter --commit <full-sha> --dry-run [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env dev --service mdtodo --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service mdtodo --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env dev --service claudeqq --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service claudeqq --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env dev --service code-queue --commit <full-sha> --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 <full-sha> [--dry-run] [--run-now] [--target D601]",
"bun scripts/cli.ts artifact-registry deploy-service --service frontend --env prod --commit <full-sha> [--dry-run] [--run-now] [--target D601]",
"bun scripts/cli.ts artifact-registry deploy-service --service frontend --env dev --commit <full-sha> [--dry-run] [--run-now] [--target D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env dev --service decision-center --commit <full-sha> [--dry-run] [--run-now] [--target D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service decision-center --commit <full-sha> [--dry-run] [--run-now] [--target D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service project-manager --commit <full-sha> [--dry-run] [--run-now] [--target D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service oa-event-flow --commit <full-sha> [--dry-run] [--run-now] [--target D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service code-queue-mgr --commit <full-sha> --dry-run [--target D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service todo-note --commit <full-sha> [--dry-run] [--run-now] [--target D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env dev --service todo-note --commit <full-sha> [--dry-run] [--run-now] [--target D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env dev --service findjob --commit <full-sha> [--dry-run] [--run-now] [--target D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service findjob --commit <full-sha> [--dry-run] [--run-now] [--target D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env dev --service pipeline --commit <full-sha> [--dry-run] [--run-now] [--target D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service pipeline --commit <full-sha> [--dry-run] [--run-now] [--target D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env dev --service met-nonlinear --commit <full-sha> --dry-run [--target D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service met-nonlinear --commit <full-sha> --dry-run [--target D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service k3sctl-adapter --commit <full-sha> --dry-run [--target D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env dev --service mdtodo --commit <full-sha> [--dry-run] [--run-now] [--target D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service mdtodo --commit <full-sha> [--dry-run] [--run-now] [--target D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env dev --service claudeqq --commit <full-sha> [--dry-run] [--run-now] [--target D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service claudeqq --commit <full-sha> [--dry-run] [--run-now] [--target D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env dev --service code-queue --commit <full-sha> --dry-run [--target D601]",
],
firstStage: "install now writes the rendered systemd/Compose/config files and starts the registry",
artifactConsumers: {
+55 -25
View File
@@ -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;
+6 -4
View File
@@ -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: {
+3 -2
View File
@@ -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 <host> 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 <host> artifact-registry ${action} --target ${options.targetId}`
: `bun scripts/cli.ts --main-server-ip ${remoteHost} artifact-registry ${action} --target ${options.targetId}`,
},
localObservation: localResult,
remoteObservation: remoteResult,
+3 -1
View File
@@ -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<string, unknown> {
const bundle = renderBundle(options);
return {
ok: true,
target: artifactRegistryOptionsTargetSummary(options),
providerId: options.providerId,
mode: "d601-host-managed",
firstStage: true,
+65 -23
View File
@@ -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<string, unknown>;
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<string, unknown> {
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",
+7 -5
View File
@@ -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<Record<string, unknown>> {
const summary = await runRemoteKubectl([
@@ -40,6 +41,7 @@ export async function status(target = ciTarget(null)): Promise<Record<string, un
return {
ok: true,
providerId: target.providerId,
target: ciTargetSourceSummary(target),
orchestrator: "native-k3s",
tekton: {
pipelineVersion: tektonPipelineVersion,
@@ -156,8 +158,8 @@ export async function cleanupRuns(config: UniDeskConfig, options: CiCleanupRunsO
stderrTail: tailTextLines(deletion.stderr, 80),
},
next: options.confirm
? { status: `bun scripts/cli.ts ci status --provider-id ${options.target.providerId}` }
: { confirm: `bun scripts/cli.ts ci cleanup-runs --provider-id ${options.target.providerId} --min-age-minutes ${options.minAgeMinutes} --limit ${options.limit} --confirm` },
? { status: `bun scripts/cli.ts ci status --target ${options.target.targetId}` }
: { confirm: `bun scripts/cli.ts ci cleanup-runs --target ${options.target.targetId} --min-age-minutes ${options.minAgeMinutes} --limit ${options.limit} --confirm` },
};
}
@@ -244,7 +246,7 @@ export async function cleanupFailedPods(config: UniDeskConfig, options: CiCleanu
stderrTail: tailTextLines(result.stderr, 20),
},
next: options.confirm
? { status: `bun scripts/cli.ts ci status --provider-id ${options.target.providerId}` }
: { confirm: `bun scripts/cli.ts ci cleanup-failed-pods --provider-id ${options.target.providerId} --namespace ${options.namespace} --min-age-minutes ${options.minAgeMinutes} --limit ${options.limit} --confirm` },
? { status: `bun scripts/cli.ts ci status --target ${options.target.targetId}` }
: { confirm: `bun scripts/cli.ts ci cleanup-failed-pods --target ${options.target.targetId} --namespace ${options.namespace} --min-age-minutes ${options.minAgeMinutes} --limit ${options.limit} --confirm` },
};
}
+20 -4
View File
@@ -1,4 +1,5 @@
// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. entry 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:3410-3600 for #903.
@@ -29,7 +30,7 @@ import { logs } from "./logs";
import { blockedArtifactResult, blockedReason, boolFlag, ciCleanupFailedPodsOptions, ciCleanupRunsOptions, ciLogsOptions, isHelpArg, numberOption, publishTransportOption, requireDesiredRef, requireFullCommit, requireRepoRelativePath, requireRevision, requireServiceId, resolveCatalogArtifact, stringOption, userServicePublishBoundaryBlock } from "./options";
import { backendCoreArtifactSourceHostPath, userServiceArtifactSourceHostPath } from "./pipelinerun";
import { publishBackendCoreArtifact, publishUserServiceArtifact, run } from "./publish";
import { ciTarget, d601ProviderId, providerIdOption } from "./types";
import { ciTarget, ciTargetSourceSummary, providerIdOption } from "./types";
export async function runCiCommand(config: UniDeskConfig, args: string[]): Promise<Record<string, unknown>> {
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 <commit>`,
},
};
}
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<string, unknown> {
export function startCiInstallJob(targetId?: string): Record<string, unknown> {
return installAsync({
target: ciTarget(providerId),
target: ciTarget(targetId ?? null),
skipPrewarm: false,
skipTektonInstall: false,
wait: false,
+9 -7
View File
@@ -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<string, unknown> {
"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 <commit>",
"bun scripts/cli.ts ci run --provider-id G14 --revision <commit>",
"bun scripts/cli.ts ci run --target G14 --revision <commit>",
"bun scripts/cli.ts ci publish-backend-core --commit <full-sha>",
"bun scripts/cli.ts ci publish-user-service --service baidu-netdisk --commit <full-sha>",
"bun scripts/cli.ts ci publish-user-service --service mdtodo --commit <full-sha>",
@@ -69,11 +71,11 @@ export function ciHelp(): Record<string, unknown> {
"bun scripts/cli.ts ci publish-user-service --service decision-center --commit <full-sha>",
"bun scripts/cli.ts ci publish-user-service --service frontend --commit <full-sha>",
"bun scripts/cli.ts ci run-dev-e2e --wait-ms 600000",
"bun scripts/cli.ts ci logs <runId-or-pipelineRun> [--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 <runId-or-pipelineRun> [--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,
+3 -2
View File
@@ -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"] : []),
+6 -2
View File
@@ -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,
+4 -3
View File
@@ -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 <host> ci publish-user-service --service <id> --commit <full-sha> --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);
+3 -2
View File
@@ -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<Record<string, unknown>>
},
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}`,
],
};
}
+48 -31
View File
@@ -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<string, unknown> {
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 {
+3 -1
View File
@@ -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<string, unknown> = {}): 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,
+6 -2
View File
@@ -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<unknown> {
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.");
}
+6 -3
View File
@@ -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<strin
usage: {
check: "bun scripts/cli.ts deploy check [--file deploy.json | --env dev|prod] [--service id]",
plan: "bun scripts/cli.ts deploy plan [--file deploy.json | --env dev|prod] [--service id]",
apply: "bun scripts/cli.ts deploy apply [--file deploy.json | --env dev|prod] [--service id] [--commit full-sha] [--dry-run] [--force] [--timeout-ms N] [--provider-id D601|local] [--run-now]",
apply: "bun scripts/cli.ts deploy apply [--file deploy.json | --env dev|prod] [--service id] [--commit full-sha] [--dry-run] [--force] [--timeout-ms N] [--target D601|local] [--run-now]",
guard: "bun scripts/cli.ts deploy guard code-queue-source [--root /home/ubuntu/cq-deploy]",
},
actions: {
@@ -67,7 +69,7 @@ export function deployHelp(action: string | undefined = undefined): Record<strin
{ name: "--dry-run", description: "Prepare and validate without mutating the target service." },
{ name: "--force", description: "Redeploy even when the live commit appears up to date." },
{ name: "--timeout-ms <n>", default: defaultTimeoutMs, description: "Per-step timeout budget where supported." },
{ name: "--provider-id <id>", default: "D601", description: "Provider used by artifact-registry consumers; use local only from the D601 host CLI." },
{ name: "--target <id>", 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 <path>", 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"),
+6 -5
View File
@@ -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"
+10 -2
View File
@@ -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<DeployEnvironment, DeployEnvironme
name: "unidesk",
},
provider: {
providerId: "D601",
nodeId: "D601",
providerId: d601DeployProviderId,
nodeId: d601DeployNodeId,
credentialScope: "prod",
},
},
+11 -9
View File
@@ -1,10 +1,11 @@
// SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0.
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-25-p0-web-probe-sentinel.
// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets.
// Responsibility: YAML source-of-truth parsing for HWLAB node/lane Workbench observability.
import { readFileSync } from "node:fs";
import { rootPath } from "./config";
export type HwlabRuntimeLane = "v02" | "v03";
export type HwlabRuntimeLane = string;
export interface HwlabRuntimeNodeSpec {
readonly id: string;
@@ -520,8 +521,8 @@ function nodeConfig(id: string, raw: Record<string, unknown>): 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<string, unknown>): 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<HwlabRuntimeLane, HwlabLaneConfig>;
const laneTargets: Partial<Record<HwlabRuntimeLane, Record<string, HwlabLaneConfig>>> = {};
})) as Record<string, HwlabLaneConfig>;
const laneTargets: Partial<Record<string, Record<string, HwlabLaneConfig>>> = {};
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<string, unknown>): void {
function parseDefaultTarget(raw: Record<string, unknown>): { 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<HwlabRuntimeLane, HwlabRuntimeLaneSpec>;
) as Record<string, HwlabRuntimeLaneSpec>;
export function isHwlabRuntimeLane(value: string): value is HwlabRuntimeLane {
return Object.prototype.hasOwnProperty.call(RUNTIME_LANE_SPECS, value);
+125
View File
@@ -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<string, unknown> {
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<Record<string, unknown>> {
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<string, unknown>;
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<string, unknown>);
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<string, unknown>)
.sort()
.map((key) => `${JSON.stringify(key)}:${stableSerialize((value as Record<string, unknown>)[key])}`)
.join(",")}}`;
}
return JSON.stringify(value);
}
+236
View File
@@ -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<string, unknown>;
consumerCatalogRef: string;
configSource: TargetConfigSource;
}
interface CodedTargetConfig<T> {
defaults: { targetId: string };
targets: Record<string, T>;
}
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<string, unknown> {
return {
configPath: target.configSource.configPath,
targetPath: target.configSource.targetPath,
defaultPath: target.configSource.defaultPath,
defaulted: target.configSource.defaulted,
};
}
export function cicdTargetSummary(target: ResolvedCicdTarget): Record<string, unknown> {
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<string, unknown> {
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<Record<string, unknown>> {
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<Record<string, unknown>>, selection: string | null | undefined, path: string): { targetId: string; record: Record<string, unknown>; 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<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be an object`);
return value as Record<string, unknown>;
}
function optionalRecord(value: unknown, path: string): Record<string, unknown> | undefined {
if (value === undefined || value === null) return undefined;
return asRecord(value, path);
}
function stringField(record: Record<string, unknown>, 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<string, unknown>, 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<string, unknown>, 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<string, unknown>, 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;
}
@@ -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",
@@ -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<string, unknown>, path: string): O
export function parseServiceConnection(record: Record<string, unknown>, 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;
}
@@ -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,
})),
};
}
@@ -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<string, string>;
configRefGraph: Array<Record<string, unknown>>;
requiredSpans: string[];
}
+7 -6
View File
@@ -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<string, string | boolean> {
export function parseOpsCommonOptions(args: string[], spec: OpsCommandOptionSpec = {}): OpsCommonOptions & Record<string, string | boolean | null> {
const stringOptions = new Set(["--target", ...(spec.stringOptions ?? [])]);
const flagOptions = new Set(["--full", "--raw", ...(spec.flagOptions ?? [])]);
const values: Record<string, string | boolean> = { targetId: "G14", full: false, raw: false };
const values: Record<string, string | boolean | null> = { 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<string, string | boolean>;
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<string, string | boolean | null>;
}
export function parseOpsApplyOptions(args: string[]): OpsApplyOptions {
+4 -2
View File
@@ -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<string, unknown> {