# CI/CD 生产事故恢复

## 适用边界

- 本流程适用于 PaC、Tekton、GitOps、Argo、git-mirror 或 runtime rollout 连续失败，以及多个 consumer 出现共同失败指纹的生产事故。
- 本流程不扩大写入授权：
  - consumer 私有配置可以在原任务范围内修复；
  - CI/CD 公共服务面默认只读；
  - 修改公共服务面必须取得用户明确授权。
- 节点、consumer、PipelineRun、仓库、分支和提交均为变化项，必须从 owning YAML 与受控 CLI 解析，禁止复制本案例取值。

## 公共服务面与 consumer 私有面

- 公共服务面包括：
  - 共享 PaC controller/release 与 admission；
  - Gitea webhook bridge、durable inbox 和共享 source authority；
  - 共享 git-mirror read/write Deployment、Service、PVC 和仓库 ref；
  - 共享 Tekton/Argo bootstrap、renderer、模板和 native helper；
  - 公共 edge、FRPC、Caddy、Secret 和 RBAC。
- consumer 私有面只包括：
  - owning YAML 中由 consumer id 唯一定位的独立配置片段；
  - 独立 lane、namespace、Pipeline、source artifact 和 runtime 参数；
  - consumer 自己仓库内由 owning YAML 声明的 `.tekton` 制品。
- 任一对象存在多个 consumer owner、共享 `repositoryRef`、共享 PVC、共享 Deployment 或共享网络入口时，按公共服务面处理。

## 最短定位路径

按以下顺序执行，前三步足以完成运行面分层，不再从裸 Kubernetes 大对象开始试探：

1. 读取节点摘要：

   ```bash
   bun scripts/cli.ts cicd status --node <NODE>
   ```

2. 一次计算选中 consumer 的回退窗口与同 source authority 共同指纹：

   ```bash
   bun scripts/cli.ts platform-infra pipelines-as-code diagnose-regression \
     --target <NODE> \
     --consumer <CONSUMER>
   ```

   - `ok` 表示只读采集是否成功；
   - `incident` 表示最新终态是否处于失败连续段；
   - `window.lastKnownGood` 与 `window.firstFailed` 给出 source commit 边界；
   - `commonFailure` 只在共享 `repositoryRef` 的 lanes 内聚类；
   - `state=read-failed` 时先处理读取可见性，不得按健康解释；
   - 窗口不足时只执行输出中的有界 `expand-window`。

3. 仅在需要精确 TaskRun 或日志证据时，对首次失败执行输出中的 `debug-first-failure`：

   ```bash
   bun scripts/cli.ts platform-infra pipelines-as-code debug-step \
     --target <NODE> \
     --consumer <CONSUMER> \
     --id <PIPELINERUN>
   ```

4. 使用命令输出的 `sourceRange.compareUrl` 比较最后成功 source commit 与首次失败 source commit：
   - 只检查两者之间的提交；
   - 优先寻找与共同失败指纹直接相关的最近变更；
   - 记录致因文件、提交、影响 consumer 和可回退性；
   - 不在此阶段设计前向重构。

## 回退决策

- 满足以下条件时直接选择回退：
  - 失败从某个提交后稳定出现；
  - 多条 PipelineRun 具有相同首个断点；
  - 变更与断点存在直接因果关系；
  - 回退不造成不可逆数据或外部状态损坏。
- 回退 PR 只包含致因变更的精确反向语义：
  - 不夹带前向修复；
  - 不夹带重构；
  - 不夹带其他 consumer 或公共服务清理；
  - 不夹带 unrelated PR。
- 致因位于公共服务面但用户没有明确授权时：
  - 停止写入；
  - 提供致因提交和精确回退候选；
  - 说明受影响 consumer 与预计恢复证据；
  - 等待用户授权公共服务面回退。

## 恢复验收

- 回退 PR 合并后，先执行 release plan；范围准确时手动 trigger，再观察新 PaC 事件：
  - webhook/Gitea delivery 已提交；
  - PaC 外层 PipelineRun 成功；
  - Tekton terminal roles 成功；
  - GitOps revision 与 source commit 对齐；
  - Argo `Synced/Healthy`；
  - runtime ready 且 `/health` 成功。
- 不得通过人工 mirror sync、直接 Gitea push、人工 PipelineRun、Argo sync、bootstrap、apply 或运行面热补补齐验收。
- 回退事件恢复健康后，才允许从最新健康基线创建独立前向修复 PR。

## Tekton 大对象与 Kine/SQLite 控制面退化

### 现象分层

- 先区分自动链传播延迟与节点控制面事故：
  - PipelineRun 已成功并生成 GitOps commit、Argo 仍停在旧 revision，
    但 node、API、lease 和 runtime workload 健康时，按传播延迟处理；
  - 只读记录 PipelineRun completion、GitOps commit、Argo reconcile/operation、
    Deployment rollout 和 runtime ready 时间，等待自动链自行收敛；
  - host 无法解析 `*.svc.cluster.local` 只说明查询平面错误，
    不能证明集群内 git mirror 或 Argo repo-server 不可用；
  - node 进入 `NotReady`、Service endpoint 消失，并同时出现 API handler timeout、
    node lease 延迟、kubelet housekeeping timeout 或持续 Kine Slow SQL 时，
    才按节点控制面事故处理。
- 不得只凭 Pod Pending、Argo `Synced/Healthy` 或单条 Slow SQL 下结论：
  - `Synced/Healthy` 必须同时核对实际 revision，避免把旧 revision 健康误判为新交付完成；
  - 周期性 compaction 或孤立 Slow SQL 只记录为余压；
  - 同窗口出现 lease、API、kubelet 和业务 endpoint 退化，才构成同类故障段。

### 根因判定

- 对疑似大型 PipelineRun，在目标 k8s 内计算短摘要：
  - 对象总字节数；
  - `spec` 字节数；
  - `status.pipelineSpec` 字节数；
  - Pipeline task、matrix TaskRun 和普通 TaskRun 数量；
  - 最大字段及其 renderer/source 路径。
- Tekton 会把解析后的 Pipeline spec 持久化到 `status.pipelineSpec`：
  - renderer 为每个服务重复展开相同大型 inline `taskSpec.steps[].script` 时，
    对象会在 `spec` 与 `status` 双份放大；
  - 运行期间的状态更新会重复写入大型对象，进一步放大 Kine/SQLite 单写者压力；
  - TaskRun results 只有在字节证据成立时才可判为主因，禁止按字段名称猜测。
- 将对象证据与同窗口控制面证据关联：
  - Kine compaction 耗时和 Slow SQL；
  - API handler timeout 与 stale resource-version；
  - node/peer lease 写入延迟；
  - kubelet housekeeping 和 pod lifecycle 延迟；
  - k3s cgroup task、内存、swap 与数据库/WAL 压力。
- 对象大小、耗时和留存阈值必须由 owning YAML 控制：
  - 状态与 OTel 只输出 `blocking=false` warning；
  - 禁止把可见性阈值升级成阻断 PipelineRun、GitOps、Argo 或业务 `/health` 的门禁。

### 快速恢复

按以下顺序恢复用户入口，禁止跳过取证后直接重启：

1. 保存最小证据：
   - 执行 `cicd status --node <NODE>`；
   - 对选中 consumer 执行 `pipelines-as-code status` 和 id-specific `history`；
   - 记录 node condition、受影响 Service endpoint、异常 Pod owner、
     Kine/API/lease/kubelet 的有界日志计数；
   - 所有 Kubernetes 大对象在目标侧计算大小和摘要，不把完整 JSON 拉回本机。
2. 判断是否仍可由 Kubernetes 自行调和：
   - API 与 kubelet 可用时，先等待 Deployment/StatefulSet controller 创建替代 Pod；
   - 只处理已被驱逐、终态、卡在删除态且已不属于 Service endpoint 的旧 Pod；
   - 删除前确认 owner、替代副本和 endpoint，禁止 broad selector、全 namespace 强删或触碰有状态单例。
3. 只在节点控制面已退化且 Pod 级调和无效时重启 k3s：
   - 该动作属于 `$unidesk-daddev` P2 紧急恢复，必须有用户授权；
   - 单节点目标通过 host 受控 route 执行：

     ```bash
     trans <NODE>:/root/unidesk systemctl restart k3s
     trans <NODE>:k3s kubectl get nodes
     ```

   - 禁止使用 Compose 替代 YAML 选中的 k8s 运行面；
   - API 恢复后立即复查 node Ready、controller 调和和业务 endpoint，
     healthy 后不再扰动 backend-core 或其他已恢复 workload。
4. 控制面稳定后释放终态 Tekton 压力：
   - 先对 owning cleanup 入口执行 dry-run；
   - 再以 `--confirm --wait` 有界删除符合 YAML retention 的终态 PipelineRun；
   - 必须保留 active run、最新成功证据、业务 PVC、Secret、runtime workload 和 GitOps desired state。
5. 恢复只以用户原入口结束：
   - node `Ready`；
   - workload ready 且无持续重启；
   - Service endpoint 存在；
   - backend/provider/public `/health` 返回成功；
   - 恢复动作、根因假设和待持久化修复进入 issue 与 TaskTree；遗留 MDTODO 按 `$unidesk-tasktree` 迁移。

### k3s VXLAN panic 后的 credential/datastore 启动闭锁

- 出现以下连续指纹时，必须拆成两个故障阶段：
  - k3s 先在 flannel VXLAN 设备重建路径 panic，或因
    `inotify_init: too many open files` 等 kubelet 启动错误异常退出；
  - systemd 随后持续自动重启，但每次都报
    `server/cred/<file> newer than datastore and could cause a cluster outage`；
  - 后一条 fatal 是重启闭锁，不等于最初触发 k3s 退出的根因。
- 快速恢复前必须保存：
  - 首次退出前后的完整 panic 堆栈；
  - fatal 明确点名的单个 credential 文件路径；
  - 该文件的大小、权限、mtime、birth time 和 SHA-256；
  - k3s unit 状态及 API、node、Service endpoint 当前状态。
  - 若存在 inotify 错误，同时记录 `fs.inotify.max_user_instances`、
    `fs.inotify.max_user_watches`、当前 inotify instance 数和遗留容器进程数。
- 只有用户已授权恢复且 fatal 明确点名单个文件时，才允许：
  - 在 `server/cred` 目录之外创建本次事故独立、权限为 `0700` 的恢复目录；
  - 将被点名文件移动到该目录，保留原名，不删除文件；
  - 禁止移动整个 credential 目录、删除 datastore、改写 token 或处理未点名文件；
  - 重启 k3s，让它从 datastore 重新生成该文件。
- 重启后必须比对重新生成文件与备份的大小、权限和 SHA-256：
  - 一致时记录为“移除磁盘时间戳闭锁”，不能宣称凭据内容已修复；
  - 不一致时停止扩大操作，保留两份文件并进入 Secret/credential 专项调查。
- inotify instance 已接近上限时，可在用户已授权的紧急恢复中临时提高
  `fs.inotify.max_user_instances`：
  - 先记录原值和当前使用量；
  - 只提高 instance 上限，不修改 `fs.file-max` 或广泛终止未知进程；
  - 临时值必须登记到 issue，后续由 owning YAML 决定持久值并调查 instance 泄漏；
  - 临时提高上限不能替代遗留 shim、runtime 生命周期或资源泄漏修复。
- 验收必须覆盖：
  - k3s `active` 且不再 restart loop；
  - node `Ready`、UniDesk workload Ready、backend `dbReady=true`；
  - frontend/provider ingress 健康；
  - 事故前失败的原始 `trans` route 恢复。
- 后续 issue 必须分别追踪：
  - flannel/k3s panic 的触发条件、版本和升级或规避方案；
  - 被点名 credential 文件的写入者与 source of truth；
  - 受控 CLI 对该 fatal 的诊断、可恢复备份和验收能力。

### PaC/Tekton controller 假健康与 Pending 队列恢复

- 适用指纹：
  - PaC watcher 或 Tekton controller Deployment 显示 Ready，但日志停止推进；
  - 多个 consumer 的 PipelineRun 长期停在 `PipelineRunPending`，没有 TaskRun；
  - watcher 访问集群内 Gitea Service 超时，而 Gitea Pod、Service endpoint
    和同节点探针正常；
  - watcher 位于已 cordon 或 Pod 网段异常的 worker，Gitea 位于控制面节点。
  - PipelineRun 已 Running，但 step 只完成 entrypoint 解码并持续等待
    `/tekton/downward/ready`，同时 Tekton controller 日志早于该 TaskRun 停止。
- 先区分公共控制面与具体 Provider：
  - watcher、Tekton controller、Gitea、Service 网络和共享 PaC 队列属于公共 CI/CD；
  - AgentRun runner、HWLAB node、业务 Deployment 和 Provider 数据通道不在恢复范围；
  - 用户只授权公共运维时，不得借机重启、迁移或修改具体 Provider。
- 快速恢复必须按依赖顺序执行：
  - 先确认 node、Gitea Pod、Service endpoint 和 watcher placement；
  - 公共无状态 controller 卡在不可调度且跨节点网络失效的 worker 时，
    只删除该精确 Pod，让 Deployment 在 YAML 允许的健康节点重建；
  - watcher 重新取得 leader 且能访问 Gitea 后，再处理 Pending 队列；
  - Tekton controller 未观察新 TaskRun 时，只重启该精确 Deployment，
    让现有 PipelineRun 和 TaskRun 由 controller 重新调和；
  - 不得先批量删除 PipelineRun，也不得用业务重跑掩盖公共依赖故障。
- watcher 停机后可能只重建内存队列，无法补发已经完成的前序事件：
  - 用 `execution-order`、source commit、`spec.status`、TaskRun 数量和终态条件
    确认卡住的事件组；
  - 当前权威 source commit 只从 GitHub source branch 获取；
  - 仅取消同一 Repository 中仍为 Pending 且已被权威 commit 取代的运行；
  - 使用 Tekton 合法的 `spec.status: Cancelled`，保留 PipelineRun 历史对象；
  - 保护权威 commit 的完整执行组、active run、latest success 和审计证据；
  - 没有明确公共运维授权时，只报告候选，不执行队列 mutation。
- 临时恢复不能作为完成证据：
  - Pod 重建、精确 Pending 取消和 controller 重新选主只证明公共通道恢复；
  - 长期调度归属必须回写 owning YAML，避免公共 controller 再落到隔离 worker；
  - 最终必须由新的正常 GitHub source event 自动经过 Gitea、PaC、Tekton
    和 public-edge reconcile；
  - 公网入口验收同时要求受控 public-edge status 无 unresolved/probe failure、
    desired/current fingerprint 一致、provenance source commit 对齐，
    并以绕过本机代理的公网 IP HTTPS probe 确认 TLS 和 readiness。

### 长期防复发

- 从 renderer/source 消除重复内联：
  - 同构多服务构建优先使用 Tekton matrix，只在 PipelineRun 中保留一份 task 实现；
  - 跨 Pipeline 共享的稳定职责使用 reusable Task，并确保 Task reconciler 是正式 owner；
  - 没有 Task reconciler 时不得以手工 apply 维持 TaskRef，改用 matrix 或 source 中的原生脚本文件；
  - 禁止用压缩字符串、编码载荷、第二 Pipeline 或 fallback 隐藏对象膨胀。
- 为 source artifact 和 renderer 增加结构验证：
  - 统计对象总大小、`spec` 和 `status.pipelineSpec`；
  - 检查重复 inline script、task 数量和 matrix 展开语义；
  - 保证每个服务仍有独立 TaskRun、结果收集和失败归属；
  - 结构检查只形成有界 warning，真正缺少渲染必需输入时才 fail-closed。
- 保持 YAML-owned retention：
  - 每个 consumer 声明终态 PipelineRun/TaskRun 留存策略；
  - cleanup 必须保护 active、latest success 和审计证据；
  - 定期清理降低对象 churn，但不能替代 renderer 减载。
- 补齐非阻塞可观测性：
  - status/history 输出对象字节、Kine/WAL、compaction、API、lease、kubelet 和 node pressure 摘要；
  - CI/CD OTel span/event 记录 source commit、PipelineRun、GitOps commit、Argo revision、
    runtime digest、阶段时间和对象压力 warning；
  - exporter 或 provenance 漂移只标记 evidence gap，禁止改变业务成功终态。
- 长效修复必须由 PR 合并后的 plan 与手动 webhook 事件验收：
  - 新 PipelineRun 成功且对象显著减小；
  - GitOps commit、Argo revision、runtime digest 与 source identity 对齐；
  - PipelineRun 执行和后续 compaction 窗口内 node、API、lease、kubelet 与业务 endpoint 保持健康；
  - 不得用 k3s restart、Pod 删除、人工 PipelineRun、Argo sync 或 mirror flush 作为最终修复证据。

## 案例：删除工作目录引发 `getcwd()` 失败

- 共同指纹：多个 PaC consumer 的后续 step 在业务逻辑执行前统一报 `getcwd()`。
- 致因：前一步删除 `/workspace/source`，后续步骤仍把该目录作为工作目录。
- 正确动作：精确回退致因提交，先恢复自动链，再重做共享 Repository 多分支与 release 支持。
- 错误动作：
  - 在故障基线上继续叠加共享架构改造；
  - 同时维护多个不完整 PR；
  - 误合并无关 PR；
  - 用人工补链制造“已恢复”证据。
- 长期结论：
  - 先恢复最后已知健康状态；
  - 再做独立前向修复；
  - 每个 PR 必须单一职责、可独立验证、可独立回退。

## 案例：PaC status 公共 renderer 生成非法 JSON

- 现象分层：
  - SelfMedia PR `#82` 合并后 `14s` 自动触发 PipelineRun；
  - PipelineRun 对 source commit `8c03bcca554457315bc16bad01e5a96764276472` 执行成功，耗时 `256s`；
  - Argo 与 runtime 正常，`pac-read-parse-failure` 只发生在共享 status 观察器；
  - 因此这是公共观察面事故，不是 SelfMedia 私有 YAML 或自动交付失败。
- 直接根因：
  - `80ca187c` 中 `bootstrap_ready` 的赋值原本正确；
  - `1f3283ff` 在回退 admission 业务阻断时删除该赋值；
  - `2b385f08` 恢复 consumer bootstrap fail-closed 引用，却没有恢复赋值；
  - shell 最终拼出非法 `"ok":,`，影响所有复用共享 renderer 的 PaC consumer。
- 回退决策：
  - 不整体回退 `2b385f08`，因为整体回退会再次丢失正确的 bootstrap fail-closed 语义；
  - 只从最后已知正确实现恢复 `bootstrap_ready` 赋值，并实际执行三种 bootstrap fixture 后 `JSON.parse` 输出；
  - 这是对致因遗漏的最小语义回退，不是在错误基础上继续扩展 admission 设计。
- 恢复期间暴露的第二断点：
  - status 在 `25000ms` YAML 预算内超时，阶段标记锁定在 `artifact-evidence`；
  - 共享 collector 对同一 TaskRun 重复读取，并为成功 run 同步串行执行 `kubectl logs`；
  - NC01 上同一条有界日志读取实测需要 `18s` 到 `38.5s`，不能作为 status 同步必需项。
- 公共面修复：
  - status 只读取一次 TaskRun JSON，并在 task summary 与 artifact terminal record 间复用；
  - status 使用 `taskrun-status-only`，不再同步读取 Pod 日志；
  - history collector 收敛为一次 selector 聚合，保留失败日志下钻；
  - 目标侧输出无敏感值的 `pac-status-progress`，CLI 在 timeout observation 中显示 `remoteStage`；
  - PipelineRun 成功、Argo `Synced/Healthy` 且 runtime ready 时，缺少同步日志证据降级为 `pac-artifact-log-evidence-deferred` 非阻断告警。
- 首次合并后的补充恢复：
  - 首个修复 PR 合并后，canary 又在 `remoteStage=argo` 间歇超过 `25000ms`；
  - 给 PipelineRun 查询直接设置过短的单次上限会返回空列表并制造 `pipeline-missing` false negative，不能以“命令不再超时”冒充恢复；
  - 最终按 Repository CR label 在服务端筛选 PipelineRun，同时保留 consumer classifier 区分共享 repository lanes；
  - PR `#2256` 把 `25000ms` 总预算按 `/8000` 派生为 `3s` 单次上限，合并后再次把超时的 PipelineRun 查询改写为空数组并误报 `pipeline-missing`；
  - 同时，admission provenance 为 default ServiceAccount 串行执行十次 `kubectl auth can-i`，累计占用总预算；
  - 恢复时把十次权限探测收敛为一次 `kubectl auth can-i --list`，并在目标侧聚合 PipelineRun/TaskRun mutation verbs；
  - 精确回退 `/8000` 错误切分，单次读取上限恢复为 YAML 总预算本身，整次 status 仍由调用侧 `25000ms` 硬上限约束；
  - PipelineRun 读取失败不再改写为空数组，而是返回 `pac-pipelinerun-read-failed`，明确表示缺失尚未成立；
  - Argo 原始 JSON 仍只读一次并由 Argo 摘要与 runtime 共同复用；阶段事件继续输出 elapsed 与 step timeout；
  - 最终补丁连续两轮 canary 分别在 `15.7s` 与 `11.3s` 完成且保持 `ok=true`。
- 验收结论：
  - 原 `status --target NC01 --consumer selfmedia-nc01 --json` 稳定在 YAML `25000ms` 预算内返回合法 JSON；
  - 输出 `ok=true`、`remoteStage=complete`、PipelineRun `Succeeded`、Argo `Synced/Healthy`、runtime `1/1`；
  - 全程未修改 SelfMedia 私有 YAML，未人工执行 PipelineRun、mirror、trigger、sync、flush、Argo sync 或 runtime patch。
- 长期结论：
  - 先区分 delivery failure 与 observer failure；
  - 修复共享 renderer 必须取得公共服务面授权；
  - 先恢复最后已知正确语义，再删除重复、无界和同步日志读取；
  - 总预算不得按假定调用数切成会制造 false negative 的局部超时，读取失败也不得伪装成对象不存在；
  - status 负责快速结构化摘要，id-specific 入口负责慢日志证据。
