# Agent Note: 并行 GitHub CI 门禁

Status: implemented
Archived: 2026-07-26

[English](2026-07-06-parallel-github-ci-gates.md) | 中文

## 问题

无密钥 GitHub CI 门禁大多相互正交：类型检查、lint、文档新鲜度、覆盖率、快照重放、构建、包（package）的发布卫生检查、demo 冒烟和已构建二进制冒烟会因不同原因失败，也不需要彼此的运行时状态。将它们作为一条有序命令链运行，会使工作流墙钟时间等于所有门禁耗时之和；而把每个短小叶子拆成独立 GitHub job，又会反复执行 checkout、Node 设置、pnpm 恢复和安装，直到编排开销成为瓶颈。

随着 workspace 增长，原有的宽车道拆分不再满足这一平衡。PR（Pull Request）#404 合并时，Linux 的静态、覆盖率、快照和产物 job 分别耗时 148、195、94 和 230 秒；Windows 的静态和产物 job 分别耗时 251 和 482 秒。每个包都调用一次包管理器打包，主导了两个产物验证器的耗时；覆盖率在仅运行源码的套件前无谓地重建输出；CPU 密集型门禁则在静态与覆盖率车道内争用资源。

产物边界仍然承载关键约束。`publint`、`verify-node-next-types`、已编译不变量加载和已构建二进制冒烟测试都需要生成的 `lib/` 输出。分片不能让这些消费方抢在构建前运行，也不能用源码执行取代它们对已发布产物的信号。

## 决策

下述生产拓扑已经成为历史，并由[基于证据采用更大的托管 runner](2026-07-22-evidence-based-larger-hosted-runners.md) 取代。更大 runner 的决策移除了其分片选择器和工作流 job；本文保留早期拓扑为何被实现的记录。

[CI](../../../../.github/workflows/ci.yml) 将非 Windows job 的一分钟和 Windows job 的三分钟视为观测所得的性能目标，而非取消截止时间。托管 runner 的波动应留下完整计时证据和有用的失败日志，而不是取消本来正确的门禁。[串行跨平台 CI 参考](2026-07-21-serial-cross-platform-ci-reference.md)会在 Linux、macOS 和 Windows 上独立运行完整、未分片的主 Node 聚合，使优化后的车道清单不会成为自身完整性的唯一判据。

在该拓扑中，[scripts/run-gates.ts](../../../../scripts/run-gates.ts) 是通用的有界调度器，GitHub 则为昂贵的门禁族提供显式分片名称。`scripts/static-shards.ts` 将静态门禁划分为基础、文档类型、API 契约、目录、正文、文档投影和文档构建等归属，并拒绝缺失或重复的门禁分配。Linux lint 使用互不重叠的 A-C、D-M、N-S、T-Z 包源码和包测试车道，Windows 则使用完整的包源码与包测试车道；两者都包含从 `.` 开始的仓库补集，使新增顶层目标无法消失在分片之间，并负责唯一一次跨文件重复检查。`scripts/coverage-shards.ts` 把每个 workspace 包恰好分配给一个源码覆盖率车道。目录过滤器保留尾部分隔符，因为 Vitest 位置过滤器按子字符串匹配，否则会纳入具有同名前缀的相邻项。每个覆盖率车道只包含其拥有的源码文件，重复运行穷尽式伴随拓扑测试，并且不先执行构建，因为从删除了所有生成式 `lib/` 的树开始，完整覆盖率套件仍可通过。

快照重放使用两个显式多文件车道，以及大型 ACP（Agent Client Protocol）文件的八个场景分区。`scripts/snapshot-shards.ts` 拥有该清单，其测试会发现快照配置允许的每个文件。每个快照 job 在其 Linux runner 准备 Bubblewrap 的同时安装依赖，随后构建已发布运行时，并且只运行分配给它的重放表面。该套件保留五个子进程的有界并发，因为重放的大部分时间都在等待子进程协议 I/O。fixture（测试前置数据）守卫仍会在每个分区中检查完整 ACP 场景表。

冷启动的独立文档类型检查会重建完整的项目引用图，因此专用文档类型车道只构建一次，再用这些声明检查 Markdown 块。Linux 文档车道使用 VitePress 的 MPA 构建，在观测所得的非 Windows 目标内保留页面渲染与死链接验证；单独的阻塞式 Windows 构建和生产站点车道保留已生成包与已发布站点检查，同时避免把两条关键路径放进同一个 job。

产物使用两个车道：一个元数据车道负责 `publint`、NodeNext 声明和已编译不变量加载，另一个负责已构建二进制冒烟。每个车道都会在其消费方之前自行构建。重复短时构建会消耗 runner 分钟数，但避免了上传/下载依赖，并使每个 job 的关键路径保持有界。

[scripts/publint-all.ts](../../../../scripts/publint-all.ts) 在进程内针对内存发布视图调用 publint 支持的 API；该视图由每份清单声明的文件和 npm 强制元数据文件构成。这样无需生成 103 次包管理器打包命令，也能保留 workspace 文件与已发布文件之间的区别。[scripts/verify-built-package-invariants.mjs](../../../../scripts/verify-built-package-invariants.mjs) 在真实包下暂存这些经过结构验证、由清单声明的 `lib/` 文件，再通过纯 Node 和 Cordis Loader 规范化导入已编译的自引用。若伴随项触及未声明的运行时分片，仍会失败。

兼容性车道会在每条声明支持的 Node 版本线上运行源码 worker 和 Zstandard 运行时冒烟。TypeScript 在专用的主 Node 24 车道中只检查一次源码图；在运行时兼容性 job 中重复同一编译器分析只会增加耗时，不会提供运行时特有信号。

工作流缓存 pnpm store，将每个不可变 ESLint 缓存的键绑定到其所属 lint 分片，为 Windows 测量保留原生 PowerShell，并保留一个聚合的 `all checks passed` 状态用于分支保护。Windows 复用三个穷尽式 lint 分区，并在共享 runner 设置后组合基础/目录/正文门禁与文档类型/API 契约门禁；只有调度方式与 Linux 分区不同。Windows 构建和生产站点验证继续阻塞，而更广泛的 Windows 静态、lint 和产物矩阵仍为观察性检查。

## 曾考虑的替代方案

- **保留宽车道**：最大限度减少工作流 YAML，但会保留观测到的数分钟反馈周期。
- **让每个叶子门禁分别成为 GitHub job**：最大化扇出，但短小的生成器和正文检查准备 runner 的时间会超过检查仓库的时间。
- **向产物消费方上传一次构建**：避免重复编译，但上传/下载和依赖调度会延长墙钟时间；干净构建足够短，可以在有界车道内重复。
- **在两个发布门禁中保留包管理器打包**：把清单选择委托给 pnpm，但会重复启动 200 多个包管理器进程。清单结构门禁加发布视图 fixture 使优化后的清单契约显式化，并会在存在磁盘上有但未发布的依赖时失败。
- **在覆盖率前保留构建**：提供源码套件已不再消费的生成输出；干净树覆盖率证明表明这只是纯粹的延迟。
- **在每个 Node 版本上执行类型检查**：重复编译器工作，而兼容性冒烟已经验证实际的 Node 特有加载与压缩行为。

## 后果

上述分片清单和矩阵 job 不属于当前仓库契约。取而代之的更大 runner 决策在单个进程中保留完整主清单，并以串行套件作为独立完整性判据。

优化后的发布验证器依赖由 `verify-package-invariants` 强制执行的清单 `files` 契约。如果发布规则超出该契约，结构门禁和两个暂存视图必须一起变化。

兼容性 job 不再声称 TypeScript 本身已在每个 Node 运行时下执行。它们证明 Node 22、24 和 26 上对运行时敏感的源码加载，而主运行时负责唯一一次源码图类型检查。
