GitHub Actions 缓存不命中排查与修复:actions/cache 实战指南
快速答案
- 核心结论:GitHub Actions 缓存不命中的根本原因通常是 cache key 不稳定、缓存路径错误或分支作用域限制,而非 actions/cache 本身的问题。
- 第一排查点:检查 cache key 是否包含
hashFiles('**/package-lock.json')且该文件已提交到仓库;确认path参数指向工具的实际缓存目录(如~/.npm)而非node_modules。 - 最小修复方案:在
actions/cache@v4的with块中同时设置key和restore-keys,确保精确匹配失败时有回退机制。 - 适用环境:GitHub Actions 所有运行环境(ubuntu-latest、windows-latest、macos-latest),actions/cache@v3 及以上版本。
它解决什么问题 / 适用场景
actions/cache 是 GitHub Actions 官方提供的缓存动作,用于在 CI/CD 工作流中缓存依赖项和构建产物,从而加速后续运行。它解决的核心问题是:
- 重复下载依赖:每次 CI 运行都从远程注册表下载所有依赖(如 npm、pip、Maven),浪费带宽和时间。
- 构建产物复用:缓存编译后的二进制文件或测试报告,避免重复编译。
适用场景:
- Node.js 项目(缓存
~/.npm或node_modules) - Python 项目(缓存
~/.cache/pip或虚拟环境) - Java/Gradle 项目(缓存
~/.gradle/caches) - Go 项目(缓存
~/go/pkg/mod) - 多分支开发、低流量仓库(缓存易过期,需额外注意)
核心配置 / 参数说明
actions/cache 有三个核心参数,下表详细说明每个参数的作用和最佳实践:
| 参数 | 是否必填 | 说明 | 示例值 | 最佳实践 |
|---|---|---|---|---|
path | 是 | 要缓存的文件或目录路径,支持通配符 | ~/.npm | 缓存工具的实际缓存目录,而非项目目录 |
key | 是 | 缓存唯一标识,通常包含 runner OS 和 lock 文件 hash | ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} | 使用稳定标识符,避免包含 github.sha 或时间戳 |
restore-keys | 否 | 精确 key 未命中时的回退 key 列表,按顺序尝试 | ${{ runner.os }}-npm- | 至少提供一个前缀回退,确保跨分支缓存共享 |
完整配置示例(Node.js 项目)
YAML- name: Cache npm dependencies uses: actions/cache@v4 with: path: ~/.npm key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-npm-
关键要点:
path设置为~/.npm而非node_modules,因为npm ci会先删除node_modules再安装,缓存node_modules会导致浪费。key使用hashFiles('**/package-lock.json'),只有依赖变化时 key 才会改变。restore-keys提供前缀回退,即使精确 key 不匹配,也能恢复最近的缓存。
与同类方案对比
| 对比维度 | actions/cache | setup-node 的 cache 输入 | 手动缓存(如 tar/upload-artifact) |
|---|---|---|---|
| 缓存命中率 | 高(支持 restore-keys 回退) | 中(仅精确匹配) | 低(需手动管理 key) |
| 配置复杂度 | 中等(需理解参数) | 低(一行配置) | 高(需编写脚本) |
| 跨分支缓存共享 | 支持(通过 restore-keys) | 不支持 | 需手动实现 |
| 缓存路径灵活性 | 高(任意路径) | 低(仅工具默认路径) | 高(任意路径) |
| 对 lock 文件的依赖 | 强(推荐使用 hashFiles) | 强(自动检测) | 无(需自行设计 key) |
| 缓存过期策略 | 7 天未访问自动清除 | 同 actions/cache | 同 actions/cache |
结论:actions/cache 提供最灵活的缓存控制,适合需要精细管理缓存的场景;setup-node 的 cache 输入适合快速配置的简单项目。
生产环境实践与注意事项
生产部署限制
- 缓存作用域为分支:非默认分支的 PR 首次构建必然 miss,因为缓存只能被同一分支或默认分支的工作流读取。
- 缓存自动清除:缓存 7 天未访问即被 GitHub 自动清除,低流量仓库需定期触发构建。
- 仓库总缓存上限 10GB:超出后旧缓存被逐出,需监控缓存使用量。
- 缓存路径必须准确:恢复成功但无加速效果,通常是因为
path指向了错误目录。 - 并发构建冲突:多个 job 同时保存相同 key 的缓存可能导致写入冲突,建议在关键 job 中使用
save-always: false。
安全性建议
- 避免缓存敏感文件:不要缓存
.env、密钥、证书等敏感文件。 - 缓存内容应仅包含依赖和构建产物:不要缓存源代码或配置文件。
跨分支缓存共享策略
YAML- name: Cache npm dependencies uses: actions/cache@v4 with: path: ~/.npm key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-npm-
关键点:restore-keys 中的 ${{ runner.os }}-npm- 前缀会匹配默认分支(如 main)上保存的缓存,即使 PR 分支的精确 key 不同,也能恢复最近的缓存。
常见报错与排查
报错 1:Cache not found for input keys
错误信息:Cache not found for input keys: Linux-node-abc123def456...
根因:cache key 不稳定或 lock 文件未提交。
解决方案:
- 检查
hashFiles引用的文件(如package-lock.json)是否已提交到仓库。 - 确保该文件未被工作流中的其他步骤重新生成。
- 添加
restore-keys作为回退。
报错 2:Cache restored successfully but node_modules is still empty
错误信息:缓存恢复成功,但 npm ci 仍然重新下载所有依赖。
根因:缓存路径错误,缓存了 node_modules 而非 ~/.npm。
解决方案:
- 将
path改为~/.npm(npm 的全局缓存目录)。 - 使用
npm ci而非npm install,因为npm ci会从缓存中安装。
报错 3:Cache key changes on every run despite no dependency changes
错误信息:每次运行 key 都不同,即使依赖没有变化。
根因:hashFiles 引用了不稳定的文件(如 package.json 被生成步骤修改)。
解决方案:
- 改用
package-lock.json或yarn.lock作为 hash 源。 - 确保该文件已提交到仓库,且不被工作流修改。
报错 4:Cache saved on a different branch and not available
错误信息:缓存保存在其他分支,当前分支无法读取。
根因:缓存作用域为分支级别。
解决方案:
- 确保工作流在默认分支(如 main)上运行,因为默认分支的缓存对所有分支可读。
- 在 PR 工作流中添加
restore-keys以匹配默认分支的缓存前缀。
常见问题 FAQ
Q: 为什么我的 npm 缓存明明恢复了,但 npm ci 还是重新下载了所有依赖?
A: 最常见的原因是缓存路径错误。如果你缓存了 node_modules 而不是 ~/.npm,npm ci 会先删除 node_modules 再安装,导致缓存浪费。正确的做法是缓存 ~/.npm(npm 的全局缓存目录),然后运行 npm ci,它会从缓存中快速安装依赖。
Q: 我的 PR 构建总是缓存 miss,但 main 分支构建正常,怎么办?
A: GitHub Actions 的缓存作用域是分支级别的,PR 分支只能读取目标分支(通常是 main)的缓存。确保 main 分支的工作流已经运行并保存了缓存。在 PR 工作流中添加 restore-keys 回退到 main 分支的缓存前缀,例如:restore-keys: ${{ runner.os }}-npm-。这样即使精确 key 不匹配,也能恢复最近的缓存。
Q: 缓存 key 中是否应该包含 github.sha 或时间戳?
A: 不应该。包含 github.sha 或时间戳会导致每次提交都生成不同的 key,从而完全破坏缓存机制。正确的做法是使用稳定的标识符,如 runner.os 和 lock 文件的 hash(如 hashFiles('**/package-lock.json')),这样只有依赖变化时 key 才会改变。
相关深度解决方案
在配置当前服务时,如果您需要实现更复杂的架构或多源数据整合,建议配合参考我们整理的 Next.js 14 构建失败:Webpack 内存溢出与配置错误的实战修复。
在配置当前服务时,如果您需要实现更复杂的架构或多源数据整合,建议配合参考我们整理的 npm ERR! code EINTEGRITY 修复:缓存损坏与哈希校验失败排查指南。