"速度"与"质量"不是选择题------我们如何用 CI/CD 同时保住两者
"发布太快会出 bug,求稳就只能慢。"------这是很多团队的真实困境,但它建立在一个错误前提上:速度和质量被当成了跷跷板的两端,此消彼长。
事实上,在成熟的工程体系里,它们更像是正反馈循环的两端:自动化兜底越厚,人就越敢快;发布越快越碎,单次变更的风险面就越小,质量反而越稳。下面是我们团队在过去一年里,把这套逻辑落到 CI/CD 流水线上的具体做法。
先拆一个误区:为什么"慢 = 稳"是错觉
传统发布流程里,一个功能从代码提交到用户手上,通常要经过:
开发 → 自测 → 提测 → QA 手工回归 → 预发验证 → 发布评审 → 上线
这个链条的问题不在于"步骤多",在于每一步之间的等待时间都是浪费:
- 开发提测后等 QA 排期:2--3 天
- QA 手工回归全量功能:1--2 天
- 发布评审排队:半天
等待期间代码不会变得更好,但会变得更难回滚------因为其他人的提交已经叠上来了,分支越偏离主干越远,合并冲突和集成风险指数级上升。
所以"慢"保住的不是质量,是人的注意力窗口------QA 有足够时间盯着看。但人的注意力是不可靠的,可重复的系统化检查才是。
我们的解法:把"人的检查"换成"机器的检查"
核心思路就一句话:让机器在分钟级完成原来人需要天级完成的验证,人只负责机器做不了的事(设计、边界判断、探索性测试)。
第一层:提交即检查(Pre-commit / Pre-push)
sql
git commit → husky → lint-staged → ESLint + Prettier + tsc --noEmit
这一步的目标不是"拦住坏代码",是把低级错误消灭在开发者自己的终端里,不浪费 CI 资源,不污染 PR 历史。
我们踩过的坑:
- 一开始把所有检查都放 pre-commit,前端项目 30 秒的 hook 让开发者想砸键盘 → 后来拆成 pre-commit 只跑 lint+format,type check 放 CI
lint-staged配了--max-warnings 0,但新人第一次提交被拦住一脸懵 → 加了 husky 的prepare脚本自动装,README 里写清楚"被拦了怎么办"
第二层:PR 即门禁(CI Pipeline)
每个 PR 触发一条流水线,并行跑四件事:
| 阶段 | 做什么 | 耗时(我们项目) |
|---|---|---|
| 依赖安装 | pnpm install --frozen-lockfile |
~45s |
| 静态检查 | TypeScript 全量编译 + ESLint | ~2min |
| 单元测试 | Vitest,按变更文件做 affected 测试 | ~1.5min |
| 构建验证 | 生产构建是否通过 | ~3min |
关键设计决策:
1. 测试按变更范围跑,不全量跑
全量跑 2000 个单测要 8 分钟,PR 等 8 分钟开发者就去刷手机了。我们用 Nx 的 affected 机制,只跑被改动文件及其下游依赖相关的测试,平均降到 1.5 分钟。全量测试留给 nightly build 和发布前 gate。
2. 构建验证不是"可选步骤"
很多团队 CI 只跑测试不跑构建,结果合并后 main 分支构建挂了------因为有人 import 了一个没声明的依赖,或者 tsconfig 路径别名写错。构建挂 = 整条线停,这个代价必须让 PR 阶段就付。
3. 失败快速反馈
把最可能失败、最快出结果的步骤放最前面。TypeScript 编译 30 秒就能报类型错,没必要等 3 分钟的构建跑完才告诉开发者"你少写了一个字段"。
第三层:合并即部署(CD)
PR 合并到 main 后:
css
main 更新 → 触发部署流水线 → 构建镜像 → 部署到 staging → 自动化冒烟 → 自动/手动 promote 到生产
这里有两个关键选择:
选择一:Staging 环境全自动部署,不卡人
合并即部署到 staging,不需要任何人点按钮。staging 挂了?自动回滚 + 钉钉告警。这意味着 staging 永远等于 main 的最新状态,QA 和 PM 随时能看最新效果。
选择二:生产发布用"渐进式发布"而非"全量切换"
我们不用"停服发布"或"蓝绿全切",用的是金丝雀:
erlang
新版本先接 5% 流量 → 监控错误率和延迟 5 分钟 → 无异常 → 25% → 50% → 100%
任何一步指标异常,自动回滚。这个机制让我们敢在工作日下午 5 点 50 分发版------因为最坏情况就是 5% 用户受影响 5 分钟,然后自动回滚,没人需要半夜爬起来。
那些"但是"------真实代价
说完了好处,说代价。这套体系不是免费的:
1. 维护成本
流水线本身会坏。依赖缓存失效、Runner 磁盘满、某个第三方 action 更新了 breaking change------每月至少 1--2 次 CI 故障需要排查。这需要有人 owning,不能"配完就忘"。
2. 测试质量的"垃圾进垃圾出"
自动化测试覆盖的是"你想到要测的东西"。如果测试本身就是敷衍的(比如只 expect(true).toBe(true) 凑覆盖率),那 CI 绿条就是个安慰剂。我们后来加了 mutation testing (用 Stryker)来验证测试的有效性------把代码里的一个 > 改成 <,看测试能不能抓到。抓不到?说明测试在裸奔。
3. 初期投入大
从零搭这套东西,前 3 个月基本是在"写测试基础设施"而不是"写业务"。管理层需要理解这个投入的回报周期,否则很容易在中途被砍。
效果:数字说了什么
跑了一年后对比:
| 指标 | 之前(纯手工流程) | 现在 |
|---|---|---|
| 平均发布周期(代码合并到生产) | 5--7 天 | 4--6 小时 |
| 生产回滚率 | ~15% | ~3% |
| 紧急 hotfix 数量/月 | 8--12 次 | 2--3 次 |
| 开发者"等流程"时间/天 | 1.5--2h | < 20min |
最意外的一个收益:开发者幸福感明显提升。不是因为"工具酷",是因为"我改了一行代码不用提心吊胆等三天看它炸不炸"。这种心理安全感直接转化为更频繁的重构和更干净的代码------因为试错成本趋近于零。
一句话总结
速度和质量不是跷跷板,是飞轮。自动化检查越厚,发布越频繁;发布越频繁,单次变更越小;单次变更越小,风险越低;风险越低,自动化检查越敢快。 转起来之后,快就是稳,稳就是快。