持续集成与发布
“发布前记得先跑一下测试”“记得改一下那个配置”“记得先执行数据库脚本”……当发布流程依赖这么多“记得”,出事只是时间问题:
- 有人忘了执行数据库脚本,新代码上线后一直报错;
- 测试环境验证的是一个包,上线时又重新构建了一次,结果不一样;
- 部署平台显示“发布成功”,服务实际上起不来;
- 出了问题想回退,才发现上一个版本的包已经找不到了。
持续集成与发布(CI/CD)就是把这些“记得”写成自动执行的流程:每一次变更都经过同样的检查,每一次发布都走同样的步骤。
读完这篇你会
- 搭建一条从提交到上线的最小流水线
- 让数据库变更和代码发布可以安全地分步进行
- 让发布可观察、可回滚
1. 原理:机场安检和登机口
可以把 CI/CD 想成机场:
- **持续集成(CI)**是安检:每一位旅客(每一次提交)都要经过同样的检查(lint、类型检查、测试、构建),不管是谁、不管多急,不通过就不能进入候机区(不能合并);
- **持续交付/部署(CD)**是登机:通过安检的那个人(那个构建产物),原样登上飞机去往目的地(测试环境、生产环境)。不能安检时是一个人,登机时换成另一个人。
text
提交代码
→ 检出代码,按锁文件安装依赖
→ lint、类型检查、测试
→ 构建产物(打上版本号,存档)
→ 部署到测试/预发环境 → 验证
→ 同一个产物部署到生产 → 健康检查 → 观察指标每一步都要有清晰的失败信号:失败了就停下,并且告诉大家哪一步、为什么失败。
2. 一个最小的 CI 配置
yaml
# .github/workflows/ci.yml
name: CI
on:
pull_request:
push:
branches: [main]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci # 严格按锁文件安装
- run: npm run lint
- run: npm run typecheck
- run: npm test
- run: npm run buildCI 调用的是和本地开发完全相同的脚本。在代码托管平台上把这个检查设为合并的必要条件,不通过就不能合并。
3. 坑一:每个环境都重新构建一次
现象:测试环境验证通过后,生产发布时又重新构建了一次。这次构建拉到了某个依赖的新版本,上线后出现了测试环境里从没出现过的问题。
修复:构建一次,到处部署。构建产物(前端的 dist 包、后端的镜像)带上版本号存档,测试环境验证的是哪个产物,生产部署的就是哪个产物。环境之间的差异只通过配置注入,见 环境与配置管理。
4. 坑二:数据库变更和代码发布“打架”
现象:新版本把 users.name 字段改名为 users.nickname,发布时先执行了数据库脚本。在新代码完全部署完成之前的几分钟里,还在运行的旧代码查询 name 字段全部报错。
原因:发布不是瞬间完成的。滚动发布时,新旧两个版本的代码会同时运行一段时间;即使停机发布,一旦需要回滚,旧代码也得能在新的表结构上运行。
修复:数据库变更要做到新旧代码都能兼容,把一次“改名”拆成几次发布(扩展—迁移—收缩):
text
第 1 次发布:新增 nickname 字段(可空);代码同时写入 name 和 nickname
第 2 步: 后台任务把历史数据的 name 复制到 nickname
第 3 次发布:代码改为读取 nickname
第 4 次发布:确认稳定后,删除 name 字段和相关代码每一步都可以单独回滚,任何时刻正在运行的代码都能正常工作。删除字段、修改类型这类不可逆的操作,放在最后,并且确认不再需要回滚之后再做。
5. 坑三:显示“发布成功”,服务其实起不来
现象:部署平台显示发布成功,实际上新版本因为缺少一个配置,启动后立即崩溃,用户访问全部失败。
原因:“发布成功”只说明文件传上去了、进程启动命令执行了,并不说明服务能正常处理请求。
修复:
- 服务提供健康检查接口,不仅返回“进程活着”,还检查数据库、缓存等关键依赖是否可用;
- 部署平台在新实例的健康检查通过之后,才把流量切过去;不通过就停止发布,保留旧版本继续服务;
- 发布后自动跑一组冒烟测试(登录、查看列表、下一个测试订单),失败就自动回滚。
6. 坑四:出了问题,回滚不了
现象:新版本上线后出现严重问题,想回退到上一个版本,才发现:上一个版本的构建产物没有保存;或者数据库已经做了不兼容的变更,旧代码跑不起来了。
修复:
- 保留最近几个版本的构建产物,回滚就是“重新部署上一个产物”,几分钟内完成;
- 数据库变更遵循上一节的兼容原则,保证回滚后旧代码仍能运行;
- 定期演练回滚,确认流程真的可用,而不是到出事时才第一次尝试;
- 每次发布记录:代码版本、配置版本、发布时间、发布人。
7. 坑五:CI 日志里出现了密钥
现象:为了排查 CI 失败,在脚本里加了 echo $DATABASE_URL,或者开启了 shell 的 set -x(打印每一条执行的命令及其参数),密钥就这样出现在了 CI 日志里,所有能看到日志的人都能看到。
修复:
- 密钥只通过 CI 平台的加密变量注入,平台通常会自动把它们在日志中替换成
***,但经过编码或拼接后的值可能无法被识别; - 不要在日志中打印环境变量和完整的连接串;
- 一旦泄露,立即更换密钥。
8. 坑六:流水线太慢,大家开始绕过它
现象:一次 CI 要跑三十分钟,大家开始“先合并,有问题再说”,或者要求管理员跳过检查直接合并。
修复:
- 缓存依赖,避免每次都完整下载;
- 并行执行互不依赖的步骤,比如 lint 和测试同时跑;
- 把最快、最容易失败的检查放在前面,尽早失败;
- 清理重复和过时的检查,把慢的端到端测试限制在关键流程。
流水线的速度直接决定了大家是否愿意遵守它。
9. 降低发布风险
- 小步发布:每次发布的改动越少,出问题时越容易定位和回滚;
- 灰度发布:先发布给一小部分用户或一台服务器,观察没有问题再逐步扩大;
- 功能开关:高风险的新功能默认关闭,部署后再逐步打开,出问题时关闭开关即可;
- 发布后观察:看错误率、响应时间、关键业务指标(下单量、支付成功率),而不只是看部署任务是否成功。
排查思路
- CI 失败:先看是哪一步失败、第一条错误信息是什么;本地用同样的命令(特别是
npm ci)在干净的环境里复现。 - 线上行为和测试环境不一致:确认两边部署的是不是同一个版本的产物,配置有什么差异。
- 发布后出问题:对比发布前后的错误率和指标曲线,确认问题是否从这次发布开始;查看发布记录里这次包含了哪些改动。
- 回滚后问题仍在:检查是否有数据库变更、配置变更或外部依赖变化,问题不一定来自代码。
检查清单
- [ ] 每次提交都自动运行 lint、类型检查、测试和构建,失败不能合并
- [ ] 构建一次,同一个产物部署到所有环境
- [ ] 数据库变更新旧代码都能兼容,不可逆操作放在最后
- [ ] 有健康检查和发布后冒烟测试,失败自动停止或回滚
- [ ] 保留历史产物,回滚流程定期演练
- [ ] CI 日志中不会打印密钥
- [ ] 流水线有缓存和并行,保持足够快