今年早些时候,Linear 团队发现其首席技术官 Tuomas 在 Linear 平台上提交的代码审查请求(PR)数量激增。随着人工智能编码代理的使用,代码生成速度呈指数级增长,但验证这些变更的速度并未同步跟上。由于每个 PR 都必须通过持续集成(CI)流程,开发速度的加快导致 CI 成为瓶颈,不仅增加了基础设施成本,还延长了开发人员和 AI 代理等待反馈的时间。

为解决这一问题,Linear 团队对 CI 系统进行了重新设计,重点优化了 PR 等待 CI 完成的时间以及 CI 运行器消耗的计算资源时间。尽管自年初以来,测试套件规模几乎增长了四倍,但通过一系列优化措施,整体效率得到了显著提升。
Linear 主要通过以下四种方式提升了 CI 性能:
- 升级基础设施和工具链
- 优化门控任务(Gating Jobs)的执行逻辑
- 减少重复性的环境设置开销
- 提高测试执行的并行效率
虽然 Linear 的代码库主要基于 TypeScript,但这些优化策略适用于多种语言和工具链。
升级基础设施与工具链
早期的优化成果几乎不需要修改 CI 本身的核心逻辑。团队将工作负载从 GitHub Actions 迁移至第三方运行器,后者具备更快的 CPU、更高性能的存储以及更优的缓存基础设施。在为期两天的对比测试中,平均工作负载速度提升了 34%,部分特定负载(如 TCS)的速度提升了 52%。
工具链的现代化也带来了显著回报。切换到原生 TypeScript 编译器 tsgo 后,类型检查(tsc)的每周中位数耗时减少了 73%,彻底消除了类型检查带来的瓶颈。
移除类型检查依赖
Linting(代码静态分析)是另一个早期优化目标。此前,一些自定义 lint 规则依赖 TypeScript 类型信息,这意味着每次 lint 运行前必须构建完整的类型图。团队重写了这些规则,转而使用抽象语法树(AST)上的静态分析来识别类似函数的构造和保护模式,从而不再需要类型信息。这一改动使 ESLint 完全摆脱了对 TypeScript 的依赖,API 页面的 linting 时间减少了 68%,全仓库的 linting 时间减少了 55%,内存使用量也大幅下降。
去除对类型信息的依赖也为后续转向 Oxlint 铺平了道路。Oxlint 进一步减少了 CI 运行器在 Linting 上花费的时间。
优化门控任务
随着基础设施和单项检查速度的提升,团队开始将 CI 视为一个整体系统进行优化。在所有工作中,门控任务(即决定后续步骤是否执行的小任务)变得至关重要。每次运行都始于检查 PR 触及的路径,并判断相关测试是否已针对相同输入通过。如果在职位层面跳过某些工作,就不会留下候选者。对于八个 API 测试分片而言,即使是很小的延迟也会累积成显著的影响。
最小化检出范围
许多工作流程始于“变更检测”任务,用于确定下一步操作(例如检查 diff 是否包含数据库迁移)。然而,这些任务此前会检出整个工作树,尽管它们只需要一小部分数据。通过限制检出深度,最慢的门控任务从 94 秒降至 20 秒;对于从未使用过工作树的检查,直接取消检出步骤,使其从 27 秒降至 7 秒。此外,采用稀疏、无补丁且历史记录有限的检出方式,又节省了约 11 秒。
增强检出的弹性
在更换底层运行器基础设施后,团队注意到 `actions/checkout` 步骤有时会变长甚至挂起。由于第三方运行器位于 GitHub 网络之外,依赖直接的 IP 连接访问 GitHub,供应商追踪到连接存在间歇性退化。鉴于许多工作流程始于检出步骤,停滞的获取操作可能会延迟整个 CI 运行。
通过设置 `GIT_HTTP_LOW_SPEED_LIMIT` 和 `GIT_HTTP_LOW_SPEED_TIME`,停滞的连接会在 30 秒后取消而不是无限期挂起,并结合使用 Checkout 缓存,有效减少了关键路径工作等待检出的时间。
精简关键路径
并非所有位于关键路径上的任务都是必要的。例如,合并前的最后检查原本会写入缓存标记,这发生在合并测试通过后。团队将此写入操作转移到一个在测试分片完成后运行的独立任务中。这一变化大约减少了 1 分钟的 API 提取请求检查时间,同时也降低了运行器的启动时间。
减少重复设置开销
接下来,团队着手解决每项工作中重复的设置成本,如启动运行器和安装包。这种开销意味着一项仅需几秒钟的有效工作,最终可能耗费数分钟的基础设施时间。
预装基础依赖
API 测试分片每次运行需花费 7 到 8 秒安装相同的 Postgres 客户端。团队将其转移到一个包含 Node.js 和客户端的小型 CI 基础镜像中。此前在设置过程中下载这些组件偶尔会导致挂起,现在这一尾部延迟问题得到缓解。
按需安装依赖
Linear 代码库是一个由 pnpm 工作空间管理的 Monorepo。API 测试工作流程此前会安装整个工作空间,尽管它只需要 API 包及其依赖。将 `pnpm install` 的范围缩小后,时间从 44-73 秒缩短至 16-18 秒。此前,每个工作都会安装完整仓库并上传依赖缓存,造成了浪费。
权衡缓存与重建
团队测试了缓存 `node_modules` 的效果,发现直接重建更快。由于缓存密钥依赖于经常更改的锁文件,即使缓存命中也需要约 28 秒恢复,而全新安装仅需约 7.5 秒。因此,缓存增加了节省时间和可变性,却未带来明显优势,故被弃用。
上述三项变化使每个分片的设置时间从 110-140 秒缩短至 67-73 秒,降幅约 44%。
避免不必要的重复设置
某些设置工作仅在输入变化时才需重复。例如,API 容器此前每次运行都会重播完整的数据库迁移历史。优化后,数据库设置时间从约 12 秒缩短至每个容器 1-2 秒。
批量处理短暂检查
七个独立的检查任务原本各自启动运行器、检出仓库、安装依赖,然后仅执行几秒钟的有效工作。团队将它们整合成两个任务,并在其中并发运行这七个检查。这使得支付相同设置费用的次数从 7 次减少到 2 次。根据 6 月份的使用数据,这一变化每月节省了约 87,000 个运行时间单位,占 CI 总使用量的 11.8%。
提高测试执行效率
在解决了设置开销后,团队能够更积极地并行化 API 测试套件。作为规模最大、执行频率最高的部分之一,其改进对合并时间产生了巨大影响。
平衡测试负载
Vitest 测试运行程序默认按文件分发工作,而非按单个测试的持续时间。这意味着异常大的测试文件可能主导某个分片,从而延迟整个套件的完成。团队将这些大文件拆分为更小、更专注的文件,同时保持测试结构不变。通过将关键任务转移到 8 个分片(此前为 3-4 个),速度提升了约 19%,成本降低了 19%。最慢的分片时间从 5.25 分钟下降到 4.33 分钟。
共享模块状态
Vitest 通常隔离每个测试文件,这意味着在每个测试片中都要重建实体、GraphQL 和装饰图。团队允许安全的文件在每个工作者内部共享模块注册表。
这是最大的单一性能提升,节省了约 17% 的月度成本。最慢的分片时间从约 300-379 秒下降到约 195 秒,API 分片的运行时间从每次 32.8 分钟下降到 22 分钟。
这也是正确性风险最高的优化。团队在每个文件上添加了选择性注释,并为共享状态添加了必要的拆解逻辑。由于部分文件使用了假定时间或共享状态,无法安全拆解,团队更新了相应的代理技能,以适应 AI 编写大部分测试的新常态。
基于低设置成本增加分片数
只有当每片固定成本较低时,进一步增加分片数量才有回报,因为加倍分片数量也会使工作流程的设置时间翻倍。之前的设置优化使得 8 个分片变得实用。此前每个碎片设置时间为 110-140 秒,8 个碎片仅设置就需 15-19 分钟,超过了测试本身的时间。现在的设置时间约为 40 秒,因此 8 个分片的总设置时间比以前的 4 个分片更少。
系统性改进的成果
如果今年早些时候没有刻意着手改进 CI,今天的测试套件运行时间大约需要 11 分钟,几乎是开发人员目前等待时间的两倍。这项工作尚未结束。随着代码库继续增长——目前每周新增约 2,000 个测试用例——保持 CI 的高速运行将是一项持续的工作,大部分新瓶颈将通过复用此次学到的经验来解决。





