# v0.2 CI/CD 规格

本文是 HWLAB `v0.2` 在 G14 上接入 CI/CD 的长期规格。目标是把 `v0.2` 作为独立加法 lane 接入现有 G14 k3s、Tekton、GitOps 和 Argo CD 体系，同时保持 `G14` DEV/PROD 发布面稳定。

实施跟踪见 [pikasTech/HWLAB#530](https://github.com/pikasTech/HWLAB/issues/530)，env 容器复用、三变量启动和 git mirror 加速设计见 [pikasTech/HWLAB#572](https://github.com/pikasTech/HWLAB/issues/572)，PR 合并后自动 CD 迁移计划见 [pikasTech/HWLAB#848](https://github.com/pikasTech/HWLAB/issues/848)，CI 无锁并发与 CD latest-only 迁移见 [pikasTech/HWLAB#866](https://github.com/pikasTech/HWLAB/issues/866)。原 `docs/plan/hwlab-v02-namespace-cicd.md` 阶段计划全文已迁入 #530 评论。本文只记录稳定规格、边界和判定标准；不要把一次性执行记录、排障流水账或临时证据写入本文。

## 在系统中的职责划分

`v0.2` CI/CD 是 G14 上的加法 lane：source branch、GitOps branch、runtime namespace、artifact catalog、Argo Application、Tekton Pipeline 和 FRP 入口均独立于 DEV/PROD。它共享 G14 k3s、Tekton controller、Argo CD controller、本地 registry 和构建工具，但不能复用 DEV/PROD runtime path、namespace、Application 或公网端口作为 v02 发布证据。code-only fast lane 必须消费独立 `devops-infra` 集群提供的 git mirror/relay；该 mirror/relay 是跨项目 DevOps 基础设施，不属于 `hwlab-v02` runtime namespace，也不改变当前 G14 registry 的部署位置或镜像引用真相。

## 内部架构

CI/CD 内部由 UniDesk 受控触发入口、CI/CD 专用 source repo、devops-infra git mirror、PipelineRun、component planner、BuildKit publish、GitOps promotion、Argo sync、GitHub flush 和公网验收构成。标准触发方式是 UniDesk CLI 合并 base=`v0.2` PR 后自动进入 v02 CD；`trigger-current` 手动入口和 `auto-cd --once` 类补偿入口保留用于人工重跑、漏触发修复和迁移期验证。`v0.2` 不设置 k8s branch poller、control-plane reconciler 或其他 CronJob；source branch 只保存源码和人写配置；`v0.2-gitops` branch 保存 catalog 和 rendered runtime desired state；live runtime 是最终通过证据。固定开发 workspace 只服务人工开发和短连接源码工具，不参与 CI/CD source commit 选择。

首期自动化不要求 GitHub webhook。PR 创建、预检和 merge 已由 UniDesk CLI 控制时，merge 成功后的同一受控流程应直接解析最新 `origin/v0.2` head 并调用现有 v02 control-plane。若未来接入 webhook，它只能作为唤醒信号；发布 source commit 仍必须由 `/root/hwlab-v02-cicd.git` fetch 到的 `refs/remotes/origin/v0.2` 决定，不能直接信任 webhook payload 创建 PipelineRun。

`v0.2` 的 GitOps 写路径采用真正写 mirror：promotion 先把 `v0.2-gitops` 写入 `devops-infra` 本地 git mirror/relay，Argo CD 从本地 mirror/relay 读取该 revision 并 rollout，CI 关键路径不等待 GitHub push。GitHub `pikasTech/HWLAB` 仍是长期源码与归档上游，但对 `v0.2-gitops` 来说是由 mirror/relay 负责 flush 的异步上游，不再是 promotion task 的同步写入目标。

## API 接口说明

| 接口 | 说明 |
| --- | --- |
| `scripts/gitops-render.mjs --lane v02` | v02 GitOps render 入口，负责 namespace、runtime path、catalog 和 endpoint 固定。 |
| UniDesk v02 PR auto-CD monitor | 观察并合并 base=`v0.2` 的 ready PR，merge 后调用现有 `trigger-current`、定点 `status` 和 `git-mirror flush`。 |
| Tekton `hwlab-v02-ci-image-publish` | 构建 affected images、复用 unchanged digest，并 promotion 到 `v0.2-gitops`。 |
| `devops-infra` git mirror/relay | 读写 `v0.2` allowlist refs；CI 读 source/catalog、写 `v0.2-gitops`，并通过手动 CLI flush 到 GitHub。 |
| Argo `argocd/hwlab-node-v02` | 从 `devops-infra` 本地 mirror/relay 的 `v0.2-gitops:deploy/gitops/node/runtime-v02` 同步到 `hwlab-v02`。 |
| `http://74.48.78.17:19666/` | v02 Cloud Web 公网入口。 |
| `http://74.48.78.17:19667/health/live` | v02 API/live 公网验收入口。 |

## 规格目标

- `v0.2` 固定作为 G14 上的新增 CI/CD lane，不改写现有 `G14` DEV/PROD lane。
- `v0.2` source branch 只保存源码、人写配置、模板、脚本和文档；CI/CD 生成物只进入 `v0.2-gitops`。
- `hwlab-v02` 是唯一 runtime namespace；`74.48.78.17:19666/19667` 是唯一公网验收入口。
- 共享 G14 k3s、Tekton controller、Argo CD controller、本地 registry、工具镜像和脚本库；隔离分支、catalog、runtime path、Application、Pipeline、ServiceAccount、SecretRef、PVC 和 FRP 入口。
- 旧 DEV/D601/main 门禁不得进入 `v0.2` 发布调用链；新增检查只覆盖固定 branch、namespace、catalog、runtime path、GitOps branch、Argo destination 和公网入口这些硬边界。

## 固定命名

| 对象 | v0.2 规格 |
| --- | --- |
| Source branch | `v0.2` |
| Development workspace | `G14:/root/hwlab-v02`，仅用于人工开发、短连接源码工具和可见性对照，不作为 CI/CD source commit 选择入口 |
| CI/CD source repo | `G14:/root/hwlab-v02-cicd.git` bare repo，由 UniDesk control-plane 自动 fetch `origin/v0.2`，再从 commit-pinned detached worktree render/apply 控制面 |
| GitOps branch | `v0.2-gitops` |
| Artifact catalog | `v0.2-gitops:deploy/artifact-catalog.v02.json` |
| Runtime path | `v0.2-gitops:deploy/gitops/node/runtime-v02` |
| Runtime namespace | `hwlab-v02` |
| Tekton Pipeline | `hwlab-ci/hwlab-v02-ci-image-publish` |
| PR merge auto-CD | UniDesk CLI v02 PR monitor 合并 base=`v0.2` PR 后自动调用 `trigger-current`，并定点观察 merge head |
| Manual trigger | UniDesk CLI `bun scripts/cli.ts hwlab g14 control-plane trigger-current --lane v02 --confirm`，仅作为人工重跑和迁移期补偿入口 |
| Reconcile fallback | UniDesk CLI v02 `auto-cd --once` 或等价补偿入口，对齐当前 `origin/v0.2` head、PipelineRun、GitOps 和 mirror flush 状态 |
| Scheduler/CronJob | 不创建、不保留 v02 k8s CronJob；PipelineRun 只能由 UniDesk CLI PR monitor、manual trigger 或 once reconcile 创建 |
| Tekton ServiceAccount | `hwlab-ci/hwlab-v02-tekton-runner` |
| PipelineRun prefix | `hwlab-v02-ci-poll-<short12>` |
| Argo CD AppProject | `argocd/hwlab-v02` |
| Argo CD Application | `argocd/hwlab-node-v02` |
| FRP Deployment | `hwlab-v02/hwlab-v02-frpc` |
| Web entry | `http://74.48.78.17:19666/` |
| API/live entry | `http://74.48.78.17:19667/health/live` |
| Git mirror/relay | 独立 `devops-infra` 集群读写服务；不设 CronJob；读 source/catalog，写 `v0.2-gitops`，按需由 UniDesk CLI 手动 sync/flush |
| Registry | 维持当前 G14 `hwlab-ci/hwlab-registry` |

`scripts/gitops-render.mjs --lane v02` 是当前 `v0.2` GitOps render 入口。该 lane 必须把默认 source branch 改为 `v0.2`、GitOps branch 改为 `v0.2-gitops`、catalog 改为 `deploy/artifact-catalog.v02.json`、runtime endpoint 改为 `19667`、web endpoint 改为 `19666`，并使用完整 source commit 作为 image tag。

## 真相源

`v0.2` 的发布真相按以下顺序判断：

1. live runtime：`hwlab-v02` namespace 中 Deployment/StatefulSet template、Pod ready、事件、日志和 `19666/19667` 公网 health。
2. Argo desired state：`argocd/hwlab-node-v02` 的 revision、sync、health、source branch 和 runtime path。
3. CI/CD source refs：UniDesk control-plane 专用 bare repo `refs/remotes/origin/v0.2`、devops-infra mirror/relay 的 `refs/heads/v0.2` 与 `refs/mirror-stage/heads/v0.2` 必须共同指向最新 source commit。
4. devops-infra mirror/relay 中的 GitOps branch：`v0.2-gitops` 中的 `deploy/artifact-catalog.v02.json` 与 `deploy/gitops/node/runtime-v02/**`。
5. Tekton 执行证据：UniDesk `trigger-current` 返回的 PipelineRun、TaskRun result、`gitops-promote` 终态。
6. GitHub 上游归档状态：mirror/relay flush 后的 `origin/v0.2-gitops` 与 `origin/v0.2`。
7. 固定开发 workspace：`/root/hwlab-v02` 的 `HEAD`、dirty 状态和 `origin/v0.2` 只作为人工开发对照线索；即使 workspace 脏或落后，也不得影响 CI/CD source commit 选择。

旧 commit 记忆、`G14`/`G14-gitops` DEV/PROD 产物、D601 legacy 路径、GitHub 上游尚未 flush 的短暂落后、source branch 中历史 generated 文件、固定开发 workspace 脏状态和临时 worktree 只能作为线索，不能作为 `v0.2` 发布通过证据。

source workspace 中被 `.gitignore` 忽略的 `deploy/gitops/node/runtime-v02/**` 文件只可能是本地生成缓存。若这类文件与 `v0.2-gitops` 或 live runtime 不一致，应删除本地缓存并以 `v0.2-gitops`、Argo 和 live ConfigMap/Deployment 为准；不要在 source branch 修补 ignored generated 文件，也不要把它们的内容写入 issue closeout 作为发布证据。

## Workspace 与 CI/CD 分离

`v0.2` 开发 workspace 和 CI/CD repo 必须分离。`/root/hwlab-v02` 是人工开发、短连接 `hwlab-cli` 和问题复现的固定 workspace；它允许出现并行任务产生的 untracked `.worktree/`、本地 dirty 文件或临时落后状态。CI/CD 不从该 checkout 的 `HEAD`、工作树 clean 状态或本地 branch 读取待发布 commit，也不得因为该 workspace 脏而跳过、误判或复用旧 PipelineRun。

UniDesk control-plane 必须使用独立 bare repo `/root/hwlab-v02-cicd.git` 作为 CI/CD source repo：每次 `status`、`apply`、`git-mirror apply` 和 `trigger-current` 先自动 fetch `origin/v0.2` 到 `refs/remotes/origin/v0.2`，再用目标 commit 创建 detached temp worktree 运行 `scripts/gitops-render.mjs --lane v02`。该 repo 没有业务工作树，因此不会被人工开发 dirty 状态污染；如果它不可用，应以 `v02-head-unresolved` 或等价结构化错误失败，而不是回退到固定 workspace HEAD。

devops-infra git mirror 仍是 PipelineRun 和 Argo CD 的集群内读写源。`trigger-current --lane v02 --confirm` 在创建 PipelineRun 前必须比较 `expectedSourceHead` 与 mirror `localV02`，不一致时自动执行 bounded `git-mirror sync` Job。`git-mirror status` 必须分别暴露 `localV02`、`githubV02`、`localGitops`、`githubGitops`、`sourceInSync`、`gitopsInSync` 和 `pendingFlush`；`githubInSync` 不得再被理解为只代表 GitOps branch。

## Source 与 GitOps 分层

`v0.2` source branch 可以包含：

- 源码、测试、文档、人写配置和模板。
- `deploy/deploy.yaml` 或等价 lane 配置。
- k8s 模板、render 脚本、CI/CD helper 和 catalog schema。

`v0.2` source branch 不得跟踪：

- `deploy/artifact-catalog.v02.json`。
- `deploy/gitops/node/runtime-v02/**`。
- Tekton/Argo 的 rendered runtime desired state。
- image digest、publish state、reuse evidence 或 CI 生成的 rollout metadata。

如果权限、schema 或 runtime desired state 发生迁移，必须分别核对 source schema/代码、`v0.2-gitops` rendered YAML 和 live runtime。source branch 应只保留当前 migration/ensure 逻辑，`v0.2-gitops` 与 live ConfigMap 应体现发布后的 rendered state，fixed workspace 下 ignored `runtime-v02/postgres.yaml` 即使存在也不能代表真相。

`v0.2-gitops` branch 必须包含：

- `deploy/artifact-catalog.v02.json`，记录 image tag、digest、source commit、component identity、publish/reuse 状态。
- `deploy/gitops/node/runtime-v02/**`，作为 Argo CD 实际消费的 desired state。
- 必要的 generated metadata，但不得包含 Secret 值。

首次初始化时，如果 `v0.2-gitops:deploy/artifact-catalog.v02.json` 尚不存在，只允许由 `v0.2` lane 的正式初始化步骤创建第一版 catalog。不得 fallback 到 `G14` source catalog、`G14-gitops` catalog、DEV runtime path 或 source branch 生成物。

## CI/CD 链路

标准链路如下：

1. PR open/update 阶段只运行 source CI、检查和 PR preflight，不修改 `hwlab-v02` runtime，也不写 `v0.2-gitops`。
2. UniDesk CLI v02 PR monitor 只观察 `pikasTech/HWLAB` base=`v0.2` 的 ready PR；ready PR 不因为其他 commit 的 PipelineRun 仍在运行而阻塞 merge。merge 成功后重新 fetch `/root/hwlab-v02-cicd.git` 的 `origin/v0.2`，以 fetch 后的完整 source commit SHA 作为待发布目标。
3. monitor 调用现有 `hwlab g14 control-plane trigger-current --lane v02 --confirm`。该入口自动复核 `devops-infra` mirror 的 `localV02` ref，必要时执行一次 bounded `git-mirror sync` Job，再创建 commit-pinned `hwlab-v02-ci-poll-<short12>` PipelineRun；同一 source commit 已有 PipelineRun 时只复用现有状态，不删除重建。默认 `--dry-run` 只返回将要创建的 manifest 和 mirror pre-sync 计划。人工重跑和 `auto-cd --once` 补偿必须复用同一入口，不得直接创建 PipelineRun。
4. `prepare-source` 通过 `devops-infra` mirror checkout `v0.2` source，并从 mirror 中的 `v0.2-gitops` 读取上一版 `deploy/artifact-catalog.v02.json`。
5. CI/CD 校验只保留最小构建、TypeScript 语义检查、自动单元测试、打包和必要冒烟检查；旧 DEV/D601/main gate、运行时内部证明型校验、健康诊断重断言和历史预检不进入 lane。
6. planner 根据 component input 判断 affected/reused services。
7. affected service 通过 BuildKit 发布到 G14 本地 registry；reused service 复用 catalog digest。
   所有 selected service 的 build TaskRun 都只依赖 `plan-artifacts`，不按 service 串行排队，也不设置 8 并发或其他 Pipeline 级限流。
   实际并发由 Tekton controller、G14 节点资源、PVC I/O、BuildKit sidecar 和本地 registry 承载能力决定。
   v0.2 保留 runtime service（`hwlab-cloud-api`、`hwlab-cloud-web`、`hwlab-gateway`、`hwlab-edge-proxy`、`hwlab-agent-skills`）必须使用 `env-reuse-git-mirror-checkout`。
   只有 package/runtime/env、env image 构建定义、launcher 输入变化或缺少可复用 env digest 时才构建 `<service>-env` 镜像。
   service source 或 boot script 的 code-only 变化只更新三变量和 GitOps desired state，不再重新构建业务镜像。
   `hwlab-codex-api-forwarder` sidecar 和 `hwlab-deepseek-proxy` 的 `responses-bridge` 是 `hwlab-cloud-api` 的运行面关联容器，复用 `hwlab-cloud-api-env` 并通过各自 boot script 启动；它们不是独立 service matrix 成员，也不得增加第六个 env-reuse service。
   CLI、CaseRun、HWPOD workspace 工具、文档、测试和其他非 service 资产不进入 env-reuse service 触发面。
8. promotion 刷新 `deploy/artifact-catalog.v02.json`，render `deploy/gitops/node/runtime-v02/**`，只在本 PipelineRun 的 source commit 仍是当前 `origin/v0.2` head 时推送到 `devops-infra` mirror/relay 的 `v0.2-gitops`；若 source branch 已推进，本轮输出 superseded/no-op，写出 `runtime-ready-required=false`，不得回写旧 GitOps revision。
9. `hwlab-node-v02` 从本地 mirror/relay 的 `v0.2-gitops:deploy/gitops/node/runtime-v02` 同步到 `hwlab-v02`。
10. UniDesk CLI auto-CD 观察定点 `control-plane status --lane v02 --source-commit <merge-head>` 或 `--pipeline-run <name>`，确认 PipelineRun、Argo、runtime workload、public probes 和 planArtifacts build/reuse 摘要。历史 merge head 若因后续 `origin/v0.2` 推进而被 superseded，PR 评论以 superseded 收口；最新 head 仍必须继续收敛到 runtime。
11. UniDesk CLI 或 mirror/relay flush 操作把本地 `v0.2-gitops` 推送到 GitHub canonical remote；flush 不在 CI runtime-ready 的关键路径内，但自动 CD 收口必须查询 pending、lastFlushed 和 failure，必要时执行 `git-mirror flush --confirm` 并等待 `pendingFlush=false`。
12. 验收只观察 `hwlab-v02` runtime 和 `19666/19667`，并把 source commit、PipelineRun、GitOps revision、build/reuse 摘要、Argo/runtime/public probe 状态写回 PR 或关联 issue。

`trigger-current --lane v02 --confirm` 默认返回异步 job，不等同于 PipelineRun 已经创建完成。调用方必须先运行返回的 `job status <jobId>`，等 `progress.stage=create-pipelinerun` 且 `progress.pipelineCreated=true` 后，再用 `control-plane status --lane v02 --pipeline-run <name>` 做定点发布观察。`progress.pipelineRun` 可以在 control-plane refresh 或 mirror pre-sync 阶段提前给出预期名称；此时如果重型 status 报 `target-pipelinerun-not-found-or-unreadable`，只能说明 PipelineRun 尚未创建，不能作为发布失败、mirror 失败或 runtime 回归结论。

定点发布观察优先使用 `--pipeline-run <name>` 或 `--source-commit <full-sha>`，避免 branch 后续推进导致历史 run 被最新 head 口径误判。`control-plane status` 会同时做 source/mirror/TaskRun/Argo/runtime/web asset 汇总，可能比普通只读查询慢；高频轮询时先看异步 job progress 和 TaskRun 条件摘要，只在阶段收口或异常定位时再跑完整 status。

GitOps promotion 成功后，本地 mirror/relay 的 `v0.2-gitops` 可立即被 Argo 消费；GitHub 上游 flush 是归档与跨节点同步步骤，不在 runtime-ready 关键路径内。`git-mirror status.pendingFlush=true` 时自动 CD 和人工收口都应运行 `git-mirror flush --confirm`，再用该 flush job 的 `job status` 或 `git-mirror status` 确认 `pendingFlush=false`、`localGitops=githubGitops`。不要把 pending flush 误判为 runtime 未发布，也不要在 flush job 仍运行时反复做全量 status。

`v0.2` 可以复用 G14 的 registry、proxy、BuildKit、工具镜像和脚本库；不得复用 `hwlab-node-ci-image-publish`、`hwlab-node-branch-poller`、`hwlab-node-control-plane-reconciler`、`G14-gitops` runtime path 或 DEV/PROD Argo Application 作为 `v0.2` 发布入口。运行时不得为每个版本硬编码 namespace、catalog、runtime path 或健康判断；版本差异只通过 `deploy.yaml.lanes[profile]`、GitOps render 输入和实际 runtime 对象表达，新增版本不得新增运行时代码分支。

`v0.2` 不设 k8s 自动轮询发布。历史上若存在 `hwlab-v02-branch-poller`、`hwlab-v02-control-plane-reconciler` 或等价 CronJob，均视为迁移残留，应由 UniDesk control-plane apply 清理。PR 合并后的自动 CD 只能由 UniDesk CLI monitor 或一次性 reconcile job 驱动，并且必须复用 `trigger-current`、定点 `status` 和 `git-mirror flush`。暂停发布时停止 UniDesk monitor 或停止创建新的 PipelineRun；不要通过新增 CronJob 维持重试。

v02 auto-CD 必须采用 CI 无锁并发与 CD latest-only 语义。不同 source commit 的 PipelineRun 可以并发运行；monitor 不等待旧 run 空闲，也不取消旧 run。同一个 source commit 已有 PipelineRun 时不得删除重建。GitOps promotion 是唯一写入点，必须在写 `v0.2-gitops` 前重新读取 `origin/v0.2` head：只有当前 PipelineRun commit 仍是最新 head 才允许写入；旧 commit 继续跑完 CI 后必须 superseded/no-op 收口，不能把 runtime 或 GitOps branch 滚回旧 commit。`auto-cd --once` 或等价补偿入口用于处理人工 GitHub merge、直接 push、monitor 重启或上次触发失败：它比较最新 `origin/v0.2` head、PipelineRun、Argo/GitOps 和 mirror flush 状态，缺什么补什么，但仍不绕过标准 control-plane 入口。

`devops-infra` git mirror/relay 同样不设周期 CronJob。标准触发命令 `bun scripts/cli.ts hwlab g14 control-plane trigger-current --lane v02 --confirm` 会在创建 PipelineRun 前按需同步 mirror；`bun scripts/cli.ts hwlab g14 git-mirror sync --confirm` 只作为显式 mirror 维护或诊断入口。promotion 成功后可执行 `bun scripts/cli.ts hwlab g14 git-mirror flush --confirm` 把本地 `v0.2-gitops` 推送到 GitHub。`git-mirror apply` 维护 mirror 的 PVC、读服务、写服务、同步/flush 脚本和旧 CronJob 清理；`git-mirror sync` 创建一次性 Job，只同步 allowlist refs `v0.2`、`v0.2-gitops`、`G14` 和 `G14-gitops`，先 fetch 到隐藏 staging refs，校验 commit/tree/object closure，再用 `update-ref` 发布到公开 refs。这样 mirror read path 与 GitOps write path 都落在本地磁盘和集群网络，同时避免 CI 看到 ref 已更新但对象还不可 checkout 的半发布窗口。

mirror 的 HTTP upload-pack 必须允许按精确 commit SHA 拉取已存在对象：`uploadpack.allowReachableSHA1InWant=true` 且 `uploadpack.allowAnySHA1InWant=true`。v0.2 Code Agent 通过 AgentRun 传递 `resourceBundleRef.commitId` 来复现实验时的源码身份，runner 会执行等价的 `git fetch --depth=1 origin <commitId>`；如果 mirror 只允许 advertised refs，旧 runtime commit、GitOps 已推进后的 commit 或隐藏 staging 已验证对象会返回 `Server does not allow request for unadvertised object` 并表现为 `git fetch failed with code 128`。这不是把 AgentRun 降级成浮动 branch fetch 的理由，修复点是 mirror read service 的 upload-pack 策略。

`hwlab-cli` 不属于 v0.2 CI/CD service matrix。它是 `G14:/root/hwlab-v02` 和 v0.2 worktree 内的短连接源码 client，只用 Bun 直接调用 Cloud Web 同源 API；不得加入 PipelineRun `services` 参数、artifact catalog、BuildKit publish、runtime desired state、Deployment、Service、Job 或 image build。CLI-only source 变更推送到 `origin/v0.2` 后，`trigger-current --lane v02` 应表现为 `build=0 reuse=<runtime-services>` 的 source-only fast path，用于证明 source/mirror/GitOps 没有产生旧 runtime artifact 副作用。若出现 `build-hwlab-cli` TaskRun、`hwlab-cli` artifact service、CLI 镜像或 CLI 常驻服务，均视为旧门禁/旧断言残留，直接删除并回到 `docs/reference/spec-v02-hwlab-cli.md` 的短连接 client 口径。

`rpt004:mvp:e2e`、`runner:issue-visibility:preflight` 和 `dev-base-image:preflight` 不属于 v0.2 最小 CI/CD 校验入口；它们代表旧验收、旧 runner 可见性预检或旧镜像基础预检口径。v0.2 `check/validate` 不再引用这些任务，若它们重新进入默认 check plan、package script 或 PipelineRun，应直接删除该入口，而不是为其补兼容逻辑。

### 最小测试/校验机制

v0.2 最小校验的目标是拦截高确定性低级错误，不恢复旧重型门禁。`hwlab-cloud-web` 源码、模板或浏览器 bundle 输入发生变化时，CI 必须在镜像发布和 GitOps promotion 前执行 Cloud Web source check。该 check 至少包含实际 bundle 输入集合的 TypeScript 语义检查、自动发现的单元测试、bundle build 和 dist freshness 校验。

语法检查和 Bun build 不能证明浏览器运行路径安全。`node --check` 只解析语法，`Bun.build()` 只转译和打包，二者都可能放过 `isRequestTraceEvent is not defined` 这类未绑定标识符；因此 Cloud Web check 必须覆盖实际 Vite/React 入口和 `web/hwlab-cloud-web/src/**` bundle 输入，并运行 TypeScript semantic check，例如 `tsc --noEmit` 或等价 Bun/TS checker。该检查失败时不得继续发布 `hwlab-cloud-web` 镜像。

Cloud Web 单元测试必须自动发现并执行 repo-owned `web/hwlab-cloud-web/**/*.test.ts`，不能只依赖手写文件清单。新增 trace、markdown、auth、status 或 HWPOD node-ops 前端纯逻辑测试后，应天然进入 `bun run --cwd web/hwlab-cloud-web check`。trace 渲染核心路径必须有不依赖浏览器、Playwright、公网或真实 provider 的轻量单测，直接构造 `runnerTrace.events` 并调用 trace row/render helper，确保请求事件、setup 事件、tool command summary、assistant markdown 和 completion row 不会因未定义 helper 或数据形态漂移在浏览器运行时崩溃。

默认 v0.2 CI 不启动 Playwright、布局 smoke、移动端截图、旧 quick prompt 检查、旧 M3 evidence 检查或历史 DEV/D601 browser gate。这些检查只能作为显式人工诊断或专项验收命令存在，不能重新进入最小 CI/CD 关键路径。新增测试也必须只表达当前 v0.2 目标行为；发现旧 UI/旧路由/旧门禁断言阻碍当前目标时，删除旧断言而不是维护兼容分支。

### 远端验证短连接规则

G14 host、worktree、k3s 控制面或 pod 内的验证命令必须按短连接可见性设计。通过 UniDesk `ssh`/`tran` route 执行 `npm run web:check`、`npm run web:layout`、Playwright、Tekton/Argo 观察、镜像构建、发布等待或其他可能超过单次维护桥预算的动作时，不得让 SSH 连接长时间阻塞等待完整输出。标准做法是用一次短连接创建后台 job、PipelineRun、Tekton task 或显式脚本 job，把 stdout/stderr、exit code、开始/结束时间和关键元数据写入 `/tmp`、`.state` 或 CI artifact；随后用有界短连接轮询 `status`、`tail`、`exit.code`、TaskRun result 或 Argo summary。

如果一次远端验证已经触发 `UNIDESK_SSH_RUNTIME_TIMEOUT`、日志尾部缺失、exit code 不可见或浏览器/CI 进度不可见，必须先把该验证改成后台 job 加短轮询，再继续排障或发布。禁止把 60s 维护桥断开当作测试失败、测试通过或外部依赖不可用；也禁止通过加大本地等待、重复长连接、全量日志 dump 或改回原生 SSH 绕过来处理。长期 CLI/CI 能力不足时，优先补 UniDesk CLI 的异步 job/status/tail 子命令，再用该入口完成验证。

写 mirror 的一致性模型是 local-first、manual-flush。promotion task 只能持有 mirror/relay 写凭证，不持有 GitHub deploy key；GitHub deploy key 只存在于 `devops-infra` mirror/relay sync/flush 边界。mirror/relay 必须在本地 receive 期间完成 object closure、目标 branch allowlist、non-fast-forward 拒绝和 changed-path 最小校验；receive 成功后本地 ref 即为 Argo 可消费事实。flush 失败不得回滚已经 rollout 的本地 GitOps revision，但必须保留 pending/outbox 状态，下一次手动 flush 可重试并输出 last error，不得静默丢弃。

## 性能预算与回归判定

`v0.2` fast lane 的性能目标是让 code-only/env-reuse 场景主要受 G14 集群调度、PVC 和本地磁盘 I/O 限制，而不是受 GitHub 网络、重复依赖安装或无效 runtime 等待限制。性能预算只用于发现退化和指导排障，不新增发布门禁；发现退化时先按本节做现场热探测，再决定是否需要完整 CI/CD 复跑。

可见性优先于性能数字。
任何优化如果让触发阶段、mirror pre-sync、GitOps promotion、Argo refresh 或 `runtime-ready` 重新变成黑盒等待，即使总耗时暂时更短，也视为 P0 回归。
性能预算必须和日志契约一起判断：每个长等待都要能定位到具体阶段、目标 revision、等待对象和 pending/blocked 摘要。

代表性 env-reuse 场景的对比基线如下。后续更换 runner、PVC、Tekton controller、Argo repo server 或 mirror 存储后，必须重新测量并更新本表的预算口径。

| 场景 | 关键路径总耗时 | 主要阶段表现 | 判定口径 |
| --- | ---: | --- | --- |
| env-reuse + mirror-write 初始路径 | 约 108s | `prepare-source` 约 56s；包含无效 `npm ci` 和多余探针；runtime wait 仍执行 | 只作为旧基线，不应回归。 |
| 移除 `prepare-source` 中的 `npm ci` | 约 76s | `prepare-source` 降到约 21s；其余路径仍有多余探针和 runtime wait | 不应再恢复依赖安装。 |
| 剪裁 fast-path 探针 | 约 50s | `prepare-source` 约 11s；`gitops-promote` 约 7s；`runtime-ready` 约 17s | runtime 有实际变化时的合理预算。 |
| P1 no-op runtime skip | 约 37-40s | 固定阶段预算见下文；`runtime-ready` 跳过 | source-only 且 runtime identity-only 变化时的目标预算；当前代表性实测为 38s。 |

当前预算判定：source-only、所有 service 都复用 artifact、GitOps runtime 只发生 source identity 变化时，总耗时应接近 40s；超过 50s 需要先查是否误触发 `runtime-ready`、是否发生 GitHub 直连、是否恢复了 `npm ci` 或无效 preflight。真正需要 rollout 的 code-only 变更允许约 50s，因为 `runtime-ready` 必须等待 Argo 与 workload 收敛。涉及 BuildKit publish、env image rebuild、registry push 或真实 runtime 滚动时，不适用 40s 预算，应按 affected service 的 build 耗时单独测量。混合变更必须按 service 作用域裁剪：只改 `package.json` 的 `scripts`、旧门禁入口、短连接 CLI、CLI 测试、非 runtime 文档或 HWPOD workspace 工具时，不得触发无关 runtime service 全量 build；若同一变更确实同时改了 `internal/cloud/**`、HWPOD node-ops server code 和 skill bundle，则只允许对应 service build/rollout，其他 service 必须复用 catalog digest。

真实 rebuild 场景必须按全并行 fan-out 判定性能。
`plan-artifacts` 之后所有 affected service build task 应同时进入 Tekton 调度队列，`collect-artifacts` 只做 fan-in 等待全部 build task 写入 service report。
不得为了稳定表面耗时在 Pipeline 拓扑上重新串行化，也不得引入固定 8 并发上限。
全量 rebuild 的关键路径应接近最慢 service build 耗时加上 source、planning、collect、promotion 和 runtime-ready 固定开销，而不是所有 service build 耗时求和。
若全并行导致节点 CPU、内存、PVC I/O、BuildKit sidecar 或 registry 竞争，先通过 TaskRun duration、Pod scheduling、node pressure、registry latency 和 BuildKit 日志定位容量瓶颈。
除非已经证明控制面无法承载，否则不要把容量问题回退成 service 串行依赖。

关键阶段预算如下：

| 阶段 | 正常预算 | 退化信号 |
| --- | ---: | --- |
| `prepare-source` | 约 10-12s | 超过 20s、出现 `npm ci`、访问 GitHub canonical remote、catalog fetch 超过数秒。 |
| `source-clone` | 约 1-2s | 超过 5s 通常说明没有命中 `devops-infra` mirror 或 PVC/网络异常。 |
| `catalog-fetch` | 小于 1s | 超过 3s 先查 mirror refs、object closure 和 catalog branch。 |
| `plan-artifacts` | 约 5-7s | `buildServices` 或 `rolloutServices` 非空但本轮只改 CI/CD 文档/脚本时，先查 component input 与 catalog 是否加载。 |
| `collect-artifacts` | 约 6-8s | 未输出运行时服务矩阵的 `artifact_reuse` 或全部 runtime services reuse 时，先查 catalog digest、PipelineRun `services` 参数和 service identity；`hwlab-cli` 不应出现在 build/reuse 统计中。 |
| `gitops-promote` no-op | 约 7-9s | 未输出 `skipped-runtime-unchanged`，或写入了 `v0.2-gitops`，说明 runtime 比对未命中。 |
| `runtime-ready` | no-op 应跳过；真实 rollout 约 15-20s | no-op 场景出现 TaskRun 即为 P1 退化；真实 rollout 超时则按 Argo/workload 排障。 |

真实 rollout 若超过约 20s，先区分是 workload 本身启动慢，还是 `runtime-ready` 观察集合过大。正常日志应带 `observedCount`，且该值应接近本轮 `rolloutServices` 数量加上明确连带依赖；如果 `workloadCount` 很大但 `observedCount` 缺失，或 FRP、Postgres 等无关服务在 cloud-web/cloud-api 变更中重启，通常说明 render 把复用服务的 Pod template 绑定到了全局 source commit。修复方向是收窄 `runtime-ready` 观察集合，并把复用服务 template identity 改成 artifact commit、boot commit、config hash 或 migration hash，而不是加大 timeout 或恢复全 namespace 等待。

## 性能优化原理

`prepare-source` 不安装依赖。该 task 只负责 checkout source、读取上一版 catalog、输出 source identity 和基础文件，不运行 `npm ci`、不探测 npm registry、不做与后续 task 重复的工具链检查。需要依赖的语法检查、render test 或 build task 必须使用各自镜像内已有工具或在专属阶段处理，不能把全仓依赖安装塞回 source 准备阶段。

Git 读写都走 `devops-infra` 本地 mirror/relay。读路径用 HTTP mirror checkout `v0.2` source 和 `v0.2-gitops` catalog；写路径把 promotion 推到 mirror/relay 的 `v0.2-gitops`，Argo 也从 mirror/relay 读取。这样 source clone、catalog fetch、GitOps clone/push 都落在集群内网络和本地磁盘，GitHub 只通过手动 `sync`/`flush` 进入或离开本地 mirror，不在 CI 关键路径内。

任何仍需要访问 GitHub canonical remote 的 SSH 操作都必须显式走 G14 proxy。Host SSH config 可以使用 OpenBSD `nc -X connect -x 127.0.0.1:10808 %h %p`；Tekton `GIT_SSH_COMMAND`、git-mirror `sync`/`flush` 和其他容器内 `git@github.com` 或 `ssh://git@ssh.github.com:443` 路径必须使用 repo-owned Node HTTP CONNECT ProxyCommand，并保留短 `ConnectTimeout` 与 `ServerAlive*`。只改 `HTTP_PROXY`/`HTTPS_PROXY` 对 OpenSSH 无效；BusyBox `nc` 不支持 `-X connect`，不得再把 `ssh.github.com:443` 当作等价 proxy 或让 GitHub SSH 直连反复 60s 超时。

env image 复用把系统依赖和业务代码身份分离。`environmentDigest` 表示可复用运行环境，`HWLAB_BOOT_REPO`、`HWLAB_BOOT_COMMIT` 和 `HWLAB_BOOT_SH` 表示本次代码启动身份。service code-only 变更只更新 boot metadata 和 runtime identity，不发布新 env image；只有真实 env 输入、env image 构建定义、launcher 输入变化或缺少可复用 env digest 时才进入 env rebuild。AgentRun v0.1 是 HWLAB 外部共享执行基础设施，`hwlab-agent-worker` 不再作为 v0.2 service image 或 suspended Job template 参与 planner、BuildKit、artifact catalog 或 GitOps render。`package.json` 只按 dependency/runtime 字段参与 env/runtime hash，`scripts` 只属于开发入口，不得因为清理旧门禁脚本而重建所有 service 或重建 env image。CLI、CaseRun、HWPOD workspace 工具、文档和测试不属于 v0.2 service runtime 输入，不得因为 env reuse 机制被纳入 service rollout 或 build 范围；若某个工具文件确实被浏览器或服务源码直接 import，必须作为对应 service 的显式 component path 建模，而不能通过宽泛 shared path 扩散到所有服务。env image 构建定义只包含真正改变 `<service>-env` 镜像内容或启动器行为的文件，不能把 planner、测试、CLI 或 host helper 混入环境输入。

BuildKit publish 采用 service 级全并行 fan-out。
每个 service build task 拥有独立 BuildKit sidecar、独立 service workdir 和独立 report 文件，均从同一 `plan-artifacts` 结果判断是否需要执行。
跳过的 task 由 Tekton `when` 表达式裁剪，执行的 task 并行写入 `/workspace/source/service-results/<serviceId>.json`。
`collect-artifacts` 只从 report 目录收敛结果，不再通过 `tasks.build-*.results` 直接依赖某个前序 task 输出，因此全并行不会改变 artifact 身份语义。

全并行不是无限资源承诺，而是 DAG 不主动限流。
CI/CD 拓扑只表达依赖：`prepare-source` 输出 source/catalog，轻量检查并行完成后进入 `plan-artifacts`。
所有被选中的 `build-*` task 只依赖 `plan-artifacts`，最后由 `collect-artifacts` fan-in。
实际同一时刻运行的 Pod 数由 Tekton controller、Kubernetes scheduler、G14 node 资源、PVC I/O、BuildKit sidecar 和本地 registry 共同决定。
如果这些节点因为容量不足自然排队，可以接受真实并发低于 selected service 数。
不允许为了掩盖排队把 Pipeline YAML 改回服务串行链或固定并发上限。

限制全并行的关键节点如下：

- `Tekton controller` 与 Kubernetes API 负责把 selected build task 展开成 TaskRun/Pod。
  控制面慢会让 TaskRun 创建或状态更新延迟；退化信号是 `plan-artifacts` 已完成但 build TaskRun 创建时间分散。
- Kubernetes scheduler 与 G14 node 决定 Pod 是否能同时落到节点。
  CPU、内存、ephemeral storage 和 image pull 会限制实际并行度；退化信号是 Pod `Pending`、`Unschedulable`、`OOMKilled`、`Evicted` 或 node pressure event 增多。
- shared workspace PVC 与本地磁盘承载 source 读取、service workdir 复制、report 写入和 BuildKit 层数据。
  退化信号是 build step 开始慢、`cp`/`tar`/workspace 操作耗时异常、Pod I/O wait 高或 PVC/local-path event。
- 每个 TaskRun 的 BuildKit sidecar 独立运行，但同时消耗 CPU、内存、overlayfs、socket readiness 和 layer cache I/O。
  退化信号是 `buildkitd` 启动慢、socket unavailable、build context upload 慢、sidecar OOM 或 build step 等待 daemon。
- `hwlab-ci` 本地 registry 是全并行 rebuild 后半段的共享写点，承载所有 affected service 的 layer 和 manifest push。
  退化信号是 push 耗时拉长、连接 reset、5xx、blob upload timeout 或 registry Pod CPU/I/O 压力。
- `devops-infra` git mirror/relay 影响 `prepare-source`、catalog fetch 和 `gitops-promote` 的固定开销。
  它通常不在 build fan-out 中；退化信号是 clone/catalog 超过预算、promotion push 慢或 mirror pending/outbox 堆积。

并发回归的高风险修改必须提前识别。
`build-*` task 之间新增 `runAfter` 会把关键路径改成服务耗时求和。
把所有 build 写到同一个 report 或共享 workdir 会产生覆盖和竞态。
直接依赖 `tasks.build-*.results` 会让 skipped task 的结果解析变脆。
在 build task 内修改 `/workspace/source/repo` 会污染其他并发 task 的输入。
对不同 service 复用同一 image repo/tag 会造成 registry tag 覆盖。
把容量抖动误判为拓扑问题会导致重新串行化。
正确做法是保持 source tree 只读、每个 service 独立 workdir 和 report、每个 service 独立 image repo。
先按容量节点定位真实瓶颈，再决定是否增加资源或修 sidecar/registry/mirror。

planner 必须按 service component model 裁剪 build。`tools/` 下的短连接 CLI 源码不是 runtime service 输入；`skills/` 只有 agent runtime 或 skills bundle 真正消费的子目录才影响对应 runtime；HWPOD workspace tools、`hwpod-cli`、`hwpod-ctl` 和 `hwpod-compiler-cli` 属于 Code Agent workspace/skills 分发输入，不得让 cloud-api、gateway、edge-proxy 等服务重建。CI 读取 artifact catalog 时支持 repo 相对路径和绝对路径，便于用真实 catalog 做本地热探测；catalog 缺失才允许 fail closed 到 rebuild，不得把“诊断命令没读到 catalog”误判为生产路径必须全量构建。

P1 no-op runtime skip 只跳过无实际 runtime 变化的等待。planner 输出 `buildServices=[]` 且 `rolloutServices=[]` 后，`gitops-promote` 会在写入前对旧 `runtime-v02` 与新 render 结果做归一化比较：忽略 source commit、artifact source commit、boot commit 和等价 commit env/annotation 这类 identity-only 字段。如果归一化结果相同，promotion 输出 `skipped-runtime-unchanged`，把 Tekton result `runtime-ready-required=false` 写出，并跳过 GitOps commit、push、Argo hard refresh 和 `runtime-ready`。如果 workload spec、image digest、env image、SecretRef、Service、Ingress、FRP、ConfigMap 或 rollout service 有实际变化，必须保持 `runtime-ready-required=true` 并等待 runtime 收敛。

`runtime-ready` 只能观察本轮 `rolloutServices` 对应的 workload readiness，不能把整个 `hwlab-v02` namespace 的全量 workload 都绑定到当前 source commit 后等待。复用 service 的 Deployment metadata 可以记录本次 source commit 作为 GitOps revision 线索，但 Pod template identity 必须来自该 service 的 artifact/runtime commit、boot commit 或配置内容 hash；否则 catalog 复用会被误转成不必要 rollout，`runtime-ready` 会被无关服务拖慢。`hwlab-cloud-api` 的 rollout 允许连带观察 `hwlab-deepseek-proxy`，因为 deepseek bridge 复用 cloud-api 镜像作为运行载体；其他连带关系必须有明确运行依赖后再加入观察集合。基础对象如 FRP、Postgres 的 Pod template 不得写入全局 source commit，应用配置 hash 或 migration hash 表达真实重启边界。

`runtime-ready` 的可见性优先级高于耗时优化。Task 启动后必须立即输出 `runtime-ready started` 事件，明确 `readinessMode`、观察服务数和当前 revision；等待 Argo、source commit refresh 或 workload ready 时必须周期性输出 `progress` 事件，包含 `observedCount`、`workloadCount`、pending/blocked 摘要或 Argo sync/health 状态。service rollout 场景继续用 `rolloutServices` 对应的 Pod template source commit 判定；`rolloutServices=[]` 但 GitOps runtime 真实变化的 infra-only 场景不得黑盒等待 source commit，应改为等待 Argo Application `Synced/Healthy` 和 workload generation ready，并输出 `argo-sync-health` 事件。禁止再出现 runtime-ready 只在 240s timeout 后才输出一条日志的不可观察状态。

触发侧也必须可见。
`hwlab g14 control-plane trigger-current --lane v02 --confirm` 默认作为异步 job 返回 `job.id`、`statusCommand`、stdout/stderr 路径。
同步调试使用 `--wait` 时，stderr 必须输出 `hwlab.v02.trigger.progress` JSON 事件。
事件至少覆盖 `control-plane-refresh`、`git-mirror-pre-sync`、`delete-existing-pipelinerun` 和 `create-pipelinerun`。
每个阶段都要有 `started` 与 `succeeded` 或 `failed`，并携带足够定位的信息，例如 mirror sync job、mode、PipelineRun name 或耗时。
异步 job 的 stderr tail 也必须能看到同一类进度事件。
如果 trigger 卡住但只有一条启动命令或空日志，应先修 UniDesk CLI 可见性，不要把问题推给 Tekton 或反复重跑 Pipeline。

Pipeline 内部的 no-op fast lane 必须形成完整证据链。
`prepare-source` 输出 `source-clone` 与 `catalog-fetch` timing。
`plan-artifacts` 输出 `ci-plan`，其中 `buildServices=[]`、`rolloutServices=[]` 且 runtime service 全部 reuse。
`gitops-promote` 输出 `skipped-runtime-unchanged`、`runtime-identity-only`，并把 `gitops-commit` 与 `gitops-push` 标为 `skipped`。
PipelineRun 的 skipped tasks 中必须包含 `runtime-ready`。
这条证据链缺一项时，不能只看总耗时通过，应先确认是日志缺失、planner 误判还是 GitOps runtime 比对退化。

真实 rollout 的 `runtime-ready` 必须 fail-closed 且可观察。
service rollout 只能等待本轮 `rolloutServices` 对应 workload 的 Pod template source commit 与 readiness。
infra-only runtime change 只能等待 Argo Application `Synced/Healthy` 和 workload generation ready。
若 `runtime-ready` 超时但日志里没有 `started`、`progress`、`argo-refresh`、`argo-sync-health`、`workload-ready` 或 `observedCount`，先按可见性回归修复日志和观察集合。
再讨论 timeout、性能或容量问题。

Kubernetes label 里的配置 hash 必须使用短 hash，完整 64 位 sha256 只能放 annotation。FRP `config-sha256`、Postgres `migration-sha256` 这类字段如果进入 Pod template label，长度必须小于 Kubernetes label value 的 63 字节上限；annotation 保留完整 hash 用于排障和人工比对。Argo sync 报 `must be no more than 63 bytes` 时，优先检查是否把完整 sha256 写入 label，不要通过删除 hash、放宽 sync 或忽略 OutOfSync 绕过。

快速优化不能绕过 fail-closed 语义。mirror miss、commit ancestry 不合法、boot script 缺失、digest 缺失、Argo observer RBAC 不足、workload 未 ready 或公网 health 不一致，都不能为了追求耗时而降级为 warning。允许跳过的只有已经证明 runtime desired state 没有实际变化的等待。

## 快速排障

触发后先看异步 job 状态，确认卡点在 UniDesk trigger、mirror pre-sync、PipelineRun 创建，还是 Tekton 内部阶段。
正常触发日志应出现 `hwlab.v02.trigger.progress` 事件，且阶段至少包括 `control-plane-refresh`、`git-mirror-pre-sync`、`delete-existing-pipelinerun` 和 `create-pipelinerun`。

```bash
bun scripts/cli.ts job status <trigger-job-id> --tail-bytes 30000
```

如果 trigger job stderr 没有进度事件，不要直接反复触发 CI/CD。
先修 trigger 可见性或查询 `.state/jobs/<jobId>.stderr.log`，让等待点重新暴露出来。
mirror pre-sync 慢时看 mirror sync job 与 `git-mirror status`；PipelineRun 创建后才进入 Tekton 排障。

先看一次 PipelineRun 总览，确认是否是性能退化、构建退化还是 runtime 收敛失败：

```bash
bun scripts/cli.ts ssh G14:k3s script -- 'set -eu
ns=hwlab-ci
pr=<pipeline-run-name>
kubectl get pipelinerun -n "$ns" "$pr" \
  -o jsonpath="status={.status.conditions[0].status}{\"\\n\"}reason={.status.conditions[0].reason}{\"\\n\"}message={.status.conditions[0].message}{\"\\n\"}start={.status.startTime}{\"\\n\"}done={.status.completionTime}{\"\\n\"}"
echo taskruns
kubectl get taskrun -n "$ns" -l tekton.dev/pipelineRun="$pr" \
  -o jsonpath="{range .items[*]}{.metadata.name}{\"\\t\"}{.metadata.labels.tekton\\.dev/pipelineTask}{\"\\t\"}{.status.conditions[0].status}{\"\\t\"}{.status.conditions[0].reason}{\"\\t\"}{.status.startTime}{\"\\t\"}{.status.completionTime}{\"\\n\"}{end}" | sort -k2,2
echo skipped
kubectl get pipelinerun -n "$ns" "$pr" \
  -o jsonpath="{range .status.skippedTasks[*]}{.name}{\"\\n\"}{end}" | sort
'
```

再抓关键日志。正常 no-op fast lane 应看到 `source-clone` 约 1-2s、`catalog-fetch` 小于 1s、所有 runtime services reuse、`skipped-runtime-unchanged`、`gitops-commit` 和 `gitops-push` 为 `skipped`，并且 `runtime-ready` 出现在 skipped tasks 中。输出中不应出现 `build-hwlab-cli`。

```bash
bun scripts/cli.ts ssh G14:k3s script -- 'set -eu
ns=hwlab-ci
pr=<pipeline-run-name>
for task in prepare-source plan-artifacts collect-artifacts gitops-promote runtime-ready; do
  tr=$(kubectl get taskrun -n "$ns" \
    -l tekton.dev/pipelineRun="$pr",tekton.dev/pipelineTask="$task" \
    -o jsonpath="{.items[0].metadata.name}" 2>/dev/null || true)
  [ -n "$tr" ] || { echo "--- $task skipped/no-taskrun ---"; continue; }
  pod=$(kubectl get pod -n "$ns" -l tekton.dev/taskRun="$tr" \
    -o jsonpath="{.items[0].metadata.name}")
  echo "--- $task $pod selected logs ---"
  pattern="skipped-runtime-unchanged|runtime-ready-required|runtime-identity-only"
  pattern="$pattern|node-cicd-timing|git-operation|ci-plan|artifact_reuse|buildSkippedCount"
  pattern="$pattern|gitops-commit|gitops-push|runtime-ready|progress|argo-refresh"
  pattern="$pattern|argo-sync-health|workload-ready|observedCount"
  kubectl logs -n "$ns" "$pod" --all-containers=true | \
    grep -E "$pattern" || true
done
'
```

常见故障按以下顺序处理：

| 现象 | 优先检查 | 处理原则 |
| --- | --- | --- |
| `prepare-source` 回到 20s 以上 | 日志是否出现 `npm ci`、npm registry probe、GitHub URL、SSH setup 或长时间 catalog fetch | 拆掉无效依赖安装和重复探针；读路径必须回到 mirror。 |
| `source-clone` 超过 5s | PipelineRun 参数 `git-read-url`、mirror service、PVC I/O、`git-mirror status` | 不要改回 GitHub 直连；先修 mirror/read service。 |
| catalog 缺失导致全量 build | `prepare-source` 是否从 `v0.2-gitops` 取到 `deploy/artifact-catalog.v02.json` | 先 `git-mirror sync --confirm`；必要时查 mirror refs/object closure。 |
| `buildSkippedCount` 偏低或突然变成 0 | `ci-plan` 的 `affectedServices`、`buildServices`、`rolloutServices`、catalog 加载状态、`package.json` 字段 diff 和 service component path | 真实业务变更可以 build；脚本清理、CLI/测试/文档、host asset 或 code-only env-reuse 变更不应误触发全量 build。 |
| 多 service rebuild 仍串行执行 | `tekton-v02/pipeline.yaml` 中所有 `build-*` task 的 `runAfter`，以及 TaskRun startTime 是否都紧跟 `plan-artifacts` | build task 只能依赖 `plan-artifacts`；发现 `build-B` 依赖 `build-A` 时直接修 render 脚本和生成物，不保留串行兼容路径。 |
| 全并行 rebuild 变慢或不稳定 | TaskRun duration、Pod Pending/Unschedulable、node pressure、PVC I/O、BuildKit sidecar 日志、本地 registry push latency | 优先定位资源容量或 registry/BuildKit 瓶颈；不要先把 Pipeline 拓扑改回串行或固定 8 并发。 |
| no-op 仍执行 `runtime-ready` | `gitops-promote` 是否输出 `runtime-ready-required=false` 和 `skipped-runtime-unchanged` | 查 runtime 归一化比较；不要直接删除 `runtime-ready`，只修 no-op 判定。 |
| trigger 或 mirror pre-sync 黑盒等待 | `job status <trigger-job-id>` stderr 是否输出 `hwlab.v02.trigger.progress` | 先恢复 trigger 进度事件；不要重复触发或手工创建 PipelineRun 绕过。 |
| `runtime-ready` timeout 后才输出一条日志 | 是否缺少 `started`、`progress`、`workload-ready` 或 `observedCount` | 这是可见性回归；先修日志和观察集合，不要先加大 timeout。 |
| mirror `pendingFlush=true` 长期存在 | `bun scripts/cli.ts hwlab g14 git-mirror status` 和最近一次 flush 错误 | 手动 `git-mirror flush --confirm`；flush 不影响已 rollout 本地 revision，但不能静默积压。 |
| Argo `Synced/Healthy` 但公网不通 | `hwlab-v02-frpc` 日志、master frps allowlist、`19666/19667` | 修 FRP allowlist 或 frpc；不要把 DEV/PROD 入口当作 v02 证据。 |
| `runtime-ready` 超时 | Argo app revision、workload Pod template source commit、Pod events、observer RBAC | 真实 rollout 必须 fail closed；先定位 Argo 或 workload，不要降低等待为 warning。 |

全并行故障定位先判定是拓扑退化、调度排队、资源压力还是共享写点瓶颈。
不要用一次 PipelineRun 总耗时直接得出“并发失败”结论。
必须同时看 `plan-artifacts` 完成时间、每个 build TaskRun 创建时间、Pod startTime、container finishTime 和 build/push 日志。

检查 live Pipeline DAG，确认所有 build task 没有互相依赖：

```bash
bun scripts/cli.ts ssh G14:k3s script -- 'set -eu
ns=hwlab-ci
pipeline=hwlab-v02-ci-image-publish
kubectl get pipeline -n "$ns" "$pipeline" \
  -o jsonpath="{range .spec.tasks[*]}{.name}{\"\\t\"}{.runAfter}{\"\\n\"}{end}" | \
  grep "^build-"
'
```

检查一次 PipelineRun 的真实并发时间线：

```bash
bun scripts/cli.ts ssh G14:k3s script -- 'set -eu
ns=hwlab-ci
pr=<pipeline-run-name>
kubectl get taskrun -n "$ns" -l tekton.dev/pipelineRun="$pr" \
  -o custom-columns=START:.status.startTime,DONE:.status.completionTime,\
STATUS:.status.conditions[0].status,REASON:.status.conditions[0].reason,\
TASK:.metadata.labels.tekton\\.dev/pipelineTask,NAME:.metadata.name \
  --no-headers | \
  grep -E "build-|plan-artifacts" | sort
'
```

检查调度和节点压力：

```bash
bun scripts/cli.ts ssh G14:k3s script -- 'set -eu
ns=hwlab-ci
pr=<pipeline-run-name>
kubectl get pod -n "$ns" -l tekton.dev/pipelineRun="$pr" -o wide
kubectl get events -n "$ns" --sort-by=.lastTimestamp | tail -80
kubectl describe node | grep -E "Name:|Pressure|Allocatable|Allocated resources|cpu|memory|ephemeral-storage" || true
'
```

检查 BuildKit 和 registry 共享写点。
先用 TaskRun 找到对应 Pod，再分别看 build step、BuildKit sidecar 和 registry Pod 日志。
如果 build step 卡在 push 或 sidecar 等待，优先处理 registry/BuildKit/PVC，而不是修改 Pipeline 并发拓扑。

```bash
bun scripts/cli.ts ssh G14:k3s script -- 'set -eu
ns=hwlab-ci
pr=<pipeline-run-name>
task=<build-task-name>
tr=$(kubectl get taskrun -n "$ns" \
  -l tekton.dev/pipelineRun="$pr",tekton.dev/pipelineTask="$task" \
  -o jsonpath="{.items[0].metadata.name}")
pod=$(kubectl get pod -n "$ns" -l tekton.dev/taskRun="$tr" \
  -o jsonpath="{.items[0].metadata.name}")
kubectl logs -n "$ns" "$pod" --all-containers=true | \
  grep -E "buildkit|daemon|socket|push|manifest|blob|timeout|reset|no space|OOM|killed" || true
kubectl get pod -n "$ns" -o name | grep -E "registry|hwlab-registry" || true
'
```

并发相关故障按下表归类，避免把不同问题混成“并行不稳定”：

| 故障类型 | 判定信号 | 修复方向 |
| --- | --- | --- |
| 拓扑退化 | build TaskRun 的 `runAfter` 出现其他 build task；startTime 严格一个接一个且没有 Pod Pending 证据。 | 修 render 和 live Pipeline，恢复 build task 只依赖 `plan-artifacts`。 |
| 控制面或调度排队 | `plan-artifacts` 完成后 TaskRun/Pod 创建分散，event 出现调度等待。 | 查 Tekton controller、scheduler、API latency 和 node 可用资源。 |
| 节点资源压力 | Pod `Pending`、`OOMKilled`、`Evicted`、ephemeral storage 不足或 node pressure。 | 增加/释放资源、调整单 task resource request，不能靠串行化掩盖。 |
| PVC 或本地磁盘瓶颈 | build step 在复制 source、上传 context、写 layer 或写 report 时集体变慢。 | 减少共享写、确认 source tree 只读、必要时优化 service workdir 和 BuildKit 存储。 |
| BuildKit sidecar 竞态 | step 日志出现 daemon/socket not ready、sidecar OOM 或 buildkitd 异常退出。 | 修 sidecar readiness、资源和日志；不要把问题转成 build task 互相依赖。 |
| registry 写瓶颈 | 多个 build 同时 push 时出现 5xx、reset、blob timeout 或 registry Pod 压力。 | 查 registry Pod、存储和网络；必要时提升 registry 资源或存储能力。 |
| report fan-in 异常 | `collect-artifacts` 找不到某个 service report，或多个 service 报告内容互相覆盖。 | 确认 report 文件名按 serviceId 隔离，失败 task 能输出明确失败报告。 |
| skipped task 误判 | selected service 正确跳过，但 collect 误读为失败或缺 digest。 | 保持 collect 读 catalog+report 目录，不恢复 `tasks.build-*.results` 硬依赖。 |
| 下游 promotion 慢 | build 全部完成后才慢，日志集中在 mirror push、Argo refresh 或 runtime-ready。 | 按 mirror/GitOps/runtime 排障；这不是 build 并发问题。 |

性能退化排障结束后，只把可复用的预算、原理和入口更新到本文；一次性 PipelineRun 名称、Pod 名称、日志全文和临时证据放到 issue 或 PR，不写入长期规格。

## Env 容器复用与三变量启动

`v0.2` code-only fast lane 的 runtime desired state 由三类输入组成：可复用 env image digest、自动推导的 code boot metadata、以及既有 service runtime config。开发者仍按当前 DEV/OPS 流程提交 `v0.2` source commit 和维护 `deploy/deploy.yaml`；发布入口不得要求人工填写 repo、commitId 或 boot script 路径。

code boot metadata 固定映射到三个启动环境变量：

| 变量 | 自动推导来源 | 约束 |
| --- | --- | --- |
| `HWLAB_BOOT_REPO` | `v0.2` lane 的 canonical GitHub source repo 配置 | 必须是 canonical GitHub URL；用于身份记录，运行时读取由 resolver 自动分流到 mirror/cache。 |
| `HWLAB_BOOT_COMMIT` | UniDesk `trigger-current` 解析到的 `origin/v0.2` 完整 source commit SHA，也就是 PipelineRun revision | 必须是完整 40 位 commit SHA；禁止 branch、tag、`latest` 或人工覆盖。 |
| `HWLAB_BOOT_SH` | service model 或 `deploy/deploy.yaml` 中 serviceId 到 boot script 的映射，默认形态为 `deploy/runtime/boot/<serviceId>.sh` | 必须是 repo 内相对路径；禁止绝对路径、`..` 越界和从 env image 中隐式寻找旧脚本。 |

CI/CD 必须把三变量同时写入 `deploy/artifact-catalog.v02.json`、rendered workload Pod template env/annotation 和 runtime health identity。三变量是由 lane 自动推导的发布事实，不是人工 OPS 参数；如果自动推导缺失或无法证明 `HWLAB_BOOT_COMMIT` 属于 `v0.2` 允许 ancestry，本轮 promotion 必须失败。

`HWLAB_BOOT_REF` 是由 lane 的 source branch 派生的 mirror transport hint，
不属于发布身份三变量，也不得作为人工 OPS 参数。env-reuse launcher 必须先通过
`HWLAB_BOOT_REF` 读取 mirror 中可广告的分支/ref，再 checkout `HWLAB_BOOT_COMMIT`；
不得直接 `git fetch <sha>` 依赖 smart HTTP 对 unadvertised object 的支持。
这样可以稳定支持历史 commit rollout、Argo 回滚和 env image 复用，
同时保持发布身份仍由 `HWLAB_BOOT_REPO`、`HWLAB_BOOT_COMMIT`、`HWLAB_BOOT_SH` 三变量表达。

推荐启动形态是 initContainer + `emptyDir` + generic env image。initContainer 或 env image 内的 launcher 读取三变量，通过 resolver 把 `HWLAB_BOOT_REPO` 的只读 fetch/checkout 自动分流到 `devops-infra` git mirror/cache，按 `HWLAB_BOOT_COMMIT` checkout 到 Pod 私有 `emptyDir`；main container 复用同一 env image，并执行 checkout 后代码目录里的 `$HWLAB_BOOT_SH`。env image 只承载系统依赖、runtime、launcher、git client 和通用工具，不把业务代码或旧 boot script 当作运行真相。

service code-only 变更时，planner 必须输出 `envChanged=false`、`codeChanged=true`，跳过 BuildKit image publish，复用上一版 env image digest，只更新 catalog 和 workload Pod template 中的 `HWLAB_BOOT_COMMIT`、code identity annotation 与相关 health metadata。非 service 变更必须输出空 `affectedServices` 或只影响自身所属的非 runtime 流程，不得为了 env reuse 触发 runtime service rollout。Kubernetes 原生 Deployment/StatefulSet rolling update 仍由 Pod template hash 变化触发；不需要在容器内长期驻留 watcher，也不把 `git pull` 结果作为运行真相。

`devops-infra` git mirror/relay 是 allowlisted、PVC-backed 或等价持久化服务，负责缓存 GitHub 对象并承接 `v0.2-gitops` 本地写入。只有 mirror/relay sync/flush 边界持有 GitHub 远端凭证；`hwlab-v02`、AgentRun 和其他业务 namespace 只能访问只读 mirror/cache endpoint，不持有 GitHub deploy key。runtime checkout 失败、mirror miss 超过明确等待窗口、commit ancestry 不满足 lane 约束或 boot script 校验失败时，Pod 必须启动失败或 NotReady，不得回退到 env image 内旧代码，也不得在业务 namespace 直连 GitHub 拉取。

git mirror/relay 接入方式遵循“CI 写本地、GitHub 异步归档、读自动加速”。GitOps 和 deploy spec 中的 repo 字段仍记录 canonical GitHub URL 作为身份字段；launcher 或只读 runtime image 内的 resolver 负责读路径自动分流。需要 push `v0.2-gitops` 的 promotion task 必须 push devops-infra 本地 mirror/relay，不得在 CI 关键路径直接 push GitHub canonical remote。

registry 与 git mirror/relay 分属不同基础设施边界。registry 保持当前 G14 `hwlab-ci/hwlab-registry`，继续通过 repository prefix 服务 HWLAB、AgentRun 和后续 lane；新增 `devops-infra` git mirror/relay 不触发 registry 迁移，也不要求在 `devops-infra` 复制 registry。

## Artifact 与镜像身份

- `v0.2` 镜像 tag 使用完整 40 位 source commitId。
- runtime manifest 必须使用 digest pin 作为部署身份。
- catalog 必须记录 lane/profile、source branch、GitOps branch、source commitId、serviceId、image tag、digest、component identity 和 publish/reuse 状态；启用 env 容器复用时还必须记录 `runtimeMode=env-reuse-git-mirror-checkout`、`environmentImage`、`environmentDigest`、`environmentInputHash`、`bootRepo`、`bootCommit`、`bootSh`、`codeInputHash` 和三变量写入证据。
- 同一 source commit 对同一 service 应生成同一镜像；lane 差异放在 manifest、env、SecretRef、namespace、FRP 和 DB 配置中，不 bake 进镜像。
- `deploy/deploy.yaml` 只承载人写 runtime intent，不承载 digest、publish state 或 reuse evidence。

## Kubernetes 与 Argo 边界

- `hwlab-v02` namespace 只能由 `v0.2` GitOps lane 管理。
- `argocd/hwlab-v02` AppProject destination 只能包含 `hwlab-v02`。
- `argocd/hwlab-node-v02` source 必须指向 `devops-infra` 本地 mirror/relay 中的 `v0.2-gitops:deploy/gitops/node/runtime-v02`，destination 必须是 `hwlab-v02`。
- `hwlab-v02-frpc` 只能暴露 `19666/19667`，不能复用 `17666/17667` 或 `18666/18667`。
- `v0.2` Secret、DB 凭据、PVC、ServiceAccount 和 runtime config 必须独立命名或独立 namespace scope；文档、issue、trace 和 report 只记录 SecretRef 名称与 key，不记录值。
- `v0.2` 初期不新增自动 registry GC；后续如启用清理，selector 必须带 lane/profile 标签，registry digest 保护集必须同时覆盖 `G14-gitops` 与 `v0.2-gitops`。

## 硬边界

以下边界是 `v0.2` CI/CD 的最小硬约束：

- source branch 必须是 `v0.2`。
- PR 自动 CD 只能观察和合并 base=`v0.2` 的 PR；其他 base branch 不得触发 v02 runtime rollout。
- GitOps branch 必须是 `v0.2-gitops`。
- runtime namespace 必须是 `hwlab-v02`。
- artifact catalog 必须是 `deploy/artifact-catalog.v02.json`。
- runtime path 必须是 `deploy/gitops/node/runtime-v02`。
- Argo Application 必须是 `hwlab-node-v02`，且只能部署到 `hwlab-v02`。
- source branch publish 后不得出现 `deploy/artifact-catalog.v02.json` 或 `deploy/gitops/node/runtime-v02/**` 变更。
- source workspace 下若残留 ignored `deploy/gitops/node/runtime-v02/**` 生成物，不能作为 source diff、PR 内容、回归证据或人工修补目标；发现和当前 `v0.2-gitops` 不一致时删除本地缓存。
- GitOps promotion 的 changed paths 只能落在 `deploy/artifact-catalog.v02.json` 与 `deploy/gitops/node/runtime-v02/**` 及必要的 v02 Argo/GitOps 元数据。
- 公网验收只能使用 `19666/19667`。
- `v0.2` 不得创建或依赖 k8s CronJob；`hwlab-v02-branch-poller`、`hwlab-v02-control-plane-reconciler` 或同类调度器出现时应清理，而不是接入发布链路。
- GitHub webhook 不是 v02 首期自动 CD 的必需入口；若未来接入，只能作为唤醒信号，不能直接决定发布 commit 或创建 PipelineRun。
- 自动 CD、手动 trigger 和 once 补偿都必须复用 UniDesk `trigger-current`、定点 `status` 和 `git-mirror flush`；不得绕到裸 `kubectl`、Argo、Tekton 或手写 GitHub API。
- 旧 DEV/D601/main gate、fallback、legacy mode 和双路径兼容不得进入 `v0.2` 调用链。
- env 容器复用 fast lane 中，`HWLAB_BOOT_REPO`、`HWLAB_BOOT_COMMIT` 和 `HWLAB_BOOT_SH` 必须由 CI/CD 自动推导并写入 GitOps desired state，不得作为人工发布参数或 runtime 临时 patch。
- `deploy/deploy.yaml` 只保存人写运行意图，不得被 auto-CD、promotion 或 flush 回写 `commitId`、service `image`、`HWLAB_COMMIT_ID`、`HWLAB_IMAGE` 或 `HWLAB_IMAGE_TAG` 等发布产物字段。
- git mirror/relay 必须来自独立 `devops-infra` 服务；`hwlab-v02` runtime namespace 不部署 mirror、不持有 GitHub deploy key、不在 mirror miss 时直连 GitHub fallback。
- GitOps promotion 必须写入 `devops-infra` 本地 mirror/relay 的 `v0.2-gitops`；除 mirror/relay flush 外，不得在 CI 关键路径直接 push GitHub canonical remote。
- mirror/relay write 必须只允许 allowlist refs，拒绝 non-fast-forward，拒绝越界 changed paths，并在 receive 成功前完成 object closure 校验。

这些硬边界优先在自然写入点做最小内联断言：render 断言 namespace/runtime path，promotion 断言 GitOps branch/changed paths，Argo spec 断言 destination，验收断言端口和 runtime identity。不要为每条设计约定再新增独立 preflight、guard、gate 或报告生成器。

## G14 DEV/PROD 不变边界

接入 `v0.2` 不得改变以下对象：

- `G14` source branch 的 poller/reconciler 语义。
- `G14-gitops` DEV/PROD catalog 与 runtime desired state。
- `hwlab-dev` 与 `hwlab-prod` namespace。
- `hwlab-node-dev` 与 `hwlab-node-prod` Argo Application。
- DEV `17666/17667` 与 PROD `18666/18667` FRP 入口。
- D601 legacy 回溯路径和旧运行面边界。

如果 `v0.2` 接入失败，回滚或暂停只能作用于 `hwlab-v02` lane：停止 UniDesk v02 monitor、停止手动触发新的 v02 PipelineRun、暂停或删除 `hwlab-node-v02`、回滚 `v0.2-gitops` runtime path、关闭 `hwlab-v02-frpc` 或清理 `hwlab-v02` namespace 资源；不得通过新增 CronJob 维持重试，也不得重启、删除或回滚 DEV/PROD 运行面。

## 验收标准

`v0.2` CI/CD 通过必须同时满足：

- `hwlab-ci` 中存在 `hwlab-v02-ci-image-publish` 和 `hwlab-v02-tekton-runner`；不存在 v02 CronJob。`hwlab-v02-branch-poller` 与 `hwlab-v02-control-plane-reconciler` 不再作为 v02 标准对象，若历史残留应由 UniDesk control-plane apply 清理。
- base=`v0.2` 的 ready PR 由 UniDesk CLI 合并后，会自动触发对应 merge head 的 v02 PipelineRun；其他 commit 的运行中 PipelineRun 不阻塞 ready PR merge 或 CI 启动。
- 同一个 source commit 已有 PipelineRun 时不重复创建，也不删除重建；失败 run 的人工重试必须走后续显式 retry 策略，不恢复默认 delete/create。
- 自动 CD 在旧 run 运行期间遇到新 `origin/v0.2` head 时，新 head 可直接触发自己的 PipelineRun；旧 run 继续完成 CI，但 promotion 必须在写 GitOps 前发现 stale head 并 superseded/no-op，不能回写旧 `v0.2-gitops` 或回滚 runtime。
- 最新 `v0.2` source commit 对应的 PipelineRun 完成，且 promotion 写入 `v0.2-gitops`。
- `v0.2-gitops` 中存在 `deploy/artifact-catalog.v02.json` 与 `deploy/gitops/node/runtime-v02/**`。
- `argocd/hwlab-node-v02` 指向 `devops-infra` 本地 mirror/relay 的 `v0.2-gitops:deploy/gitops/node/runtime-v02`，sync revision 与目标 GitOps revision 对齐。
- `hwlab-v02` 中长驻 workload ready，没有把 DEV/PROD namespace 当成 `v0.2` 通过证据。
- `gitops-promote` 推送本地 mirror/relay 的 `v0.2-gitops` 后应触发 `argocd/hwlab-node-v02` hard refresh，减少 GitOps push 与 Argo 仓库发现之间的漂移窗口；refresh 触发失败只作为低噪声事件输出，最终通过仍由 `runtime-ready` 判定。
- mirror/relay status 必须能显示本地 `v0.2-gitops` revision、GitHub 已 flush revision、pending flush revision 和最近一次 flush 错误；CI 通过不要求 GitHub flush 已完成，但 auto-CD 收口必须在 `pendingFlush=true` 时触发 flush 并最终观察到 `pendingFlush=false`。
- `runtime-ready` 必须以 workload Pod template 的 source commit 判断 Argo 刷新，并在 Argo 刷新超时、observer RBAC 不足或 workload 未就绪时失败；不得把 CrashLoop、未刷新或公网不可用的 runtime 标成绿色。
- 启用 env 容器复用的 runtime service 必须在 catalog、Pod template annotation/env 和 `/health/live` 或等价 runtime identity 中同时暴露 env image digest 与 `HWLAB_BOOT_REPO`/`HWLAB_BOOT_COMMIT`/`HWLAB_BOOT_SH`；只暴露镜像 digest 不能证明 service code-only rollout 已生效。
- code-only rollout 的 PipelineRun 必须能证明没有发布新 env image、复用了上一版 env digest、只更新 boot commit/code identity，并由 Kubernetes Pod template hash 触发滚动。
- 非 service 资产变更必须能证明没有因为 env reuse 触发 runtime service build 或 rollout；典型非 service 资产包括 `tools/hwlab-cli*`、CaseRun runner/helper、`.hwlab/` workspace spec、docs 和 tests。
- `devops-infra` git mirror/cache miss、commit ancestry 拒绝或 boot script 校验失败必须导致本轮 rollout fail closed，不得隐式回退到 GitHub 或 env image 内旧代码。
- `http://74.48.78.17:19666/` 返回 `v0.2` Cloud Web。
- `http://74.48.78.17:19667/health/live` 返回 `v0.2` runtime health，payload 中的 namespace、revision 或 runtime identity 能与 `hwlab-v02`/`v0.2` 对齐。
- `trigger-current` 的异步 job 或 `--wait` 输出必须能显示 trigger 阶段进度；若 control-plane refresh、mirror pre-sync 或 delete/create PipelineRun 卡住，日志必须能定位阶段名和状态。
- 自动 CD 回写 PR 或关联 issue 的 rollout 评论必须包含 source commit、PipelineRun、GitOps revision、planArtifacts build/reuse 摘要、Argo/runtime/public probe 状态和耗时；该评论不能替代需要用户原入口验收的问题关闭证据。
- 自动 CD 过程中 source branch 的 `deploy/deploy.yaml` 不得出现 `commitId`、service `image`、`HWLAB_COMMIT_ID`、`HWLAB_IMAGE` 或 `HWLAB_IMAGE_TAG` 回写。
- no-op env-reuse fast lane 必须输出完整证据链：`ci-plan` 全复用、`skipped-runtime-unchanged`、`gitops-commit`/`gitops-push` skipped、`runtime-ready` skipped。
  不能只用 PipelineRun `Completed` 或总耗时证明没有退化。
- 真实 rollout 的 `runtime-ready` 必须输出 `started`、周期性 `progress` 和最终 `argo-refresh`/`argo-sync-health`/`workload-ready` 类事件；缺少这些事件时应按可见性回归处理。
- Bun 语法检查和测试入口必须分开：`bun --check <file.ts>` 只用于非测试 TypeScript 源文件语法/转译检查；`*.test.ts`、`*.test.mts` 或其他使用 `test(...)` 的文件必须用 `bun test <file>` 或 repo-owned package script 执行。不得再用 `bun --check` 直接跑 `.test.ts`，该命令会在测试运行器外求值并报 `Cannot use test outside of the test runner`，这不是业务回归证据。

GitOps branch 已更新、source branch render 通过、PipelineRun 名称存在或 `G14` DEV/PROD health 正常，都不能单独代表 `v0.2` CI/CD 通过。

## 测试规格

## T1

阅读 docs/reference/spec-v02-cicd.md，然后用 cli 手动测试以下内容：确认 `origin/v0.2` 最新 commit 对应的 PipelineRun 完成，promotion 只写入 `v0.2-gitops`，且 source branch 没有跟踪 `deploy/artifact-catalog.v02.json` 或 `deploy/gitops/node/runtime-v02/**` 生成物。

## T2

阅读 docs/reference/spec-v02-cicd.md，然后用 cli 手动测试以下内容：查询 Argo `hwlab-node-v02`，确认 source repo 为 `devops-infra` 本地 mirror/relay，branch/path 为 `v0.2-gitops:deploy/gitops/node/runtime-v02`，destination namespace 为 `hwlab-v02`，sync revision 与目标 GitOps revision 对齐。

## T3

阅读 docs/reference/spec-v02-cicd.md，然后用 cli 手动测试以下内容：访问 `http://74.48.78.17:19666/` 和 `http://74.48.78.17:19667/health/live`，确认公网入口、payload environment、revision 和 runtime identity 都指向 v02，不把 DEV/PROD health 当作 v02 证据。

## T4

阅读 docs/reference/spec-v02-cicd.md，然后用 cli 手动测试以下内容：涉及 Code Agent 时使用短连接 submit/result/trace 轮询，确认 `status=completed` 且 assistant reply 非空；不得只用 `/health/live` 中 `codeAgent=ready` 证明 provider 鉴权通过。

## T5

阅读 docs/reference/spec-v02-cicd.md，然后用 cli 手动测试以下内容：对启用 env 容器复用的服务触发一次 code-only commit，确认 PipelineRun 标记 `envChanged=false`、复用上一版 env digest、catalog 与 Pod template 写入 `HWLAB_BOOT_REPO`、`HWLAB_BOOT_COMMIT`、`HWLAB_BOOT_SH`，runtime health 同时暴露 env digest 和 boot commit，且 `HWLAB_BOOT_COMMIT` 来自完整 `origin/v0.2` source commit SHA。

## T6

阅读 docs/reference/spec-v02-cicd.md，然后用 cli 手动测试以下内容：在不打印任何 GitHub 凭证的前提下确认 runtime checkout 读路径命中 `devops-infra` git mirror/cache，业务 namespace 没有 GitHub deploy key；模拟 mirror miss 或非法 commit 时 rollout fail closed，不回退到 GitHub 直连或 env image 内旧代码。

## T7

阅读 docs/reference/spec-v02-cicd.md，然后用 cli 手动测试以下内容：触发一次 `v0.2` code-only promotion，确认 `gitops-promote` 只 push 到 `devops-infra` 本地 mirror/relay，Argo 从本地 mirror/relay rollout；再执行 `git-mirror flush --confirm`，确认 GitHub `origin/v0.2-gitops` 快进到同一 revision，status 中 pending 清空。

## T8

阅读 docs/reference/spec-v02-cicd.md，然后用 cli 手动测试以下内容：在 v0.2 source checkout 或对应 PipelineRun primitive validation 日志中确认 `bun run --cwd web/hwlab-cloud-web check` 会在 `hwlab-cloud-web` 镜像发布前执行，并且该 check 包含实际 bundle 输入集合的 TypeScript 语义检查、自动发现的 `*.test.ts` 单元测试、bundle build 和 dist freshness 校验。

## T9

阅读 docs/reference/spec-v02-cicd.md，然后用 cli 手动测试以下内容：确认默认 v0.2 CI 没有重新加入 Playwright、旧 layout smoke、旧 quick prompt、旧 M3 evidence 或 DEV/D601 browser gate；若这些旧门禁重新进入最小 check plan 或 PipelineRun，应直接删除入口并保留 Cloud Web semantic check 与单元测试。

## T10

阅读 docs/reference/spec-v02-cicd.md，然后用 cli 手动测试以下内容：用一个低风险 base=`v0.2` PR 验证 UniDesk v02 PR monitor 的端到端链路，确认 merge 后自动触发对应 merge head 的 `hwlab-v02-ci-poll-<short12>`，定点 status 通过，`pendingFlush=true` 时自动 flush，PR 或关联 issue 评论包含 source commit、PipelineRun、GitOps revision、build/reuse 摘要、Argo/runtime/public probe 状态和耗时，且 `deploy/deploy.yaml` 没有发布产物回写。

## 规格的实现情况

| 规格项 | 状态 | 说明 |
| --- | --- | --- |
| v02 独立 source/GitOps/runtime lane | 已实现 | `v0.2`、`v0.2-gitops`、`hwlab-v02` 和 `runtime-v02` 已固定。 |
| 手动 CLI trigger/pipeline/promotion | 已实现 | 通过 UniDesk `trigger-current` 创建 commit-pinned PipelineRun；v02 不设 CronJob，由 `hwlab-v02-ci-image-publish` 与 GitOps promotion 管理。 |
| PR merge 后自动 CD | 待迁移 | 目标见 #848：UniDesk CLI v02 PR monitor 合并 base=`v0.2` PR 后复用 `trigger-current`、定点 `status` 和 `git-mirror flush`；首期不要求 GitHub webhook，也不创建 k8s CronJob。 |
| v02 runtime readiness fail-closed | 已实现 | `gitops-promote` 推送后触发 Argo hard refresh；`runtime-ready` 读取 workload Pod template source commit，timeout、observer RBAC 不足或未就绪会让 PipelineRun 失败。 |
| v02 裁撤服务不进入发布面 | 已实现 | v02 Tekton build service set、artifact catalog 和 runtime render 只包含保留服务；v02 Argo 开启 prune 清理旧 live 对象；裁撤服务仍可保留在 DEV legacy 源码中。 |
| Argo v02 Application | 已实现 | `hwlab-node-v02` 指向 v02 GitOps path 和 namespace。 |
| FRP `19666/19667` 入口 | 已实现 | 由 `hwlab-v02-frpc` 与 master frps allowlist 共同提供。 |
| SecretRef 独立与 provider 验收 | 已实现/持续约束 | SecretRef 已独立；验收必须做真实短连接聊天。 |
| env 容器复用三变量启动 | 已实现/持续约束 | 五个 v0.2 runtime service 均启用 env-reuse fast lane，并由 CI/CD 自动推导 `HWLAB_BOOT_REPO`、`HWLAB_BOOT_COMMIT`、`HWLAB_BOOT_SH`；service code-only rollout 复用 env image digest，只更新代码身份。 |
| 非 service 资产不触发 env reuse | 已实现/持续约束 | CLI、CaseRun、HWPOD workspace 工具、`.hwlab/`、docs 和 tests 不进入 env-reuse service 触发面；被服务直接 import 的文件必须显式挂到对应 service component path。 |
| `devops-infra` git mirror/relay 加速 | 已实现/持续约束 | source/catalog/runtime checkout 读路径和 GitOps promotion 写路径均使用独立基础设施集群 mirror/relay；Argo source 指向本地 mirror，GitHub flush 由 UniDesk CLI 手动触发，不设置 CronJob。runtime namespace 不持有 GitHub deploy key，registry 保持 G14 `hwlab-ci/hwlab-registry`。 |
| `hwlab-cli` 不进 CI/CD service matrix | 已实现/持续约束 | CLI 是固定 repo 短连接 client，不发布镜像、不生成 artifact、不创建 `build-hwlab-cli` TaskRun；相关旧入口出现时直接删除。 |
| CI/CD fast lane 性能预算 | 已实现/持续约束 | env-reuse no-op 目标约 40s；真实 runtime rollout 目标约 50s；不得恢复 `prepare-source` 依赖安装、GitHub 关键路径写入或 no-op runtime 等待。 |
| Service build 全并行 fan-out | 已实现/持续约束 | selected services 的 build TaskRun 全部只依赖 `plan-artifacts`，不设置 8 并发或其他 Pipeline 级限流；`collect-artifacts` 通过 service report 目录 fan-in。 |
| 自动 registry GC | 未实现 | 初期不启用自动 GC，后续需 lane/profile 保护集。 |

## 平行 lane 运维边界

后续新增 `v0.x` 或其他平行 runtime lane 时，优先复用本节的判定顺序和排障边界，避免把一次性补丁沉淀成新的宽泛门禁。本节只保留可复用的运维边界；具体执行记录、排障流水和一次性证据应放在 issue 或 PR 中。

### Secret 导入与重启边界

平行 lane 的 Secret 必须独立命名，不能在 runtime 里直接引用 DEV Secret。允许把 DEV 当前值一次性导入到目标 lane 的独立 Secret 名，但验证只能输出 Secret 对象、key、字节数和哈希指纹，不能打印 Secret value、token 片段、完整 DB URL 或 `auth.json` 内容。

`v0.2` 至少需要独立维护以下 SecretRef：

- `hwlab-v02-postgres/POSTGRES_PASSWORD`。
- `hwlab-cloud-api-v02-db/database-url`。
- `hwlab-v02-code-agent-provider/openai-api-key`。
- `hwlab-v02-code-agent-codex-auth/auth.json`。

Code Agent 的 Secret 修复不是只改一个 Secret 对象就结束。`hwlab-cloud-api` 通过 env 读取 `OPENAI_API_KEY`，Secret 更新后必须滚动 `hwlab-v02/hwlab-cloud-api`。DeepSeek profile 还通过 `hwlab-deepseek-proxy` 的 initContainer 把 `DEEPSEEK_API_KEY` 渲染进 Moon Bridge 配置，Secret 更新后也必须滚动 `hwlab-v02/hwlab-deepseek-proxy`，否则 proxy 仍会使用旧配置并返回上游鉴权失败。

### Code Agent 验收

`/health/live` 中 `codeAgent=ready`、`codexStdio=ready` 只能证明运行时结构、二进制、workspace、token boundary 和会话 supervisor 就绪；它不证明真实 provider 鉴权可用。涉及 Code Agent 的扩容验收必须追加一次真实短连接聊天闭环：用 `Prefer: respond-async` 和 `X-HWLAB-Short-Connection: 1` 提交 `/v1/agent/chat`，再轮询 `/v1/agent/chat/result/<traceId>`，只有 `status=completed` 且 assistant reply 非空才算通过。

如果 trace 里出现 `Authentication Fails`、`upstream stream error` 或 `codex_stdio_provider_retry`，先按 profile 分层判断：`deepseek` profile 走 `hwlab-deepseek-proxy.<namespace>.svc.cluster.local:4000/v1/responses` 和 Moon Bridge；`codex-api` profile 走 Pod-local loopback forwarder。不要用 `codex-api` readiness 掩盖 DeepSeek 专用凭证或 Moon Bridge 配置问题。

长耗时 smoke 不应通过 UniDesk SSH 长连接等待完整 Codex turn。Codex 首 token 可能超过短查询窗口，验证应使用短连接 submit/result/trace 轮询，避免把控制通道超时误判成 provider 失败。

### GitOps 与 runtime 收敛

GitOps promotion 成功不等于 runtime 已经运行新版本。验收必须等待 `argocd/hwlab-node-v02` 的 sync revision 对齐最新 `v0.2-gitops` revision，并确认 `19667/health/live` 的 `environment`、`endpoint`、关键 SecretRef 和 service revision 与目标 lane 对齐。

Argo `Synced/Healthy` 也不能单独替代公网验证。FRP server 侧 `allowPorts` 缺失时，`hwlab-v02-frpc` 会反复报告 `port not allowed`；这种问题应修 master 侧 `frps` allowlist 并只重启 `hwlab-frps-dev`，不要改 DEV/PROD GitOps、Service 或 v02 runtime path。修复后必须同时验证新增 `19666/19667` 和既有 DEV `17666/17667`。

### 后续扩容建议

下一条平行 lane 建议先列一张最小资源映射表，再落地 CI/CD：source branch、GitOps branch、runtime namespace、runtime path、Argo Application、FRP ports、artifact catalog、Postgres Secret、Cloud API DB Secret、Code Agent provider Secret、Codex auth Secret、DeepSeek proxy restart 对象和公网验收 URL。映射表是执行清单，不是新 gate；只有 branch、namespace、runtime path、GitOps branch、Argo destination、SecretRef 名称和公网端口这些硬边界需要内联断言。

Secret、FRP 和 Argo 问题都应先在目标运行面做最小真实闭环，再进入完整 CI/CD 复跑。不要用完整 PipelineRun 反复探索 Secret 值、FRP allowlist 或 provider 鉴权；CI/CD 只负责固化已经在目标 namespace 证明可行的配置和源码。
