在使用 GitHub Actions 的 `actions/cache` 时,构建健壮的依赖缓存策略需要理解其缓存查找的顺序和不同参数的作用。缓存查找的流程是:首先尝试精确匹配 `key`;如果失败,则尝试 `key` 的部分匹配;如果仍未命中,则按顺序依次检查 `restore-keys` 中的每个键进行部分匹配。只有在所有查找都失败后,才算作缓存未命中(cache miss)。

**关于 `restore-keys` 的层级结构设计:**

为了在依赖文件变更时既能避免频繁重建缓存,又能防止因分支合并导致缓存污染,应遵循从**最具体到最通用**的层级结构设计 `restore-keys`。

1. **最具体层级(精确匹配依赖):** 应该包含基于关键依赖文件哈希值的键,例如 `npm-feature-${{ hashFiles('package-lock.json') }}`。这确保了只有当核心依赖文件发生变化时,缓存才会失效,从而最大化命中率。
2. **中间层级(通用前缀):** 接着设置一个不包含具体哈希值的、但仍能代表特定功能或环境的通用前缀,例如 `npm-feature-`。当哈希值改变时,如果找不到精确匹配,系统会尝试匹配所有以 `npm-feature-` 开头的缓存,并选择其中最新的一个。
3. **最通用层级(环境/基础):** 最后设置最基础的、不依赖于任何特定文件哈希值的键,例如 `npm-` 或仅包含操作系统信息(如 `${{ runner.os }}-build-`)。这作为最后的兜底,确保在所有更具体的缓存都失效时,至少能恢复一个基于环境的通用缓存。

这种层级结构保证了:如果依赖文件变化(哈希改变),则会优先尝试更具体的键;如果仅是环境变化(如操作系统),则会退回到更通用的键进行恢复。

**关于低信任触发器和缓存投毒风险的控制:**

当工作流由如 `pull_request_target` 这样的低信任触发器(low-trust trigger)触发时,默认情况下,工作流对默认分支(如 `main`)的缓存写入权限是**只读**的,以防止“缓存投毒”(cache poisoning)攻击。这意味着,如果工作流尝试写入缓存,操作会失败,但工作流本身不会中断,只会记录一个警告。

* **避免缓存污染:** 如果您在低信任触发器下需要写入缓存(即需要写入权限),您必须显式地使用 `cache-mode: write` 或 `cache-mode: write-only` 来覆盖默认的只读限制。**请注意,这样做会重新引入缓存投毒的风险。**
* **推荐做法(安全优先):** 为了安全,如果工作流是低信任触发器(如 `pull_request_target`),建议:
1. **只恢复缓存:** 仅使用 `actions/cache/restore` 模式,或在 `actions/cache` 中不设置写入权限,以确保只能读取现有缓存。
2. **由受信任流程维护:** 确保一个由**受信任触发器**(如 `push` 到默认分支)触发的工作流负责创建和更新缓存。低信任触发器的工作流只需读取这些由受信任流程维护的缓存即可。
* **使用 `cache-mode`:** 您可以通过设置 `cache-mode` 来精确控制权限。例如,如果仅需要读取缓存,应确保 `cache-mode` 设置为 `read` 或不设置(如果默认行为符合要求)。如果需要写入,则必须明确设置 `cache-mode: write`,但需充分理解其安全风险。

总结来说,设计缓存策略时,应利用 `hashFiles` 等表达式生成具有高区分度的 `key`,并通过层级化的 `restore-keys` 确保回退机制的健壮性;同时,在处理低信任触发器时,必须警惕缓存写入权限,并优先采用只读模式或由受信任流程维护缓存的做法。

信息来源