流水线阶段划分:源码获取、构建、测试、部署

做过CI流水线的人都知道,阶段划分是设计的"骨架"。骨架歪了,后面再怎么填肉都是白搭。我个人习惯把流水线切成四个标准阶段——源码获取、构建、测试、部署。为什么是这四个?说白了,每个阶段对应一个核心问题:代码从哪来、怎么变成制品、质量过不过关、怎么跑起来。

你想想看,如果阶段职责不清,就会出现"构建阶段偷偷改代码"或者"测试阶段才去装依赖"这种尴尬事。我在项目中遇到过好几次,因为阶段边界模糊,导致流水线一跑就是两个小时,最后还经常挂。后来我狠下心来把阶段拆干净,问题少了一大半。

阶段划分的底层逻辑

每个阶段必须有明确的输入、动作、产出。输入是上一阶段给你的东西,动作是你这个阶段要干的事,产出是交给下一阶段的成果。三个要素缺一个,这个阶段就不该存在。

我总结了一个对照表,帮你快速理解每个阶段的定位:

阶段 核心职责 典型任务数 关键产出物 粒度控制要点
源码获取 确保代码正确拉取与校验 3-5 源代码目录、检查报告 避免一次拉取过多分支,增量检出
构建 将源码转为可交付制品 3-5 构建产物、Docker 镜像 合理利用缓存,避免重复编译
测试 验证质量与功能正确性 4-6 测试报告、覆盖率数据 按金字塔模型分层并行
部署 将制品发布到目标环境 3-5 部署记录、监控告警 灰度策略与回滚能力必须内置
核心原则:每个阶段的任务粒度控制在 3-5 个原子操作。少于 3 个说明拆得太粗,多于 5 个说明塞了太多不相干的事。

下面这张图展示了四个阶段的完整脉络,建议你先看一遍图,再往下读细节:

流水线阶段划分与任务粒度设计 源码获取 ▸ 代码检出与分支校验 ▸ 代码静态检查 ▸ 依赖版本锁定 ▸ 增量变更分析 构建阶段 ▸ 依赖安装与缓存 ▸ 编译与代码生成 ▸ 制品打包与签名 ▸ 镜像构建与推送 测试阶段 ▸ 单元测试与覆盖率 ▸ 集成测试与契约 ▸ 端到端冒烟测试 ▸ 质量门禁卡点 部署阶段 ▸ 环境初始化与检查 ▸ 灰度发布与回滚 ▸ 健康检查与监控 ▸ 自动化验证 🔑 每个阶段的职责边界必须清晰,任务粒度建议控制在 3-5 个原子操作之间 阶段间通过制品和报告进行数据传递,避免跨阶段耦合

源码获取阶段——别让第一块多米诺骨牌倒下

源码获取是整个流水线的起点。很多人觉得这步简单,不就是 git clone 吗?其实不然。我见过太多流水线在这步埋雷——要么拉取了不该拉的分支,要么依赖锁了一堆没用的版本。

这个阶段我习惯拆成四个任务:

  • 代码检出与分支校验——只拉取本次构建需要的分支,用 refs 规范控制,别一把全拉下来。
  • 代码静态检查——在构建之前先跑 ESLint、Checkstyle 这类工具,早发现问题早止损。
  • 依赖版本锁定——用 lockfile 确保每次构建的依赖版本一致,别让"昨天还能跑今天挂了"的事发生。
  • 增量变更分析——对比上一次构建,只处理有变化的模块。这能省下大量时间。

我曾经在一个大型 monorepo 项目里,没做增量分析,每次都是全量检出。结果流水线跑一次要40分钟,其中光代码检出就占了8分钟。后来加上增量策略,检出时间降到了30秒以内。

我的建议:源码获取阶段不要做任何"可能改变代码逻辑"的事。校验、检查、锁定都可以,但不要修格式、不要自动合并——那是构建阶段的事。

构建阶段——把原材料变成产品

构建阶段的输入是源码,输出是制品。说白了,就是"编译 + 打包 + 镜像"。这个阶段的粒度设计直接影响流水线的速度。

我常用的拆分方式:

  1. 依赖安装与缓存——npm install、mvn install 这类操作,必须配缓存。没缓存的构建就是在烧钱。
  2. 编译与代码生成——真正的编译过程,比如 TypeScript → JavaScript,或者 Java 源码 → class 文件。
  3. 制品打包与签名——生成 jar、war、zip 等交付物,加上签名确保完整性。
  4. 镜像构建与推送——如果是容器化部署,这里把制品打成 Docker 镜像并推送到仓库。

你想想看,如果把依赖安装和编译混在一起,缓存就没法用了。每次都得重新下载依赖,那速度能快才怪。我记得有一次帮一个团队优化流水线,就是把依赖安装单独拎出来并配了缓存,构建时间从15分钟降到了3分钟。

注意:构建阶段不要做"编译+测试+打包"一锅端的事。一旦某个子任务失败,整个阶段都得重跑。拆开做,失败了只重跑对应任务。

测试阶段——质量门禁不能形同虚设

测试阶段的核心是"分层 + 并行"。按照测试金字塔,从上到下是:端到端测试、集成测试、单元测试。每层的任务粒度和并行策略都不一样。

我习惯这样拆:

  • 单元测试与覆盖率——单测必须快,跑完立刻出覆盖率报告。我一般要求单测在3分钟内跑完,超过5分钟就说明拆得不够细。
  • 集成测试与契约——验证模块之间的接口是否对得上。用契约测试替代部分端到端测试,能省不少时间。
  • 端到端冒烟测试——只覆盖核心链路,别想着全量跑。全量E2E测试放到夜间或预发布环境去做。
  • 质量门禁卡点——覆盖率低于阈值?静态检查有严重问题?直接卡住,不让往下走。

这里有个坑。我曾经把E2E测试和单元测试放在同一个阶段里串行跑,结果每次都要等40分钟才知道单测过了没。后来我把单元测试和E2E拆成独立子任务,并行执行,整体时间缩短了60%。

一个实用技巧:测试阶段的任务可以按"耗时从短到长"排序。短任务先跑,快速反馈;长任务后跑,同时做深度检查。这样即使长任务挂了,你已经拿到了大部分质量信号。

部署阶段——把制品交到用户手里

部署阶段是流水线的最后一环,也是风险最高的一环。很多团队在这个阶段偷懒,搞个 scp 上去就完事了。但我告诉你,部署阶段的任务粒度设计直接决定了你的"回滚能力"。

标准的部署阶段应该包含:

  • 环境初始化与检查——确认目标环境的资源、配置、依赖是否就绪。避免"部署到一半发现磁盘满了"的尴尬。
  • 灰度发布与回滚——先推给一小部分实例,观察没问题再全量。同时准备好回滚脚本,一旦异常立马切回来。
  • 健康检查与监控——部署完成后自动检查接口、日志、指标。别等用户报错你才发现。
  • 自动化验证——用 smoke test 验证核心功能在目标环境上正常运转。

我个人习惯把"灰度发布"和"全量发布"拆成两个独立子任务,中间加一个人工审批卡点。这样做的好处是,灰度阶段发现了问题,只需要回滚灰度的那一小部分,影响面非常小。

血的教训:我曾经在一个项目里把"发布+健康检查"做成了一个任务,结果发布失败了,健康检查根本没跑,直接标记为成功。等到用户反馈问题,已经过去了15分钟。从那以后,我坚持把健康检查单独拆出来,并且失败后自动触发回滚。

阶段间的数据传递

四个阶段不是孤立的,它们之间需要传递数据。最常见的传递方式有两种:

  • 制品传递——构建阶段产生的 jar、镜像等产物,通过制品仓库传给测试和部署阶段。
  • 报告传递——源码获取阶段的检查报告、测试阶段的覆盖率报告,通过持久化存储共享。

这里有一个关键点:阶段之间不要直接传递文件。你想想看,如果构建阶段直接把产物放在本地磁盘上,测试阶段去读,那流水线就没法分布式跑了。正确的做法是把产物推送到中间仓库,下游阶段再去拉。

我的习惯:每个阶段运行前,先声明自己需要什么输入、会产出什么输出。把这些写在一个 pipeline.yml 里,团队成员一眼就能看出数据流向。

说到底,阶段划分没有银弹。但只要你抓住"职责边界清晰、任务粒度适中、数据传递规范"这三个要点,流水线就不会跑偏。我见过太多团队在阶段划分上栽跟头,其实就是没想清楚每个阶段到底该干什么。

一句话总结:阶段划分的本质是"关注点分离"。源码获取关注代码完整性,构建关注制品正确性,测试关注质量达标,部署关注发布安全性。各司其职,流水线才稳。