“GitHub Actions 自带的包管理缓存是我用过最差的一块积木。”—— 某云原生团队在 200 个 Runner 同时爆燃 35 分钟后留下这句吐槽。本文把骂声翻译成技术拆解,再给出能立即落地的参数与清单。
一、黑盒里到底装了多少 “雷”
-
锁文件只管 key,不管校验
setup-node默认把hashFiles('**/package-lock.json')直接当缓存 key;只要哈希对不上,整包废弃重拉,没有增量补丁。锁文件本身并不写入缓存 tarball,也无法在解压后做二次校验 —— 相当于用门牌号当房产证。 -
缓存 “只写不改”
官方文档白纸黑字:「不能修改已有缓存」。依赖一旦升级,旧 key 瞬间失效;如果 50 个 feature 分支同时 rebase,50 份新缓存会并发上传,10 GB 上限秒满,触发 “缓存雪崩”——Runner 排队从 2 min 飙到 30 min。 -
restore-keys 的 “前缀回退” 在 monorepo 里放毒
服务 A 升级了eslint@9,服务 B 的 key 没命中,于是回退到runner.os-node- 前缀,把 A 的旧 node_modules 抱回家,结果 B 产物里混进两份不同版本的 eslint,幽灵 Bug 上线。 -
TTL 与时间戳黑箱
GitHub 全局缓存 7 天过期,但setup-*不暴露最后命中时间。团队只能靠 “感觉” 改 key,每周一上午集体 cache-miss,重新拉包 3 GB,咖啡喝完构建还没完。
二、三条官方规则如何叠加成灾难
| 规则 | 触发条件 | 实际表现 |
|---|---|---|
| hashFiles 精确 key | 锁文件任一字符变化 | 整包重建,无增量 |
| 不能修改缓存 | 同一 key 已存在 | 强制写新 key,旧包立刻失效 |
| restore-keys 前缀匹配 | 精确 key 未命中 | 回退到 “最近” 前缀,可能拿到别人过期依赖 |
当大版本升级撞上周一早高峰,三条规则同时点火:
- 大量分支同时推送 → 锁文件变更 → 精确 key 统一失效
- 不能改缓存 → 人人写新 key → 并发上传打满 10 GB
- restore-keys 回退 → 跨目录拉错包 → 构建成功但产物错
于是出现 “绿色对勾交付错误代码” 的奇观。
三、可立即落地的 4 组参数
-
key 加 “周序号”,强制滚动失效
用%U把一年中的周序号拼进 key,每周第一次构建必然失效,避免 7 天 TTL 的 “黑箱抽奖”。key: ${{ runner.os }}-node-w$(date +%U)-${{ hashFiles('**/package-lock.json') }} -
path 拆目录,减少单包体积
把 “包管理器全局缓存” 与 “项目 node_modules” 分开,降低单次上传量,也避免跨项目污染。path: | ~/.npm **/node_modules -
restore-keys 去前缀,禁止 “跨服务乱亲”
只保留同服务、同 OS 的最近一次缓存,拒绝回退到兄弟目录的旧包。restore-keys: | ${{ runner.os }}-node-w$(date +%U)- -
雪崩时手动限速 30% Runner
在 repo settings → Actions → Runners 里把最大并发数临时砍 30%,让缓存上传错峰,避免 10 GB 配额瞬间被打光。
四、可重现构建的 5 步清单
-
锁文件必须进仓库
没有package-lock.json/pnpm-lock.yaml就别谈重现;setup-node会直接抛Dependencies lock file is not found。 -
pin 包管理器版本
在package.json写死"packageManager": "pnpm@9.1.0",防止 Runner 偷偷升级,导致缓存内容漂移。 -
CI 内核打标
在 job 末尾加一步:- name: Export cache hit run: echo "CACHE_HIT=${{ steps.cache.outputs.cache-hit }}" >> $GITHUB_ENV把命中状态写进产物,任意分支都能回溯最后一次真正用到缓存的时间。
-
多项目仓库显式写
cache-dependency-path
避免 glob 回退到根目录:cache-dependency-path: 'frontend/pnpm-lock.yaml' -
定期 “冷构建” 验证
每月手动清一次缓存(Settings → Actions → Caches → Delete all),强制走全量安装,确保锁文件与远端 registry 仍能对齐。
五、结语:官方不改,就只能自己接管 key
GitHub Actions 的 “内建包管理器” 把锁机制做成了看起来省心、实则踩坑的黑盒:锁文件只当门牌,不当房产证;缓存只能写不能改;TTL 与命中时间完全不透明。只要这三条规则不变,“最差” 标签就撕不掉。工程上的唯一出路是:
- 把 key 设计权从官方手里夺回来
- 用周序号、拆路径、去前缀、限速并发四件套,把雪崩概率压到可接受区间
- 用 5 步清单把 “可重现” 变成强制门禁,而不是口头愿景
锁机制本应是确定性之锚,但在默认策略里却成了随机数发生器。与其等官方发新版,不如现在就把 key 写死、把并发限死、把清单焊死 —— 让 Runner 排队时间从 30 min 回到 3 min,让 “绿色对勾” 第一次真正代表可信的构建。
参考资料
- GitHub Docs《缓存依赖项以加快工作流程》, 2025-02
- 社区讨论《schedule 时区与缓存失效》, https://github.com/community/community/discussions/13454