GitHub Actions 完全指南 | CI/CD 自动化工作流配置教程
很多开发者第一次听说 GitHub Actions,是在某个开源项目的 README 里看到一排绿色的通过徽章,或者在提交代码后收到一封构建失败的邮件。它的样子很像一个外挂的 CI 服务,但它真正的位置其实更基础:它是直接长在仓库里的一套自动化机制,代码一动,它就跟着动。你不需要另外搭一台服务器,不需要把代码同步到别的平台,只要在仓库里放一个配置文件,推送、提合并请求、定时任务这些事件发生时,对应的流程就会自动跑起来。
理解 Actions 最好的方式,是先忘掉 CI/CD 这个缩写带来的距离感。它的本质就一句话:当仓库里发生某件事,就按你写好的步骤,在一台临时机器上做一串事。装依赖、跑测试、构建产物、发到服务器、发一条通知,都是这串事里的一环。写工作流的过程,就是把你平时手动敲的命令,按顺序写进一个文件,再交给 GitHub 去执行。
Actions 是什么,适合哪些自动化场景
一个工作流文件由几个层次组成。最外层是工作流本身,也就是 .github/workflows/ 目录下的一个 YAML 文件。文件里定义了触发条件,也就是什么事件发生时它要跑;然后是一个或多个任务,每个任务跑在一台独立的虚拟机上;每个任务里又是一串步骤,步骤可以是一条 shell 命令,也可以是别人写好的一个动作。动作是 Actions 生态里最值得理解的概念:它把一串常用操作打包成可复用的单元,比如拉取代码、安装某个语言环境、登录某个仓库,你只需要在步骤里引用它,不用自己重写实现。
触发条件决定了一个工作流的性格。推送到主分支时跑构建,提合并请求时跑测试,每天凌晨跑一次数据同步,手动点一下按钮跑一次发布,这些都是不同的触发方式。初学者最容易犯的错误,是把所有事情塞进一个“推送就跑”的工作流,结果每次改一行注释也要经历完整的构建和部署。触发条件写得越精确,自动化的成本就越低:测试类的流程适合在合并请求时跑,发布类的流程适合在打标签或手动触发时跑,巡检类的流程适合定时跑。
Actions 适合的场景,大致可以分成四类。第一类是质量门禁:每次有人提合并请求,先跑测试和代码检查,通不过就挡在合并之前。这类流程的价值不在于它发现了多少 bug,而在于它把“能不能合”从主观判断变成了客观结果,审查者不用再凭感觉放行。第二类是发布流水线:代码合入主分支后自动构建、生成产物、部署到服务器或对象存储,省掉手动打包上传的环节,也省掉了“本地能跑、线上不行”的环境差异。第三类是定时巡检:每天固定时间跑一次依赖检查、链接检查、数据同步,把那些“想起来才做”的事变成“到点就做”。第四类是仓库杂务自动化:给新 issue 打标签、提醒长期没更新的 PR、定期清理旧的构建产物,这些事单件看都不大,但长期占用注意力,交给机器最划算。
也有一些场景不适合一上来就用 Actions 硬套。如果流程高度依赖某台特定机器上的私有环境,或者步骤里有大量需要人工判断的环节,自动化之前应该先把流程本身理顺。自动化放大的不只是效率,还有混乱:一个本身就含糊的流程,写成工作流之后只会以更快的速度出错。所以判断一个任务值不值得自动化,有个简单的标准:它是不是已经被手动跑通过至少三次,而且三次的步骤基本相同。满足这个标准,再写成工作流,返工的概率会小很多。
第一个工作流:从零跑通一次
学 Actions 最快的方法,是先让一个最小的工作流跑起来,再慢慢往里面加东西。不要一开始就照着某个大型项目的配置文件抄,那些文件里往往混着缓存、矩阵、条件判断等多种技巧,第一次读很容易被细节淹没。
第一步是在仓库里新建 .github/workflows/ 目录,放一个文件,比如叫 ci.yml。文件名没有强制要求,起一个能说明用途的名字就可以。文件开头是名字和触发条件:名字会显示在仓库的 Actions 页面上,触发条件先写最简单的,也就是推送到主分支时运行。
name: CI
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: 拉取代码
uses: actions/checkout
# 版本号填 Marketplace 当前主版本,下同
- name: 安装依赖
run: pnpm install
- name: 跑测试
run: pnpm test这个文件只有三个步骤,但它已经是一个完整的工作流。把它提交并推送到主分支,然后打开仓库的 Actions 页面,你会看到一次运行记录,点进去能看到每一步的日志。第一次运行最可能遇到的问题不是语法错误,而是环境差异:本地装好的依赖,在干净的虚拟机里可能因为少了某个系统包而装不上。这恰恰是自动化的价值所在,它逼着你把“本地环境里默认存在的东西”显式地写进流程,久而久之,项目的搭建文档也会因此变得更可靠。
跑通之后,下一步是让这个工作流变得有用。测试命令可以换成你项目真实的命令,构建产物可以加一个打包步骤,测试覆盖率可以接一个上报步骤。加东西的节奏建议一次只加一个步骤,每加一个就推一次、看一次日志。这样做看起来慢,但定位问题时非常省时间:某次运行失败,你立刻知道是刚才加的那一步出了问题。反过来,如果一次加了十个步骤再推,失败时要排查的范围就大得多。
还有一个新手常忽略的点:工作流文件的语法错误不会在提交时报错,而是在运行前由 GitHub 解析时才暴露。所以每次改完文件,先在本地用 YAML 校验工具过一遍,再推送。缩进错误、冒号后面少空格、列表层级错位,都是 YAML 里最常见的坑,校验工具能在几秒钟内指出,比等一次运行失败再翻日志要快。
常用触发与任务组织
当第一个工作流跑顺之后,下一个要解决的问题是组织:触发条件怎么配,任务之间怎么配合,重复的部分怎么抽出来。
触发条件里最常用的是推送和合并请求。推送触发适合主分支的持续构建,合并请求触发适合在合并之前做检查。两者经常搭配使用:合并请求时跑测试,合入主分支后跑部署。这样分开的好处是,部署流程不会被每一个试探性的提交触发,而测试流程又能覆盖每一次合并请求。定时触发用 cron 表达式,写的时候要注意它用的是 UTC 时间,换算成本地时间时要减掉时差。手动触发则适合发布类流程,由人在 Actions 页面点一下按钮再跑,发布节奏完全掌握在自己手里。
任务组织的核心是依赖关系。默认情况下,一个工作流里的多个任务是并行跑的,各自用独立的虚拟机,互不干扰。如果某个任务必须等另一个完成,比如部署要等测试通过,就用 needs 声明依赖。依赖还可以用来传递结果:上游任务的输出可以作为下游任务的输入,典型的用法是构建任务产出版本号,部署任务拿这个版本号去打标签。依赖链不宜拉得太长,链越长,单次运行的总耗时越接近各任务耗时之和,并行的优势就被吃掉了。判断依赖是否必要的标准是:下游任务是不是真的需要上游任务的产物或结论,而不是仅仅“习惯上先跑那个”。
矩阵是另一个值得掌握的组织方式。它让你用一份任务定义,在多个环境组合下各跑一次,比如多个操作系统版本、多个语言版本。矩阵的适用场景是兼容性验证:你的项目声称支持多个版本,那就应该在每次提交时真的在这些版本上都跑一遍。但矩阵也有成本,组合数一多,运行时间与资源消耗会成倍增长。一个实用的做法是:日常开发时只跑最常用的一个组合,合并到主分支或打版本标签时再跑完整矩阵。
并发控制是容易被忽视的一环。默认情况下,同一个工作流可以同时跑多次,比如短时间内连续推送了几次,就会排起几次运行。如果流程涉及部署,后一次部署还没开始,前一次部署可能还在进行,互相覆盖的风险就出现了。用并发组把同一分支的运行限制为一次,并让新的运行取消旧的排队运行,是个简单有效的办法。取消的是排队中的旧运行,而不是正在运行的那一次,这个细节值得注意:它保证了正在进行的部署不会被半路打断,同时又不会让队列越积越长。
环境是把“跑测试”和“做发布”区分开的关键机制。你可以为生产环境设置审批人,只有被批准的运行才能继续往下走;也可以给不同环境配不同的密钥,测试环境的密钥即使泄露,影响范围也有限。环境与密钥的搭配使用,是后面安全一节的基础。
可复用工作流解决的是跨仓库的重复问题。当你有多个仓库都要跑同一套检查流程时,把流程抽成一个独立的工作流文件,其他仓库的工作流直接调用它,就不用每个仓库复制一份。调用时可以传参数,比如目标分支或构建命令,复用程度比简单的复制粘贴高得多。复用带来的约束是:被复用的工作流改动时,所有调用方都会受影响,所以它的接口要尽量稳定,改动前要先想清楚影响范围。
密钥与权限安全
自动化流程一旦涉及部署和发布,就绕不开密钥问题。密钥管理的第一原则是:密钥只出现在两个地方,一个是仓库的 Secrets 设置里,一个是运行时的内存里,绝不出现在代码、日志和构建产物中。把密钥写进工作流文件再提交,是最常见也最严重的错误,因为提交历史一旦公开,密钥就等于公开了,即使事后删掉文件,历史记录里依然能找到。
Secrets 的配置路径在仓库设置里,添加时只需要名字和值,值一旦保存就再也看不到明文,只能覆盖或删除。在工作流里引用时用表达式语法,运行器会在执行前把引用替换成真实值,并且自动把值从日志里打码。但打码机制有个边界:它只对完整匹配的值打码,如果你在脚本里把密钥拆开、拼接或编码后再输出,打码就认不出来了。所以最稳妥的做法是:永远不要在步骤里主动输出密钥,哪怕是为了调试。调试时如果怀疑密钥配错了,验证的方法应该是看使用它的那一步是否成功,而不是把密钥打印出来检查。
除了仓库级别的 Secrets,还有环境级别的 Secrets。环境级别的密钥只有运行到对应环境时才可用,配合审批机制,可以做到“测试环境的密钥谁都能用,生产环境的密钥只有审批通过的运行才能用”。对于个人项目,这套机制可能显得重,但它的思路值得借鉴:把密钥按影响范围分层,影响越大的密钥,动用的门槛越高。
权限是另一条容易被忽视的安全线。每个工作流运行都会拿到一个临时令牌,默认情况下这个令牌的权限范围由仓库设置决定。很多项目从来没有改过这个默认设置,导致一个只跑测试的工作流也拿着写仓库的权限。一旦某个步骤里引入了不可信的输入,比如合并请求里的代码,这个令牌就可能被滥用。正确的做法是在工作流顶部显式声明最小权限:只跑测试的流程给读权限就够了,需要推送或发评论的流程再单独放开写权限。声明权限还有一个好处:它让流程的意图变得可见,别人读你的工作流文件时,一眼就能看出它会动仓库里的哪些东西。
还有一类风险来自第三方动作。引用别人写的动作,本质上是在自己的流程里运行别人的代码。选择动作时,优先看它的来源是否可信、更新是否活跃、使用的用户多不多,版本引用要固定到某个具体的提交或主版本,而不是跟随一个随时会变的分支名。对于安全要求高的流程,还可以把动作的源码 Fork 到自己的组织里再引用,这样即使上游被篡改,你的流程也不会立刻受影响。
密钥轮换是安全习惯的一部分。任何密钥都不应该长期不变:员工离职、设备丢失、怀疑泄露,都是轮换的触发条件。轮换的流程本身也可以自动化的一部分:先在服务端生成新密钥,更新到 Secrets 里,跑一次完整流程验证新密钥可用,再作废旧密钥。这个顺序不能反过来,先作废旧密钥再更新,中间就会出现一段服务不可用的窗口。
常见问题排障
工作流出问题时,先看日志,再猜原因。Actions 的日志按步骤分段,每一步的开始、结束和退出码都标得很清楚。定位问题的第一步永远是找到第一个失败的步骤,而不是从最后的错误信息倒推。很多失败的表象在最后一步,但根因在前面某一步悄悄埋下的伏笔,比如依赖安装时某个包下载失败,后面构建时才报找不到模块。
工作流不触发是最让人困惑的一类问题。排查时按顺序想四个地方:文件是不是放在 .github/workflows/ 目录下,文件名后缀是不是 YAML,触发条件是不是真的匹配这次事件,分支名是不是写对了。还有一个隐蔽的原因:工作流文件本身有语法错误,GitHub 会直接拒绝解析,Actions 页面上甚至不会出现这次运行记录。这时可以到仓库的 Actions 页面看顶部的提示,或者用本地的 YAML 校验工具先过一遍。
密钥读不到时,先确认三件事:名字有没有拼错,密钥是不是加在正确的层级,引用语法是不是写对了。名字是大小写敏感的,差一个字母就会读到空值。层级方面,仓库级别的密钥所有工作流都能用,环境级别的密钥只有运行到对应环境时才可用,组织级别的密钥则要看仓库有没有被授权使用。还有一种情况是密钥本身没问题,但使用它的步骤把值传错了地方,比如把密钥当成了命令行参数,shell 在解析时做了意外的拆分,这时改成通过环境变量传递通常就能解决。
权限不足的报错通常很直白,信息里会明确说哪个操作被拒绝。处理方法是回到工作流顶部,把对应的权限放开,或者到仓库设置里调整默认权限。放开权限时要克制,只放开报错里提到的那一项,不要顺手把权限全开。权限放开后,最好再跑一次完整流程确认,同时检查一下其他步骤有没有因为权限变化而产生新的副作用。
运行时间过长,往往不是某一步真的需要那么久,而是流程里藏着重复劳动。依赖安装每次都从零开始、构建产物每次都全量生成、测试每次都跑全部用例,都是常见的耗时来源。缓存是解决办法:把依赖目录、构建缓存按锁文件的哈希做键,命中时直接复用,失活时重新生成。缓存策略要配得保守一点,键里带上操作系统和锁文件哈希,命中失败时自动回退到部分匹配的键,这样既省时间,又不会因为缓存过期而拿到错误的结果。
偶发失败是最难缠的一类。网络抖动、第三方服务短暂不可用、并行任务之间的资源竞争,都可能导致某次运行失败、重跑一次又通过。对于这类问题,先区分它是偶发还是必现:重跑一次通过,大概率是外部波动;连续三次都失败在同一个地方,就是流程本身的问题。对偶发失败,可以给关键步骤加重试,但重试要有上限和间隔,无限重试只会把一次波动拖成长时间挂起。更重要的是把偶发失败记录下来,如果某个步骤每周都偶发一次,说明它依赖的外部服务稳定性有问题,值得换一个更可靠的方案。
进阶方向
当基础流程跑顺之后,进阶的方向不是堆更多的步骤,而是让流程变得可维护。第一个值得投入的方向是可复用工作流和复合动作。多个仓库共用的检查流程抽成可复用工作流,多个步骤里重复出现的 shell 组合抽成复合动作,流程文件本身就会变短,改动的影响范围也更清晰。抽取的时机有个简单的信号:同一段配置第三次出现时,就是该抽出来的时候。
第二个方向是部署流程的分层。测试、预发布、生产不应该共用同一套步骤,而应该用环境把它们隔开:测试环境自动部署,预发布环境半自动,生产环境需要人工审批。审批不是为了拖慢发布,而是给发布留一个最后的确认点,尤其是在发布窗口之外的时间,有人能拦一下总比自动上线一个有问题的版本要好。部署步骤本身也值得做成幂等的,也就是同一个版本部署两次,结果应该和部署一次一样,这样重跑失败的部署时才不会产生副作用。
第三个方向是自托管运行器。GitHub 提供的虚拟机覆盖了大多数场景,但当你需要特定的硬件、内网访问权限,或者对构建环境有合规要求时,自托管运行器就是答案。它本质上是在你自己的机器上装一个代理程序,接收 GitHub 派发的任务并执行。自托管的代价是维护成本:机器的系统更新、安全补丁、代理程序的升级,都要自己负责。所以只有当托管运行器确实满足不了需求时,才值得走这条路,而且自托管运行器不应该用来跑不受信任的代码,比如公开仓库里陌生人提的合并请求。
第四个方向是观测。流程跑得越多,越需要知道它跑得怎么样:每次运行耗时多久,哪个步骤最慢,失败率最高的是哪一步,缓存命中率是多少。这些数据不用专门搭系统,Actions 自带的运行记录和日志已经够用,定期翻一翻,就能发现流程里慢慢变差的部分。比如某个步骤的耗时逐月上升,可能是依赖体积在膨胀;某个任务的失败率突然升高,可能是上游服务改了接口。观测的目的是让流程的退化可见,而不是等到某天彻底跑不动了才发现。
第五个方向是动作版本的升级策略。动作的主版本会演进,新版本可能带来性能提升,也可能改掉旧行为,升级时最稳妥的做法是先在一个非关键仓库或分支上验证,再推广到主力项目。验证时重点看三件事:新版本有没有改动输入参数的名字,默认行为有没有变化,运行日志里有没有新增的警告。警告尤其值得留意,它往往是下一次大版本 breaking change 的预告,提前处理比等到被迫升级时再返工要从容。升级记录也可以写进提交信息,这样日后回查某次流程变化的原因时,能直接从历史里找到答案。
最后还有一个容易被忽略的进阶点:工作流文件本身也是代码,应该享受和业务代码一样的待遇。提交信息写清楚改了什么,大的改动先在分支上验证再合入主分支,废弃的流程及时删除而不是注释掉留在那里。很多仓库的 .github/workflows/ 目录里躺着三四个早已没人用的文件,新人接手时根本分不清哪个还在跑。定期清理这些文件,和定期清理无用代码一样,都是维护工作的一部分。
结尾
GitHub Actions 的学习曲线并不陡,陡的是从“跑起来”到“跑得稳”之间的那段路。跑起来只需要一个 YAML 文件,跑得稳需要触发条件写得精确、任务组织得有层次、密钥管得严格、失败时有据可查。这篇文章的顺序也是按这个思路排的:先理解它是什么,再跑通第一个流程,然后学组织和安全,最后才谈进阶。按这个顺序走,每一步都有前一步的经验垫着,不容易在细节里迷路。
开始动手之前,先给自己定一个小目标:给手头的一个项目加一个工作流,推送时跑测试,合入主分支后自动部署。跑通之后,再回过头来按安全和组织的要求收紧一遍。这个从简到严的过程,比一开始就追求完美配置要扎实得多。自动化真正的收益从来不是省下几次手动操作,而是让那些本来依赖记忆和细心的事,变成每次都一样的结果。
下一步学习: