CI/CD集成实战:从零搭建高效自动化部署流程

ლ奕玮 工具 阅读 1,178
赞 28 收藏
二维码
手机扫码查看
反馈

优化前:卡得不行

上个月我们团队的前端 CI/CD 流水线跑一次构建要 5 分多钟,每次提 PR 都得等半天才能看到检查结果。最离谱的是,有时候改了个文案,整个 pipeline 还是把所有依赖重新装一遍、所有测试全跑一遍,连 E2E 都不放过。本地开发还好,但一到合并请求,CI 就像老牛拉破车,动不动就超时失败。

CI/CD集成实战:从零搭建高效自动化部署流程

更烦的是,缓存经常失效。明明只改了组件样式,却因为 package-lock.json 的微小变动,导致 node_modules 全部重装。我试过手动加缓存 key,但效果不稳定,有时候缓存命中率不到 30%。那段时间,每天光等 CI 就浪费至少半小时,心态都快崩了。

找到瓶颈了!

实在忍不了,我决定深挖一下。先在 GitHub Actions 的 workflow 日志里逐行看耗时,发现几个明显问题:

  • npm install 平均耗时 2 分 10 秒
  • 单元测试跑 1 分 40 秒,但其实 80% 的测试文件根本没被改动影响
  • 构建产物上传到 Artifacts 又花了 30 多秒,而且每次都传全部文件

我用 act 在本地模拟运行 workflow,配合 time 命令打点,确认了这些确实是主要耗时点。特别是依赖安装阶段,网络波动大时甚至能卡到 3 分钟以上。看来得从这三个地方下手。

依赖安装:缓存策略大改造

之前用的是 GitHub Actions 官方推荐的 actions/cache,但 key 写得太粗糙,只用了 package-lock.json 的 hash。问题在于,只要 lock 文件有变动(哪怕只是版本号微调),缓存就失效。后来我改成组合 key,把 node 版本、操作系统、以及 lock 文件内容都考虑进去,同时用 restore-keys 做 fallback。

但真正提升大的是换包管理器。我们项目其实早就该切 pnpm 了,但一直拖着。pnpm 的硬链接 + 符号链接机制,安装速度比 npm 快不少,而且磁盘占用少。实测下来,pnpm install 平均只要 45 秒,还自带高效的缓存策略。

以下是优化后的 workflow 片段:

- name: Setup pnpm
  uses: pnpm/action-setup@v2
  with:
    version: 8

- name: Get pnpm store directory
  id: pnpm-cache
  run: |
    echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT

- name: Cache pnpm store
  uses: actions/cache@v3
  with:
    path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
    key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
    restore-keys: |
      ${{ runner.os }}-pnpm-store-

这里注意我踩过好几次坑:一开始没用 pnpm store path 动态获取路径,直接写死 ~/.pnpm-store,结果在不同 runner 上路径不一致,缓存根本没生效。后来查文档才发现得用命令动态取。

测试:只跑受影响的文件

单元测试这块,我试了两种方案。第一种是用 Jest 的 --changedSince,配合 git diff 自动检测变更文件。但问题在于,如果改了工具函数,可能影响多个测试,这种依赖关系很难自动追踪。

最后选了更简单的办法:在 workflow 里加个判断,如果 commit message 包含 [skip ci-tests] 就跳过测试(当然只对特定分支开放)。不过这治标不治本。

真正有效的还是用 turbo run。它内置了任务依赖图和增量执行,能自动分析哪些测试需要重跑。配置起来也不难,先在 package.json 里定义任务:

{
  "scripts": {
    "test": "jest",
    "build": "vite build"
  },
  "turbo": {
    "pipeline": {
      "test": {
        "dependsOn": ["^build"],
        "outputs": []
      },
      "build": {
        "outputs": [".next/**", "dist/**"]
      }
    }
  }
}

然后在 CI 里替换原来的测试命令:

# 优化前
npm test

# 优化后
npx turbo run test --filter=./src

实测下来,如果只改了一个组件,测试时间从 100 秒降到 15 秒左右。当然,第一次全量跑还是慢,但日常开发中大部分都是小改动,收益很明显。

构建产物:只传变化的部分

之前我们用 actions/upload-artifact 一股脑传整个 dist 目录,其实 90% 的文件都没变。后来改用 changesets 的思路——只对比差异再上传。不过 changesets 主要是发版用的,不太适合 CI。

折腾了半天发现,其实 GitHub Actions 本身支持基于文件 hash 的增量上传,但官方 action 不提供。最后我用了一个社区方案:先生成文件清单,再和上次的对比,只传新增或修改的。

核心代码就这几行:

# 生成当前文件 hash 清单
find dist -type f -exec md5sum {} ; | sort > current.manifest

# 下载上次的清单(如果存在)
if [ -f previous.manifest ]; then
  # 找出差异文件
  comm -23 <(cut -d' ' -f2 current.manifest) <(cut -d' ' -f2 previous.manifest) > changed_files.txt
else
  # 第一次全传
  find dist -type f > changed_files.txt
fi

# 只上传 changed_files.txt 里的文件
tar -czf dist-changed.tar.gz -T changed_files.txt

不过这个方案有点重,后来简化了:直接用 rsync 同步到临时目录再上传,省去了手动 diff 的步骤。虽然还是传整个目录,但因为缓存和压缩,实际传输体积小了很多。最终这一步从 30 秒压到 8 秒,够用了。

性能数据对比

把上面几招全加上之后,CI 时间大幅下降。以下是连续 5 次 PR 构建的平均数据:

  • 依赖安装:2 分 10 秒 → 45 秒(↓60%)
  • 单元测试:1 分 40 秒 → 22 秒(↓78%,小改动场景)
  • 产物上传:32 秒 → 9 秒(↓72%)
  • 总耗时:5 分 15 秒 → 1 分 20 秒(↓75%)

最爽的是,现在改个按钮颜色,CI 80 秒内就能跑完,基本不用等。而且缓存命中率稳定在 90% 以上,再也不用担心网络抖动导致安装失败。

当然,也不是完美无缺。比如 E2E 测试还没优化,因为涉及真实浏览器启动,暂时没找到好办法。不过日常开发中 E2E 只在主干跑,影响不大。

结尾唠叨两句

这次优化最大的体会是:CI 性能问题往往不是单一环节导致的,得系统性地看整个流水线。别一上来就猛搞并行,先把最耗时的几个点揪出来,针对性解决。另外,工具选型很重要,pnpm + turbo 这套组合拳比死磕 npm 脚本高效多了。

以上是我踩坑后的总结,希望对你有帮助。有更优的实现方式欢迎评论区交流,比如你们是怎么处理 E2E 测试提速的?我还在找方案……

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论