GitHub Actions锁文件:锁住了什么
工作流文件没变,下一次CI却可能运行不同的依赖代码:因为标签和分支会移动。GitHub Actions新增的工作流依赖锁文件,把“写在YAML里的引用”和“本次实际执行的提交”分开记录。它解决的是依赖选择漂移,不是替你判断依赖代码是否可信。
文章目录
如果团队依靠可读的版本标签维护CI,这项更新值得了解。关键不只是生成一个文件,而是弄清它锁住哪些依赖、更新时审查什么,以及哪些构建输入仍在锁文件之外。Read in English。
公开预览带来了什么
GitHub在2026年10月14日公告中宣布工作流依赖锁文件进入公开预览。更正后的适用范围包括Enterprise Cloud、带数据驻留的Enterprise Cloud、Team和Free。公告最初摘要与更正后的范围不同,应以更正内容为准。
官方工具生成.github/workflows/actions.lock,记录工作流依赖的具体提交。已接入的工作流按锁定提交运行,而不是因为标签后来移动就自动跟着换代码。这让审查对象与执行对象之间多了一项可复查的约束。
这里的工程收益是缩小不确定性。若一次CI行为变化,维护者不必先猜某个标签是不是悄悄指向了新版本;可以从记录的提交与依赖变更入手。但“锁定”不意味着被锁住的版本没有缺陷,也不意味着它永远适合继续使用。
同一个标签,为什么不是同一份代码
把一个标签想成一个名字,而不是代码内容本身。教学例子中,审查时demo/lint@v2指向提交A,后来上游把同一个标签指向B。下游仓库没有提交新YAML,只要运行时重新解析这个名字,就可能选到B。这个例子描述引用漂移,不声称某个真实项目发生了这样的事件。
审查时:标签 → A
上游后来:标签 → B
未锁定运行:B
锁定后选择:A
接入锁定后,选择路径发生了变化:工作流保留便于维护的引用,执行依据是已经记录的提交。名字仍然可读,代码身份则要能追到明确的审查结果。团队可以把两类变化放进更新评审:引用为什么要变,以及实际提交从哪里变到哪里。
因此,标签看起来还是v2,不应成为跳过差异审查的理由;反过来,提交确实没变,也不能单独证明运行环境、输入数据和外部服务都没变。先分清这些维度,排查才不会把所有变化都归因于锁文件。
真正需要看的是依赖图,而不只第一层
一个动作还可能调用其他动作。若只盯着工作流中的顶层引用,下层变化仍然容易被忽略。官方概念文档说明,锁文件覆盖动作的传递依赖;可复用工作流则有仓库边界,其内部依赖由定义该工作流的仓库锁文件负责。
本仓库工作流
顶层动作
动作调用的子依赖
可复用工作流:看其定义仓库
这意味着“我的仓库有锁文件”还不够回答全部问题。维护者还要知道哪些工作流已经接入,哪些引用属于当前仓库的覆盖范围,哪些交给了另一处维护。一个很长的文件,也可能漏掉真正运行的那条路径。
适合审查的记录应从入口向下展开:入口工作流是谁,调用了哪些动作,是否跨到可复用工作流,那处由谁更新与审查。不要把文档中的传递覆盖,误读成所有仓库边界和任意运行时下载都被统一锁住。
用一个本地模型观察漂移与漏项
下面的Python脚本只使用虚构依赖图与修订标识,演示两个条件:上游引用改变时,锁定选择保持不变;必需依赖没有锁条目时,拒绝给出完整选择。它不是GitHub工具,也不是actions.lock格式,不访问网络或修改仓库。
# Fictional identifiers and graph: not GitHub's lockfile format.
ROOT = "demo/build@v1"
CHILD = "demo/lint@v2"
graph = {ROOT: [CHILD], CHILD: []}
locked = {ROOT: "reviewed-A", CHILD: "reviewed-B"}
upstream = {ROOT: "reviewed-A", CHILD: "reviewed-B"}
def dependencies(graph, roots):
seen, active = set(), set()
def visit(ref):
if ref in active:
raise ValueError("cycle requires review")
if ref in seen:
return
if ref not in graph:
raise ValueError("unknown dependency")
active.add(ref)
for child in graph[ref]:
visit(child)
active.remove(ref)
seen.add(ref)
for ref in roots:
visit(ref)
return seen
def selected_commits(graph, roots, locked):
needed = dependencies(graph, roots)
missing = needed - locked.keys()
if missing:
raise ValueError("missing lock entries: " + str(sorted(missing)))
return {ref: locked[ref] for ref in sorted(needed)}
before = selected_commits(graph, [ROOT], locked)
upstream[CHILD] = "later-C"
assert upstream[CHILD] != locked[CHILD]
assert selected_commits(graph, [ROOT], locked) == before
for bad_graph, bad_lock in [
(graph, {ROOT: "reviewed-A"}),
({ROOT: [CHILD], CHILD: [ROOT]}, locked),
({ROOT: ["unknown/ref@v1"]}, locked),
]:
try:
selected_commits(bad_graph, [ROOT], bad_lock)
except ValueError:
pass
else:
raise AssertionError("incomplete graph was accepted")
assert selected_commits({}, [], {}) == {}
print(before)
脚本先得到reviewed-A、reviewed-B,随后让虚构上游的子依赖变为later-C,锁定结果仍保持原值。它还覆盖缺少子依赖条目、循环、未知节点和空入口。代码已本地执行;这不代表真实GitHub接入或安全校验已经完成。
循环报错是这个教学检查器的保守设计,不是宣称GitHub采用相同算法。更重要的是它展示了“有顶层条目”和“覆盖完整依赖集合”的区别。真实锁文件还承担代码身份等验证,不能用这个集合模型替代。
“条目齐全”与“条目可信”要分开验收
本地模型里,只要每个依赖有对应修订,就能返回结果。可是假设有人把reviewed-B换成一个从未审查的标识,覆盖检查照样通过。它没有访问来源仓库,也没有证明这个标识对应什么代码。因此,覆盖完整只是必要条件,不能推出条目可信。
官方项目说明把这两类检查分开:--verify-local离线检查条目覆盖,不证明条目可信;--verify用于CI中的进一步验证。实际采用时应以所用版本说明为准。本文脚本只演示前一种思路的一部分,不能贴上“供应链已验证”的标签。
评审可以分别留下两项结论:依赖图是否有漏项,记录的来源与修订是否有依据。如果只写一个“验证通过”,接手的人就不知道它通过的是哪一层。工具报告、审查记录与平台运行结果,应该保留各自的含义。
接入与更新,应该是两类审查
官方gh-actions-lock项目提供生成和验证功能;公开预览仍在演进。实际操作前应核对所用版本的说明,使用工具生成文件,不手改一个看似合理的格式。本篇没有安装该扩展或替任何仓库启用策略。
首次接入时,最重要的不是“生成命令成功”,而是审查生成后的差异。确认选中的工作流就是预期范围,依赖来源与提交可解释,新增的传递依赖有人负责。随后运行正常CI,检查实际使用的路径与原有任务是否一致。
后续更新则要回答另一个问题:为什么现在需要从旧提交迁移到新提交?可能是功能修复,也可能是常规维护。审查时同时看引用变更与锁定变更,不要把重新生成的一大段内容当成无需阅读的机器产物。自动更新节省的是整理时间,不会替维护者做出接受风险的决定。
检查通过,也不等于整个构建可复现
锁住动作版本,不能自动锁住动作内部从网络下载的任意工具。构建使用的包索引、基础镜像、运行器环境、输入参数和外部服务,也可能影响结果。这里是构建边界的工程分析,不是说锁文件没有用,而是不要把它解决的一类问题扩大成全部保证。
可以把排查分成两层:先核对工作流依赖是否按记录选择;再核对构建过程额外读取了什么。若第二层仍使用可变地址或未固定的包版本,就把那部分单独列出来,按相应工具的锁定和校验方式处理。
同样,固定提交与权限最小化是两件事。可信版本也不应获取不必要的写权限或凭据;需要理解权限边界,可继续看本站的GitHub App最小权限说明。该文章讨论的是App授权,不能直接替代Actions令牌的具体配置文档。
把维护节奏留在设计里
锁文件的价值会随着更新流程而变化。只锁不更新,代码身份稳定了,却可能长期保留已知问题;自动改完就合并,又会削弱代码审查。更合适的做法是为依赖安排责任人,区分紧急修复和常规升级,并保留可追踪的审查依据。
推广时可先选职责明确、依赖可解释的一条工作流,完成接入、正常运行、更新和失败处理,再逐步扩展。暂时无法覆盖的路径应明确标记,而不是把“有锁文件”当成全仓库已经完成治理。
一次有用的交付应能回答:这次运行选了什么提交,谁接受了这项变化,覆盖范围在哪里结束,下一次更新如何提出。锁文件让这些问题更容易有据可查,真正的收益来自审查与执行对象保持一致,而不是多出一个文件。
来源与验证范围
GitHub官方公告,2026年10月14日,含套餐范围更正;工作流依赖锁文件概念文档;官方gh-actions-lock项目。Google News聚合条目发表于UTC 10月14日13:49:15,早于本文计划时段。文中依赖名、修订标识与图形为原创教学材料,没有运行真实工作流或测量构建性能。
支持
如果文中的解释或示例帮你解决了问题、节省了时间,欢迎自愿打赏支持本站。
二维码可点击放大。更多支持方式见支持页面。


