登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  软件教程

GitHub Actions 缓存键怎样按依赖锁文件精确失效缓存为什么结果为空或不符合预期

来源:17golang原创

时间:2026-11-10 01:37:25 494浏览 收藏

GitHub Actions 里的依赖缓存,最容易出现的错觉是“我改了 package-lock.json,下一次运行却还在用旧内容”。真正决定缓存是否失效的不是缓存目录本身,而是 key 的最终值:把锁文件交给 hashFiles() 计算后,依赖文件变化就会得到一个新键;如果只写固定字符串,缓存当然不会随着依赖变化。

官方地址:https://github.com/

下面以 npm 的 package-lock.json 为例,把配置、可见结果和排查路径串起来。pnpm、Yarn、Go 依赖只需要替换锁文件路径,判断逻辑不变。

先记住:失效缓存等于换一个 key

GitHub Actions 的缓存动作会先查完整 key,没有精确命中时才尝试键前缀和 restore-keys。完全未命中时,如果作业成功,动作会用当前 key 保存新的缓存。已有缓存不能直接改内容,所以“让旧缓存失效”的实际做法是让依赖锁文件的哈希变化,进而生成一个新 key。

因此,一个可维护的缓存键至少包含三类信息:

  • 运行环境,例如 runner.os,避免 Linux 和 Windows 共用不兼容的缓存;
  • 缓存对象名称,例如 npm 或 go-build,方便从日志判断用途;
  • 锁文件哈希,例如 hashFiles('**/package-lock.json'),让依赖变化触发新键。

第 1 步:在工作流文件中找到缓存入口

打开仓库的 .github/workflows/,选择实际执行安装依赖的工作流。进入 GitHub 仓库页面时,路径是“Actions → 目标工作流 → Run workflow”或对应运行记录;编辑文件时则是“Code → .github → workflows → 目标 YAML 文件”。先确认缓存步骤位于安装依赖之前,否则缓存恢复就失去了意义。

在编辑器中搜索 actions/cache、setup-node 或 cache:。使用 setup-node 的内置缓存时,仍要检查它实际使用的锁文件;需要自定义缓存目录或回退策略时,再使用显式的 actions/cache 步骤。

此时的可见确认是:缓存步骤在 npm ci、npm install 或构建步骤之前,并且 path 指向包管理器真正使用的目录,例如 Linux runner 上的 ~/.npm。

第 2 步:用锁文件哈希组成精确缓存键

在“编辑工作流文件 → 缓存步骤 → with → key”位置,把固定字符串改成下面这种结构。每一行注释都直接写在 YAML 代码中,方便以后维护时知道它为何存在。

- name: Cache npm downloads
  id: cache-npm
  uses: actions/cache@v4
  with:
    # 只缓存 npm 下载内容,不把 node_modules 当成跨环境制品
    path: ~/.npm
    # 系统、缓存用途和锁文件哈希共同决定精确键
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      # 先尝试同一系统下的其他 npm 缓存
      ${{ runner.os }}-npm-
      # 最后才回退到系统级前缀,避免误用其他平台内容
      ${{ runner.os }}-

hashFiles() 的路径是相对工作区的 glob。单仓库项目可以写 package-lock.json;monorepo 或锁文件位于子目录时,用 **/package-lock.json。pnpm 和 Yarn 分别替换为 **/pnpm-lock.yaml、**/yarn.lock;Go 项目则可以使用 **/go.sum。

在“保存并提交 → Actions → 目标运行”后,展开缓存步骤的日志,能看到一条由系统名、缓存用途和哈希组成的 resolved key。图 1 是这一路径的原创界面说明图,用来帮助定位字段,不是实际 GitHub 截图。

原创 GitHub Actions 缓存键配置界面说明图,显示 runner 系统、锁文件哈希和回退键的关系
图1:缓存键配置的原创界面说明图,展示锁文件哈希如何参与键生成;不是实际 GitHub 截图。

如果提交只改了业务源码,锁文件哈希不变,命中原缓存是正常的;如果新增或升级依赖,锁文件哈希改变,就应该看到新的精确键。不要为了“强制刷新”给键拼接每次都变化的时间戳,那会让每次运行都无法复用缓存。

第 3 步:用 cache-hit 判断到底命中了哪一种缓存

很多人只看“缓存步骤成功”,但成功不代表精确命中。给缓存步骤加上 id 后,在下一步读取 steps.cache-npm.outputs.cache-hit,就可以把精确命中和回退命中区分开。

- name: Show cache state
  if: always()
  run: |
    # cache-hit 只有精确 key 命中时才是 true
    echo "cache-hit=${{ steps.cache-npm.outputs.cache-hit }}"
    # 用短消息帮助运行记录快速区分命中、回退和完全未命中
    if [ "${{ steps.cache-npm.outputs.cache-hit }}" = "true" ]; then
      echo "Exact cache hit: reuse the lockfile-specific cache"
    else
      echo "No exact hit: restore-keys may have restored a partial cache"
    fi

“Actions → 目标运行 → 展开 Show cache state”就是结果核对入口。判断可以按下面三种情况进行:

日志现象实际含义下一步
cache-hit=true完整 key 精确匹配确认锁文件没有变化,继续复用
cache-hit=false,但日志显示恢复了前缀缓存没有精确命中,使用了较旧的可兼容前缀让安装步骤补齐新依赖,观察作业是否成功保存新 key
cache-hit=false,且没有可恢复条目完全 miss检查锁文件路径、分支范围和保存条件

图 2 把这三个可见状态放进一张原创结果说明图。它只用于说明面板应该看什么,不代表某个仓库真实运行过这些结果。

原创 GitHub Actions 缓存结果界面说明图,显示精确命中、回退命中和新缓存键状态
图2:缓存结果核对的原创界面说明图,展示 cache-hit 与新缓存键状态;不是运行截图。

第 4 步:结果为空或不生效时按四个位置排查

1. hashFiles 路径没有匹配到锁文件

如果 glob 写错,哈希表达式就不能代表真正的依赖文件。检查“Code → 目标锁文件”,确认它在工作区内、文件名大小写一致,并且 checkout 步骤在缓存步骤之前。monorepo 不要把根目录路径想当然地写死,优先使用与仓库结构匹配的 glob。

2. 把 restore-keys 误当成精确命中

restore-keys 是降级策略,不是新的精确 key。它可能恢复最近的旧前缀缓存,所以 cache-hit 仍然是 false。依赖变化后看到旧缓存被恢复并不矛盾,后续安装成功时,当前带新哈希的 key 才会保存成新缓存。

3. 分支或触发事件没有缓存访问范围

缓存有分支范围。当前分支找不到时,动作会按规则尝试默认分支,但不能把其他无关分支的缓存当成全局资源。对于低信任触发器,缓存可能只有读取权限,保存动作会被跳过;这时日志会给出提示,不能只根据“步骤没有报红”判断缓存已经写入。

4. 把敏感文件放进缓存路径

缓存内容可能被有读取权限的人取出,不要把 Token、登录凭据或含密钥的配置放进 path。依赖下载目录适合缓存,包含环境密钥的构建产物不适合缓存。

一份可以直接改造的完整模板

如果你只需要 npm 下载缓存,可以从下面模板开始,再按项目目录改写锁文件 glob。关键是保持“checkout → cache → install → build/test”的顺序。

name: Node CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Cache npm downloads
        id: cache-npm
        uses: actions/cache@v4
        with:
          # npm ci 会从这个目录读取下载缓存
          path: ~/.npm
          # 锁文件变化时产生新键,系统变化时也不会串缓存
          key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            # 仅允许同一系统下的旧 npm 缓存作为回退
            ${{ runner.os }}-npm-

      - name: Install dependencies
        run: npm ci

      - name: Test
        run: npm test

提交后先看缓存步骤的 resolved key,再看安装和测试是否成功,最后查看当前运行是否出现保存新缓存的日志。这个顺序能把“键算错”“回退成功但不精确”“作业失败没有保存”三个问题分开。

最后的核对清单

  • key 是否包含正确的 runner.os 和锁文件 hashFiles()?
  • path 是否是包管理器实际使用的缓存目录?
  • restore-keys 是否从具体到宽泛排列,并且没有跨系统回退?
  • 是否通过 cache-hit 区分了精确命中和前缀回退?
  • 依赖变化后是否看到新 resolved key,而不是用时间戳强制制造每次新键?
  • 是否避免把 Token、凭据和其他敏感文件放入缓存路径?

把锁文件哈希放进精确键、把回退键限制在兼容范围内,再用 cache-hit 和运行日志核对结果,基本就能解释“为什么缓存为空”以及“为什么改了依赖却没有失效”。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>