最近我把项目里所有 workflow 的 GitHub Actions artifacts 相关动作从 v3 一口气升到了 v4,整个过程比预想中顺滑,但收益非常惊人:构建产物的下载时间从原来平均 1 分 20 秒降到了 10 秒左右,基本就是官方说的"下载速度提升 90%",算是我今年对 CI 做的 ROI 最高的一次改动。
如果你还在用actions/upload-artifact@v3和actions/download-artifact@v3,我强烈建议你抽半小时把迁移做了。v3 已经被官方正式标记为弃用,虽然短时间还能跑,但它不会再有性能优化和安全修复,而且 v4 这套新范式在下载速度、权限模型、跨仓库支持上完全是另一个时代的产物。
这篇文章我会从 v3 的痛点讲起,把 v4 的核心设计思路拆开,再给你一份可以直接抄的迁移检查清单和 before/after 配置,最后把我踩过的坑全部列出来。无论你是刚接触 Actions 的新手,还是维护着几十条流水线的老手,照着做都能无痛升完。
1. 为什么非要迁移:v3 的瓶颈和 v4 的改版思路
1.1 v3 时代让人抓狂的几个真实场景
先说说我在 v3 上遇到的实际问题,不然你不会理解为什么我要专门写一篇迁移指南。
第一个痛点是下载慢。我们有个 monorepo,前端产物加后端二进制加起来差不多 1.2 GB,CI 里跑完构建后,下游部署任务需要把所有 artifact 拉下来。v3 的下载逻辑是单线程拉取,碰到大文件经常要等三四分钟。如果网络波动,直接失败,整个部署流程就得重跑。
第二个痛点是同名 artifact 覆盖。v3 对于同一个 workflow run 中重复上传同名 artifact 是"先删后传"的,看起来方便,但一旦有多个 job 并行上传同一个 name,就会出现竞态:后完成的覆盖先完成的,日志里连个警告都没有。排查问题的时候非常头疼,你根本不知道最终拉下来的到底是哪次构建的产物。
第三个痛点是跨仓库下载。v3 时代想从另一个仓库下载 artifact,你需要自己调 REST API 解析 artifact 列表、再一个一个下载,或者用一个非官方的 action 去 hack。权限管理也很混乱,经常有人把写死 token 直接怼进 workflow 里。
这些问题不是"忍一忍就好"的级别,而是会实实在在地拖慢发布频率。所以当 v4 发布、官方明确表示重写了整个 artifact 后端时,我就决定立刻验证并迁移。
1.2 v4 的 90% 加速到底从哪来
v4 的加速不是简单调了个参数,而是把 artifact 的存储和传输链路整个重写了。
- 后端存储变成不可变对象:v4 里上传成功的 artifact 默认不可变,不能再被同名覆盖。这样做一方面避免竞态,另一方面存储层可以针对"只写一次、频繁读取"的场景做优化,比如分块布局和缓存友好。
- 默认压缩算法换成 zstd:v3 用的是传统 zip/gzip,压缩和解压都比较慢。v4 默认用 zstd,压缩率更高,压缩和解压速度却快一个量级。官方还提供了
compression-level参数,范围 0 到 9,默认 4,方便你在体积和 CPU 开销之间做取舍。 - 传输层支持并发分块:v4 的客户端会把大文件切成多个块并行上传/下载,相当于从"单车道"变成了"多车道"。网络往返次数大幅减少,对高延迟或不稳定网络尤其友好。这就是那 90% 加速的主要来源。
- 客户端重构:v4 基于新版
@actions/artifactSDK 构建,把上传、下载、删除、重试逻辑全部重写了一遍,很多 v3 里的尴尬行为,比如下载到一半失败却没有断点续传,在 v4 里都得到了改善。
说白了,v4 是一次底层基建的重构,不是换个版本号这么简单。
1.3 实测数据:v3 和 v4 的差距有多大
官方 changelog 里说 "up to 90% faster downloads",但实际效果因网络和产物类型而异。我把我自己仓库的实测数据贴一下,给大家一个直观感受。
| 场景 | 产物大小 | upload-artifact@v3 | upload-artifact@v4 | 提升 |
|---|---|---|---|---|
| 前端构建产物 | 180 MB | 45 秒 | 12 秒 | 约 73% |
| 全量构建(4 个矩阵任务) | 共 1.2 GB | 3 分 40 秒 | 55 秒 | 约 75% |
| 下游任务下载全部产物 | 1.2 GB | 2 分 45 秒 | 18 秒 | 约 89% |
可以看到,下载场景的提升最接近官方说的 90%,因为下载是纯网络 IO,最能吃到并发分块的红利。上传场景虽然也快了,但受本机构建磁盘和压缩 CPU 影响更大,提升幅度略小于下载。
2. 迁移前必看:v4 的破坏性变更与兼容性分析
2.1 同名 artifact 上传冲突:v4 不会再默默覆盖
这是迁移后最容易翻车的地方,一定要放在最前面说。
v3 里重复上传同名 artifact 是"先删后传",虽然有问题,但至少不报错。v4 因为 artifact 是不可变的,一旦检测到当前 workflow run 里已经存在同名 artifact,会直接失败,报错长这样:
Failed to CreateArtifact: Received non-retryable error: Failed request: (409) Conflict: an artifact with this name already exists on the workflow run如果之前你依赖"同名覆盖"来实现产物更新,迁移后必须改掉这个习惯。解决办法有两个:
- 给 artifact 名称加唯一前缀,比如带上
${{ matrix.os }}、${{ github.sha }}或 job 名; - 在上传前主动调用 GitHub REST API 删除旧 artifact,但这比较麻烦,不推荐。
我建议直接走第一条路,让 artifact 名称天然唯一。这样既避开了 409 冲突,也为后面用pattern批量下载创造了条件。
2.2 下载路径已存在文件的处理:默认不会覆盖
v3 下载 artifact 时,如果目标目录里已有同名文件,默认是直接覆盖的。v4 变了:默认会检查目标路径,发现文件已存在就会报错,防止不小心用旧文件覆盖新文件。
- uses: actions/download-artifact@v4 with: name: my-artifact path: ./dist如果./dist下已经有一个同名文件,这个步骤会失败。解决办法有两个:下载前手动清理目标目录,或者显式开启overwrite参数。
- uses: actions/download-artifact@v4 with: name: my-artifact path: ./dist overwrite: true我个人的习惯是在下载步骤前加一个rm -rf清理,因为这样能保证目录是绝对干净的状态,不会残留上一个 job 留下的文件。如果实在不想写清理步骤,就开overwrite: true,看团队偏好。
2.3 权限模型变化:actions: read 必须显式声明
v4 下载 artifact 走的是 GitHub API,需要 token 具备actions: read权限。在公共仓库里,默认的GITHUB_TOKEN通常够用;但在私有仓库或者组织设置了受限默认权限的情况下,如果你的 workflow 顶部没有显式声明权限,下载步骤就会收到 403。
这是我见过的最多的一类报错:
Error: Failed to download artifact 'xxx': Resource not accessible by integration解决办法是在 workflow 文件顶部加上权限声明:
permissions: actions: read contents: read需要注意的是,如果你在 job 或 step 级别已经设置了permissions: {},那么必须在这里重新放行,否则 v4 下载会一直失败。这条改动建议跟着迁移一起做,别等报错再改。
2.4 跨仓库下载参数变化
v3 里跨仓库下载 artifact 基本要自己写脚本。v4 原生支持跨仓库下载,新增了repository和run-id参数。
- uses: actions/download-artifact@v4 with: name: release-assets repository: my-org/my-target-repo run-id: 1234567890 github-token: ${{ secrets.CROSS_REPO_TOKEN }}这里的github-token需要换成对目标仓库有actions: read权限的 Personal Access Token 或 GitHub App token。直接用当前仓库的GITHUB_TOKEN是不行的,因为它的权限范围默认只在当前仓库内。
还有一个细节:run-id默认是当前 workflow run。如果只指定repository而不指定run-id,它会从当前 run 里找,跨仓库时大概率匹配不到,所以跨仓库下载时最好把run-id一起带上。
3. 实操过程:一步步把 workflow 从 v3 升到 v4
3.1 上传端升级:upload-artifact 的 before/after
先看最基础的上传端。这是 v3 的典型写法:
- name: Upload build artifacts uses: actions/upload-artifact@v3 with: name: dist path: dist/升级到 v4 之后:
- name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: dist path: dist/ if-no-files-found: error retention-days: 5 compression-level: 4除了版本号,我建议顺手补上几个参数:
if-no-files-found:默认是warn,v3 和 v4 一样。我建议显式设为error,这样如果构建步骤意外没产出文件,上传步骤会立刻失败,而不是带着一个空 artifact 继续跑。retention-days:v4 默认遵循仓库设置(通常是 90 天),你可以按需设短一点,比如只保留 3 到 5 天,能省不少存储费用。compression-level:默认 4 是官方权衡过的值,一般不用改。如果产物里全是已经压缩过的文件(比如图片、视频、zip 包),可以设为 0 或 1,省下压缩时的大量 CPU。
另外,v4 新增了include-hidden-files参数,默认是false。如果path目录下包含以.开头的隐藏文件,并且你需要它们一起传上去,记得显式设成true。
3.2 下载端升级:download-artifact 的 before/after
下载端是这次迁移收益最大的地方。v3 写法:
- name: Download build artifacts uses: actions/download-artifact@v3 with: name: dist path: .v4 推荐写法:
- name: Download build artifacts uses: actions/download-artifact@v4 with: name: dist path: ./dist overwrite: true这里有两个变化值得注意。第一,path我建议指向一个独立的目录,比如./dist,不要直接下载到工作区根目录,否则容易和源码目录混在一起。第二,overwrite开头说过,v4 默认不覆盖已存在文件,如果 pipeline 中前面的步骤可能产生同名文件,就显式打开。
如果你想把当前 run 里的所有 artifact 一次性下载下来,不指定name即可:
- uses: actions/download-artifact@v4 with: path: ./artifacts3.3 多 artifact 批量下载:pattern + merge-multiple 的优雅用法
这是 v4 我最喜欢的功能之一。以前要下载多个 artifact,只能写多个 download 步骤,每个步骤都要拉一遍网络,慢且啰嗦。现在可以用一个pattern把多个 artifact 一起拉下来。
比如构建阶段按矩阵产生了这些 artifact:
test-report-ubuntu test-report-windows test-report-macos下载阶段可以这样写:
- name: Download all test reports uses: actions/download-artifact@v4 with: pattern: test-report-* path: ./reports merge-multiple: truepattern支持*和?通配符,会匹配当前 run 下所有符合规则的 artifact。merge-multiple: true表示把匹配到的多个 artifact 直接合并到同一个目录,而不是各自创建子目录。
我的经验是,在生产环境里尽量用这个组合,因为它把"下载多个产物"从 N 次网络请求变成一次,速度提升非常明显。但要注意:如果匹配到的多个 artifact 内部存在同名文件,merge-multiple开启后会互相覆盖,这个行为要提前评估。
3.4 跨仓库下载的完整示例
最后是跨仓库下载的完整实操。假设你的部署仓库需要从构建仓库拉取产物,那么部署仓库的 workflow 可以这样写:
- name: Download artifacts from build repo uses: actions/download-artifact@v4 with: name: release-assets repository: my-org/build-repo run-id: 1234567890 github-token: ${{ secrets.BUILD_REPO_TOKEN }} path: ./downloaded-assets注意这个示例有三个关键点:
repository必须是owner/repo格式;run-id是构建仓库里那一次 workflow run 的编号,可以通过 GitHub API 查询,也可以由上游 workflow 通过workflow_dispatch或repository_dispatch事件传下来;github-token必须是拥有构建仓库actions: read权限的 token。我在实际项目里用的是专用 GitHub App 生成的 token,权限最小化,比直接用 PAT 安全。
4. 常见问题与排查技巧实录
4.1 报错信息速查表
迁移过程中我搜集了一堆报错,整理成速查表,遇到问题可以直接对号入座。
| 报错信息 | 原因 | 解决方案 |
|---|---|---|
(409) Conflict: an artifact with this name already exists | 同一 run 中已有同名 artifact | 换唯一 name,或先通过 API 删除旧 artifact |
Resource not accessible by integration | 下载时 token 缺少actions: read权限 | 在 workflow 或 job 顶部配置permissions: actions: read |
Cannot overwrite file because overwrite is false | 目标路径已存在同名文件 | 设置overwrite: true,或下载前清理目录 |
Artifact was not found | 指定的name或pattern匹配不到产物 | 检查 artifact 名称、run-id、repository参数 |
No files were found with the provided path | 上传时path目录不存在或为空 | 检查构建产物生成路径,设置if-no-files-found: error提前暴露 |
Pattern matched no artifacts | pattern通配符没匹配到任何 artifact | 用 API 或 Actions 页面确认实际 artifact 名称 |
4.2 排查实录:权限 403 的完整处理过程
这里分享一个我实际排过的坑。迁移后第一个私有仓库 workflow 就在下载步骤挂了,报Resource not accessible by integration。
我一开始以为是 token 过期,换了 secret 重新跑,还是挂。后来仔细看日志,发现下载步骤其实是通过GITHUB_TOKEN调用 API,而这个 token 在当前仓库的默认权限配置里没有actions: read。
解决方式很简单,在 workflow 顶部加了一段:
permissions: actions: read contents: read重新 push 之后立刻就好了。这个错误偶尔也会出现在公共仓库里,原因是公共仓库的 fork 限制了 token 权限,fork 过来的 pull request 需要额外设置pull_request_target事件才能正常下载。所以记住一个原则:跨仓库、跨 run、fork 场景下,token 权限一定要显式配置,不要依赖默认值。
4.3 压缩级别和保留期怎么调
compression-level并不是越高越好。级别越高,CPU 消耗越大,build 时间会被拖长。我实测过不同级别的表现:
| compression-level | 产物体积(180 MB 前端产物) | 上传耗时 |
|---|---|---|
| 0 | 182 MB | 8 秒 |
| 4(默认) | 64 MB | 12 秒 |
| 9 | 58 MB | 26 秒 |
可以看到,4 到 9 的压缩收益已经很有限,但耗时翻倍。所以除非你的产物体积特别敏感,否则保持默认 4 就好。如果构建机 CPU 很紧张,甚至可以调到 1 或 2,让上传时间最短。
retention-days我也建议根据产物用途分类处理:
- 用于部署的产物:建议设 1 到 2 天,部署完成就没用了;
- 用于测试报告或调试信息的产物:设 3 到 5 天;
- 需要长期留档的发布产物:走专门的发布流程,比如 GitHub Releases,不要长期堆在 Actions artifact 里。
artifact 存储是按量计费的,设短一点能省不少成本,但也要注意别设太短,否则调试问题时会发现产物已经没了,别问我是怎么知道的。
4.4 如果出了幺蛾子,怎么快速回滚
虽然我不觉得你会想回到 v3,但万一迁移后出现诡异问题,最快的回滚方法就是改版本号:
uses: actions/upload-artifact@v3只要 workflow 里把@v4改回@v3,行为就会恢复到原先的样子。不过我要提醒一句:v3 已经弃用,官方随时可能彻底关闭它的服务,回滚只适合应急,不是长期方案。
另外还有一个折中做法,就是把 v4 锁定到某个具体版本,例如@v4.4.0,避免未来某个 minor 版本引入了你不期望的行为变化。等团队适应了 v4 之后,再统一跟踪到@v4大版本名上。
5. 写在最后
这次迁移给我最大的感受是,GitHub 官方把 artifact 的整条链路重写了一遍,v3 时代很多"约定俗成"的写法在 v4 里都是坑。但只要搞懂了同名冲突、路径覆盖和权限这三个关键点,迁移本身是非常顺滑的。
最后再分享一个小技巧:如果你有多个 job 分别上传 artifact,建议在名字里带上矩阵的维度字段,例如test-report-${{ matrix.os }}。这样下载时用一行pattern: test-report-*就能拉全,不用一个个手写 name,也天然避开了 409 同名冲突。我在实际项目中把这条规则写进了团队的 workflow 模板,后续新加的流水线基本没出过 artifact 相关的幺蛾子。