Skip to content

持续集成与发布 ​

“发布前记得先跑一下测试”“记得改一下那个配置”“记得先执行数据库脚本”……当发布流程依赖这么多“记得”,出事只是时间问题:

  • 有人忘了执行数据库脚本,新代码上线后一直报错;
  • 测试环境验证的是一个包,上线时又重新构建了一次,结果不一样;
  • 部署平台显示“发布成功”,服务实际上起不来;
  • 出了问题想回退,才发现上一个版本的包已经找不到了。

持续集成与发布(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 build

CI 调用的是和本地开发完全相同的脚本。在代码托管平台上把这个检查设为合并的必要条件,不通过就不能合并。

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. 降低发布风险 ​

  • 小步发布:每次发布的改动越少,出问题时越容易定位和回滚;
  • 灰度发布:先发布给一小部分用户或一台服务器,观察没有问题再逐步扩大;
  • 功能开关:高风险的新功能默认关闭,部署后再逐步打开,出问题时关闭开关即可;
  • 发布后观察:看错误率、响应时间、关键业务指标(下单量、支付成功率),而不只是看部署任务是否成功。

排查思路 ​

  1. CI 失败:先看是哪一步失败、第一条错误信息是什么;本地用同样的命令(特别是 npm ci)在干净的环境里复现。
  2. 线上行为和测试环境不一致:确认两边部署的是不是同一个版本的产物,配置有什么差异。
  3. 发布后出问题:对比发布前后的错误率和指标曲线,确认问题是否从这次发布开始;查看发布记录里这次包含了哪些改动。
  4. 回滚后问题仍在:检查是否有数据库变更、配置变更或外部依赖变化,问题不一定来自代码。

检查清单 ​

  • [ ] 每次提交都自动运行 lint、类型检查、测试和构建,失败不能合并
  • [ ] 构建一次,同一个产物部署到所有环境
  • [ ] 数据库变更新旧代码都能兼容,不可逆操作放在最后
  • [ ] 有健康检查和发布后冒烟测试,失败自动停止或回滚
  • [ ] 保留历史产物,回滚流程定期演练
  • [ ] CI 日志中不会打印密钥
  • [ ] 流水线有缓存和并行,保持足够快

延伸阅读 ​

为复用而记录,为理解而整理。