GitHub Actions 实战避坑指南
我在 GitHub 工作,每天都在用 Actions。同时,我也在凌晨两点调试过 YAML,看着日志查看器把浏览器吃掉,连续推了一堆 commit 只为搞清楚一个条件表达式为什么没有生效。
我不打算告诉你 Actions 是完美的。它不是。日志查看器让资深工程师开始怀疑人生。YAML 表达式语法的学习曲线更像是学习悬崖。“推代码 → 等待 → 失败 → 重复”的调试循环能把一个五分钟的修复变成一整个下午的消耗战。
这些我都经历过。也见过无数开发者经历同样的事。
但我也观察到:大部分痛苦来自于可以避免的使用模式。不是全部——有些确实是平台需要追赶的地方。但很多问题已经有现成的解决方案,只是大家还没发现,因为复制粘贴 YAML 太容易了,就一直在忍受。
这篇文章整理的是我希望第一天就知道的那些实用技巧。
别用日志查看器了
我是认真的。Web 界面的日志查看器是 Actions 最大的槽点之一,最快的解决方法就是不用它。
gh run view --log-failed
就这一行。失败的步骤直接输出到终端,立刻可见。不用点三层页面,不用等浏览器决定今天要不要渲染,不用体验返回按钮的随机跳转。
查看完整日志:
gh run view --log
实时观察运行进度:
gh run watch
CLI 更快,可以用 grep 搜索,而且不会在测试输出 50,000 行日志时崩溃。如果 2026 年了你还在通过 Web 界面一层层点进去看构建日志,是时候改变了。
YAML 膨胀是真实存在的问题,但可以控制
所有 CI 系统最终都会变成”一堆 YAML”。Actions 也不例外。但一个清晰做一件事的 40 行 workflow 和一个充满嵌套条件、矩阵策略和内联 bash 脚本的 400 行怪物之间,差别巨大。
400 行怪物的出现,是因为很多人不了解专门设计来解决这个问题的两个功能。或者知道但不用。
可复用 Workflow
如果你在多个仓库中有相同的 CI 步骤,大概率在复制粘贴 workflow 文件。别这么干了。
# .github/workflows/ci.yml
jobs:
build:
uses: your-org/.github/.github/workflows/build.yml@main # 内部仓库用 @main 没问题——但如果其他地方都固定了 SHA,这里也考虑固定
with:
node-version: '20'
secrets: inherit
一个 workflow,维护在一个地方,所有仓库调用。更新一次,所有使用它的仓库自动生效。不会出现版本漂移,不会出现”等等,哪个仓库里的部署脚本是最新的?“
组合 Action(Composite Actions)
可复用 workflow 用于完整流水线。组合 action 用于步骤级别——可以理解为构建积木。
# .github/actions/setup-project/action.yml
name: 'Setup Project'
runs:
using: 'composite'
steps:
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: ${{ inputs.node-version }}
- run: npm ci
shell: bash
- run: npm run build
shell: bash
这样你的 workflow 文件读起来像句子,不像电视连续剧:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/setup-project
with:
node-version: '20'
- run: npm test
YAML 还在。但从 120 行缩减到了 12 行。设置变更时只需改一个地方。
打破”推代码 → 等待 → 失败”的循环
调试 Actions 最折磨人的地方:改了一个字符,推上去,等四分钟等 runner 启动,结果发现漏了一个引号。一堆 commit 之后,你的 git 历史看起来像一封求救信。
act,一个社区维护的工具,可以在本地 Docker 容器中执行你的 workflow。相同环境,无需推送。
act -j build
它不是完美复刻。有些 GitHub 特有的上下文在本地不存在。但对于”YAML 有没有写错”和”bash 脚本到底能不能跑”这类问题,它把反馈循环从分钟级缩短到秒级。
如果只想做语法校验:
gh workflow view ci.yml
如果 YAML 有明显问题,它能快速发现。虽然不是完整的 linter,但足以在不推代码的情况下排除基本错误。
Marketplace 的信任问题
每写一行 uses: 某个陌生人/酷炫-action@v2,就是在让你从未见过的人的代码访问你的仓库和密钥。这是真实的安全风险。“固定到 SHA”是正确答案,但几乎没人执行。
实际可行的做法:
- 固定到 SHA,让 Dependabot 管理更新:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
在注释中写上版本号方便阅读。Dependabot 会在新版本发布时自动开 PR,你可以审查 diff 后再更新。(本文中的 SHA 均于 2026 年 3 月 30 日验证。)
-
尽量使用 GitHub 官方维护的 action。
actions/checkout、actions/setup-node、actions/cache—— 由构建平台的同一个团队维护,经过审计和测试。 -
其他 action,先读源码。都是开源的。如果一个 marketplace action 本质上就是一个 20 行的 shell 脚本套了个 Dockerfile,不如直接把这 20 行复制到
run:步骤里,自己掌控。 -
用 OpenSSF Scorecard 在采用前评估 action 维护者的安全实践。
条件逻辑的生存法则
${{ }} 表达式语法属于那种”入门简单,深入之后让人崩溃”的东西。字符串插值、truthy 判断、类型强转的边界情况坑过所有人。
几条实用规则:
# if 中的表达式始终加引号
if: ${{ github.event_name == 'push' }}
# 用 fromJSON 处理 input 中的布尔值
if: ${{ fromJSON(inputs.deploy) == true }}
# 多条件?用 >- 将换行折叠为空格
if: >-
${{ github.ref == 'refs/heads/main' &&
github.event_name == 'push' }}
如果你的条件逻辑复杂到需要画流程图,说明你需要的是一个带输入参数的可复用 workflow,而不是更多的 if: 语句。
case() 函数 是近期新增的功能,值得了解。可以理解为表达式中的 switch 语句,用来替代没人能读懂的嵌套三元表达式。
让 Copilot 来写 YAML
写 YAML 不好玩,我不装了。但在 2026 年,大部分 YAML 不需要你手写。
在 VS Code 中使用 GitHub Copilot,用注释描述你想要的效果:
# push 到 main 时自动部署生产环境,先跑测试,
# 缓存 node_modules,失败时通知飞书群
Copilot 生成 workflow,你审查和调整。它处理语法细节——on: 触发器、runs-on:、步骤排列——你只需要关注流水线应该做什么,不用纠结 environment 到底写在 jobs: 里面还是外面。
对于已有的 workflow,Copilot 智能体模式可以把一个 400 行的 YAML 文件重构为可复用 workflow 和组合 action。告诉它你的目标,审查 PR 即可。
这不是修复 Actions 本身,而是让你不用直接跟它肉搏。
缓存:你可能还没用上的免费加速
如果你的 workflow 每次都重新安装依赖但没有配置缓存,你在白白浪费几分钟的构建时间。
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: '20'
cache: 'npm'
就这一行 cache: 'npm',告诉 setup-node 在多次运行之间缓存 node_modules。第一次正常安装,之后每次跳过完整的 npm ci 下载。对于大型项目,每次构建能省两到四分钟。
其他技术栈同理:
# Python
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.12'
cache: 'pip'
# Go(手动缓存)
- uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
如果需要更精细的控制,actions/cache 可以缓存任何东西——构建产物、Docker 层、编译后的二进制文件。核心思路是用 lockfile 的哈希值作为缓存 key,确保依赖真正变化时才刷新缓存。
别让同一个 Workflow 跑两遍
如果团队推代码频率高,你大概见过这种场景:十分钟内三个 commit,三个 workflow 排队运行,前两个跑完的时候早就过时了。
并发组可以解决:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
加在 workflow 的顶层。同一个分支、同一个 workflow——新的运行启动时自动取消旧的。不再为已经被覆盖的 commit 浪费 runner 资源。
这在 PR 工作流中特别有用,开发者快速迭代时效果显著。不用这个的话,你就是在为没人会看的构建结果买单。
哪些在变好,哪些还需要努力
既然说了不粉饰,那就具体说说哪些改善了,哪些还差得远。
已经改善的:
- Job summaries,结构化输出,不只是日志行
- 更大的 runner,64 核机器可用,ARM runner 已经 GA
- Required workflows 通过 Rulesets 实现,组织级别强制执行,无需复制粘贴
- 更大的 runner,64 核机器在付费计划(Team/Enterprise)中可用,ARM runner 已对公共仓库 GA
- 不可变 action,发布到 GHCR 并附带 provenance
case()函数和近期表达式能力增强
仍需改进的:
- 日志查看器。有进步,但对大型构建来说还不够好。用 CLI 吧。
- 调试体验。
act有帮助,但如果官方提供本地执行能力,会改变一切。 - 表达式的学习曲线。文档团队一直在持续改进,效果有目共睹——但仍有提升空间。
我写这篇文章不是为了给 GitHub 辩护。是因为我见过太多团队在有解决方案的问题上反复受苦,而这些方案传播得不够快。
基础能力是有的。平台能用。但”能用”和”好用”是两码事,中间的差距就是你下午消失的地方。这些实践能缩小这个差距。不能完全消除,但够用了。
🆕 更新: 我把这篇文章里的实践经验整理成了一个可复用的 Copilot skill — 一个 markdown 文件,你的 AI 编程助手在编写 workflow 时可以直接参考。涵盖 SHA 固定、matrix 策略、artifacts、权限配置、超时设置,以及那些让你下午白白浪费的文件系统问题。目前还在测试中,但你现在就可以试试:GitHub Actions Skill
🌐 关于中文版本: 本文由作者创建并维护于 mainbranch.dev 上的开源仓库中。如果你发现翻译中有任何不准确的地方,欢迎直接提交 PR 帮助改进:github.com/AndreaGriffiths11/mainbranch-zh