第33章|GitHub Actions 缓存与制品:actions/cache 与 artifact

CI 的速度像一阵风:你以为是 YAML 的语法在“跑”,其实是两种东西在拉扯—— 缓存(把依赖/中间产物“留住”)和制品(把构建成果“寄走”)。 本章用两把钥匙带你开锁:actions/cache 的命中与失效, artifact 的上传与下载,以及两者之间最容易混淆的边界。

Cache

减少重复劳动

  • cache key 与 invalidation
  • restore-keys 做“部分命中”
  • cache 并非强保证:会被淘汰
Artifact

阶段间传递结果

  • upload / download 让 job 解耦
  • 保留日志、报告、包文件
  • retention 控制生命周期
Strategy

命中率与可复现

  • 先快后稳:restore 立刻生效
  • 依赖变更必触发失效
  • 敏感信息谨慎上传

1. 先把比喻说清:缓存像冰箱,制品像快递

Cache:面向“重复构建”。同一套依赖(例如 lockfile 没变)时,你希望下次 workflow 直接拿到上次的热身成果,让构建少跑几段路。 但缓存不是数据库:它可能缺失、可能被淘汰、命中也不绝对。

Artifact:面向“阶段交接”。同一次 workflow run 内,你需要把 build/test/package 的结果给后面的 job(甚至人工排障时也要能追溯)。 制品不会跨 run 保证可用;它的价值在于这一次 run 的证据链完整。

判题标准:如果你要的是“加速依赖准备”,选缓存;如果你要的是“把构建结果带到下一阶段/下一台机器”,选 artifact。
Cache Lifecycle:Key → Restore → Build → Save 命中率来自你写的 key;失效控制来自 key 的组成方式 Compute cache key OS · language · lockfile hash Restore cache hit? fast path : miss? build full Build / install only missing parts get rebuilt restore-keys 允许部分命中(例如只差小版本) 但要确保产物与目录结构兼容,否则会“脏命中”
图 1:缓存不是魔法,它依赖 cache key 的“正确性”。key 设计越好,恢复越快且越不容易缓存污染。

2. actions/cache 深入:paths、key、restore-keys 的三角关系

2.1 paths:你要缓存什么目录

path 指定要打包/还原的目录。建议缓存“可重建但贵”的东西:依赖下载缓存、编译中间文件、构建工具本地仓库等。 如果你缓存的是“和构建强绑定”的产物(例如把整套 workspace 直接缓存),就更容易出现 ABI/版本漂移。

2.2 key:命中/失效的裁判

key 最重要。它决定这次能不能拿到上次“同版本依赖”的缓存。 常见做法是把 lockfile hash 放进 key:只要依赖变化,缓存必定失效并重新生成。

2.3 restore-keys:部分命中,换取更高速度

restore-keys 允许你在精确 key 没命中时,尝试更宽松的前缀。 这能把“完全 miss”的冷启动变成“半热身”。但前提是:你的目录内容对“轻微版本差异”仍然安全。

Cache Key Anatomy:组成越“可解释”,越不容易误伤 OS runner.os / arch Toolchain node / python / java Lockfile hash package-lock / requirements Inval. policy restore-keys compatibility avoid “dirty hits” lockfile 变更 → key 变更 → 强制失效
图 2:缓存 key 的“组成模块”越清晰,你越能控制失效边界:该重建就重建,不该重建就继续复用。

3. 常见生态的缓存模板(用关键目录而非整坨 workspace)

下面的片段是“可用模板”。你要做的不是照抄,而是把 pathkey 对齐到你项目的依赖模型。

3.1 Node.js(npm / pnpm / yarn)

推荐缓存包下载与构建缓存,而不是把整个 `node_modules/` 作为目录直塞进去。

- name: Cache npm
  uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${ runner.os }}-node-${ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${ runner.os }}-node-

3.2 Python(pip)

缓存 pip 的 wheel/download 目录,通常能显著降低依赖安装时间。

- name: Cache pip
  uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: ${ runner.os }}-pip-${ hashFiles('**/requirements*.txt') }}
    restore-keys: |
      ${ runner.os }}-pip-

3.3 Java(Maven / Gradle)

缓存本地仓库目录(如 Maven `~/.m2` 或 Gradle `~/.gradle`),让依赖解析更快。

4. Artifact:把构建证据“随包寄出”,让后续 job 可追溯

你应该把 artifact 看作“证据与成果的快递盒”。比如:测试报告(JUnit XML)、构建产物(zip / docker manifest 摘要 / wheel)、安全扫描结果等。 这样后续 job 不必重复构建同一个内容,或者至少能对齐“同一个版本的证据链”。

Artifact Conveyor:Build → Upload → Download → Ship 同一次 workflow run 内跨 job 传递;证据链可追溯 Job A: Build compile / test outputs ready Upload Artifact upload-artifact@v4 retention control box Job B: Deploy download-artifact@v4 deploy exact artifact Upload 一次,后续 job 读一次;避免“重新打包又不一致”
图 3:artifact 让 job 之间“只交换成果,不交换过程”。当后续部署要严格对应构建证据,这是首选。

4.1 上传:actions/upload-artifact

- name: Upload test report
  uses: actions/upload-artifact@v4
  with:
    name: test-report
    path: test-results/
    retention-days: 14
    if-no-files-found: warn

4.2 下载:actions/download-artifact

- name: Download test report
  uses: actions/download-artifact@v4
  with:
    name: test-report
    path: ./test-results/

5. 缓存 vs artifact:别让“加速工具”变成“发布数据源”

关键差异:缓存的目标是“下一次更快”;artifact 的目标是“这一次可追溯”。把两者边界搞反,常见后果是:偶发错误、版本不一致、以及“我明明跑过为啥没拿到”的困惑。
One sentence rule: Cache speeds up, Artifact proves actions/cache Restore for next runs Key-based (might miss / might evict) Best for dependencies, build caches Goal: time reduction Artifact Upload once, download in run Run-scoped evidence Best for reports, packages, logs Goal: traceability
图 4:把“加速”与“证明”分开。Cache 是速度的优化器,Artifact 是证据的传送带。

6. 最小清单(把命中率和一致性一起做对)

  1. 先决定缓存目标:依赖/下载缓存/构建缓存,而不是整坨 workspace。
  2. 为缓存写清晰 key:把 lockfile hash 纳入 invalidation。
  3. restore-keys 只用于你确定“兼容”的前缀范围,避免脏命中。
  4. 跨 job 传递构建产物用 artifact,而不是指望缓存能“保活”。
  5. artifact 里不要塞 secrets;必要时用最小化字段或只上传报告。
← 上一章:GitHub Actions 触发与权限 下一章:矩阵与并行(matrix / parallel)→