GitHub Actions CI/CD 实战
GitHub Actions 是开发者最易上手的 CI/CD,但写好也有讲究。本文讲清 workflow 结构、触发器、job/step/matrix、缓存与 artifact、密钥管理、并发与成本,以及一份"前端 + 后端 + 部署"的实战 workflow,让 CI 既快又稳又不烧额度。
本文要点
- 主题聚焦:DevOps
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
CI/CD 这件事,工具多得能开个展会——Jenkins、GitLab CI、CircleCI、Drone、Argo CD……但过去三年,我把几乎所有项目的流水线都迁到了 GitHub Actions。原因不是它最强,而是它门槛最低、和代码贴得最近。
但"易上手"不等于"写得好"。一个能跑的 workflow 和一个又快又稳又省钱的 workflow,中间隔着十几条实战经验。这篇把我踩过的坑和形成的习惯一次讲清。
一、为什么是 GitHub Actions
三个理由,按重要性排。
第一,和仓库一体。.github/workflows/*.yml 跟你的业务代码一起 review、一起版本化、一起回滚。改流水线不用去别的平台点鼠标,提个 PR 就行。这条优势被低估了——它意味着 CI 配置可以享受代码审查的全部红利,谁改了部署脚本、谁加了新密钥,清清楚楚。新人 onboarding 也只需读一个仓库,不用同时翻 Jenkins 控制台和 Confluence 文档。
第二,免费额度对个人和小团队够用。公开仓库无限免费;私有仓库每月 2000 分钟(免费账号),Pro 是 3000。对大部分 indie 项目和小公司,这个额度撑很久。超了再按用量付费,也比养一台 Jenkins 服务器省心——后者还要有人维护插件升级、磁盘扩容、节点弹性伸缩。
第三,生态丰富。Marketplace 上几千个 action,装个 Node、配个 AWS、发个飞书通知,都是一行 uses: 的事。不用再写一堆 shell 脚本去处理 nvm install、aws configure 这种琐碎活。官方维护的 actions/checkout、actions/setup-node、actions/cache 质量都很高,可以放心用。
但它的坑也明确:计费按"分钟数"且不同 OS 系数不同(Linux 1x、Windows 2x、macOS 10x),自托管 runner 的配置有学习曲线,大型 monorepo 的并发队列会卡。下面会逐一说怎么绕开。
二、workflow 的骨架:trigger / job / step / action
一个 workflow 文件就四个概念:trigger(什么时候跑)、job(跑一台机器上的任务序列)、step(job 里的每一步)、action(可复用的步骤单元)。
name: CI
on:
push:
branches: [main]
paths-ignore: ['**/*.md', 'docs/**'] # 文档改动不触发,省额度
pull_request:
schedule:
- cron: '0 2 * * *' # 每天凌晨 2 点跑一次,适合扫依赖漏洞
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm test
几个我坚持的习惯:
actions/checkout@v4用带版本号的 tag,别用@main。哪天上游发了破坏性更新,你的 CI 就炸了。pin 到主版本(@v4)是平衡,对供应链安全要求高的团队就 pin 到 commit SHA。permissions一定要显式写,默认给最小权限。GITHUB_TOKEN 默认权限过大,2023 年起 GitHub 已经把新仓库的默认 token 改成了 read-only,但显式声明是给后来者看的信号,也防止有人手滑改回去。npm ci而不是npm install。前者严格按 lockfile 装,更快、更可复现,CI 里就该用它。pnpm 对应的是pnpm install --frozen-lockfile。- 多 job 之间用
needs串成 DAG。needs: [lint, test]既决定执行顺序,也决定依赖关系——上游挂了下游不跑,省下无效的执行时间。needs还能配合outputs在 job 间传值,比如构建产物版本号、镜像 digest。
三、矩阵构建与条件执行:让 CI 干活的两种姿势
**矩阵(matrix)**让你一份配置跑多种组合——多 Node 版本、多 OS、多依赖版本。开源库最典型:同时测 Node 18/20/22。
jobs:
test:
strategy:
fail-fast: false # 一个挂了别把其他也取消,我想看全貌
matrix:
node: [18, 20, 22]
os: [ubuntu-latest, windows-latest]
exclude:
- node: 18
os: windows-latest # 老版本在 Windows 上不再支持,排掉
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm ci
- run: npm test
注意 windows-latest 计费是 2x,没事别加。真要测 Windows,优先考虑能不能用容器化的 Linux 跑。include 用于给特定组合追加变量(比如只有 Node 22 的那条任务要多跑个 e2e),exclude 用于抠掉不合理的笛卡尔积,这两个关键字能让矩阵既灵活又不冗余。
条件执行(if) 是另一把利器。最常见的:只在 main 分支部署,只在打 tag 时发版。
- name: Deploy
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: ./deploy.sh
if 里能用一堆上下文:github.event_name、github.actor、steps.build.outputs.skip、甚至 contains(github.event.head_commit.message, '[skip ci]')。最后这条我常用来支持"commit message 里带 [skip ci] 就不跑"的约定。还要记住一个细节:if 表达式里字符串比较要写完整引号,且 && 周围最好留空格,否则解析会出怪问题。
四、缓存与 artifact:提速的关键
CI 慢,90% 是装依赖和重复构建。缓存能把这两块压下去。一个冷启动 90 秒的安装,命中缓存后 8 秒结束——差距就是这么大。
actions/setup-node@v4 自带 npm 缓存,开个参数就行:
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # 自动缓存 ~/.npm
pnpm 项目要手动配一下:
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
更通用的方案是 actions/cache@v4,自己指定 path 和 key。key 一定要带上 lockfile 的 hash,否则依赖变了还命中旧缓存,等于没缓存——这种 bug 极其隐蔽,你会在某次升级依赖后莫名其妙地测到旧版本的代码:
- uses: actions/cache@v4
with:
path: |
~/.pnpm-store
.next/cache
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
restore-keys 是"前缀降级匹配",精确 key miss 时退而求其次,比冷启动快得多。.next/cache、Vite 的 node_modules/.vite 这类构建缓存也值得存,二次构建能省一半时间。
artifact 用来在 job 之间传产物,或者把构建结果存下来供下载。前端项目典型用法:build job 产出 .next 或 dist,deploy job 拉过去部署:
- name: Upload build
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 7 # 别无限期存,artifact 也占额度
actions/download-artifact@v4 在下游 job 拉回来。注意 v4 之后 artifact 不能跨 workflow 默认共享,得在同一 workflow 里传。retention-days 一定要设,默认 90 天,debug 用的产物堆三个月,空间账单会让你惊讶。
五、密钥与环境:别把 token 硬编码进 yml
第一条铁律:任何 secret 都不能写进 yml,也不能写进代码。 一旦 push 到仓库,即使后来删掉,Git 历史里还在,爬虫几秒就能扫到。GitHub 自己也会扫,扫到了会自动通知云厂商吊销那个 token——但那已经是事故响应了,不该指望。
正确做法:在 repo 的 Settings → Secrets and variables → Actions 里加 secret,workflow 里用 ${{ secrets.XXX }} 引用。引用时务必走 env,不要插值到 run 命令的字符串里:
- name: Deploy
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
run: ./deploy.sh # 脚本内部读 $DEPLOY_KEY,日志里会自动打码
环境(environments) 是进阶武器。给 production 看护一套更严的 secret,并要求人工审批、限制分支:
jobs:
deploy:
environment:
name: production
url: https://example.com
runs-on: ubuntu-latest
配 environment 时能在 UI 里设"部署到 production 需要 reviewer 批准""只能从 main 分支触发""等待 1 小时再继续"。这是免费版就有的能力,但九成团队没用。生产部署加一道审批,关键时刻能救命。
再进一步是 OIDC 无密钥认证(keyless auth)。传统做法是把 AWS Access Key 塞进 secret,长期有效、权限大,泄露了就是大事故。OIDC 让 GitHub 向云厂商签发一次性 token,workflow 跑完就失效,仓库里根本没有任何长期凭证。AWS、GCP、Azure 都支持,配置一次终身受益,这是 2026 年所有认真对待安全的团队该有的基线。
六、并发、超时、重试:让 CI 又稳又不烧额度
并发控制(concurrency) 是省钱的关键。同一个分支反复 push,旧 CI 应该被取消,只跑最新的:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
PR 场景这条尤其值钱——开发者一天推十次代码,不取消进度的话,额度烧得飞快。group 命名有讲究:workflow-ref 是分支级粒度,适合大多数场景;deploy 这类敏感操作则不该 cancel,以免中断到一半的部署,这时 cancel-in-progress: false 更稳。
超时(timeout-minutes) 必加。卡死的 job 默认能跑 6 小时,直接烧光你当月额度。我就见过一个项目因为第三方 API 挂了,deploy job 卡了 5 小时,当月免费额度直接清零。给每个 job 设个合理上限,CI 类 15 分钟、部署类 20 分钟、e2e 类 30 分钟,超出就杀掉报错,比让它挂着强。
失败重试 用 strategy.retry-attempts(作用于整个 job)或 step 级别的脚本重试。第三方依赖的偶发 flaky,加一次重试能省很多"重跑 CI"的人力。但别滥用——如果一个测试经常 flaky,根因是测试本身写得有问题,加 retry 是把垃圾扫进地毯下面,迟早绊倒人。
七、实战:一份能直接抄的 workflow
下面这份是我现在的标配,质量检查 + 测试 + 构建部署,带成本控制。删掉敏感信息直接能用:
name: CI
on:
push:
branches: [main]
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
quality:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with: { version: 9 }
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm typecheck
test:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with: { version: 9 }
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'pnpm' }
- run: pnpm install --frozen-lockfile
- run: pnpm test -- --coverage
build-and-deploy:
needs: [quality, test]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
timeout-minutes: 20
environment: production
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with: { version: 9 }
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'pnpm' }
- run: pnpm install --frozen-lockfile
- run: pnpm build
- name: Configure AWS
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::xxx:role/github-actions # OIDC 无密钥
aws-region: ap-northeast-1
- run: ./scripts/deploy.sh
成本控制的精髓在这一行:if: github.ref == 'refs/heads/main'。 部署只在 main 分支的 push 触发,PR 不跑构建产物上传、不跑部署。needs: [quality, test] 保证前两步过了才进 deploy,避免白跑。三个 job 里 quality 和 test 并行跑,整体耗时是 max(quality, test) + deploy,而不是全部串行——流水线设计的第一原则就是"能并行的绝不串行"。
再给一条省钱的经验:别在 PR 上跑全量 e2e。Playwright/Cypress 跑一遍几分钟,一天十次 PR 就是几十分钟。做法是:PR 只跑单元测试 + 关键冒烟;全量 e2e 放到 main 分支的 nightly schedule 里跑一次。质量有保障,额度也不爆。同理,lint 和 typecheck 也可以用 paths 过滤,只动文档时不触发。
小结
- workflow 四件套要分清:trigger(何时跑)、job(一台机器上的任务序列)、step(每一步)、action(可复用单元)。pin action 版本,显式写
permissions,CI 用npm ci/--frozen-lockfile,用needs把 job 串成 DAG。 - 矩阵铺版本、
if控条件:Node 多版本用 matrix;部署、发版这类动作用if卡分支和事件。Windows 计费 2x,没事别加。 - 缓存和 artifact 是提速的两把刀:setup 自带 cache,key 带 lockfile hash,
restore-keys做前缀降级;artifact 别忘设retention-days。 - secret 永远不进代码,环境给生产加审批,OIDC 走无密钥:这三件事是 2026 年的安全基线,免费版就能做到。
- 并发取消 + 超时 + 分层测试 = 省额度三件套:
cancel-in-progress砍掉冗余跑,timeout-minutes防卡死烧钱,PR 不跑全量 e2e。
一句话收尾:好的 CI 不是"能跑",是"该跑才跑、跑得快、跑挂了不连累别人"。 把流水线当代码来对待——它就该和业务逻辑一样被 review、被优化、被尊重。
Related
相关推荐
文章
Docker 部署 Next.js 社区项目的注意事项
围绕 DevOps、Next.js 的技术趋势、工程实践和独立开发视角。
文章
OpenTelemetry 如何帮助你观察 AI 应用?
围绕 DevOps、AI 的技术趋势、工程实践和独立开发视角。
文章
长运行工具调用:Operation、Poll 与 Cancel 的终态协议
初始请求超时后,工具可能仍在服务端运行;发出取消也不等于副作用已停止。本文为单次长运行工具调用建立 operation 身份、轮询水位、取消竞态和唯一终态接受点。
文章
Agent 记忆删除传播:从用户遗忘请求到可验证清除
删除主表一行不会清掉向量、摘要、缓存和恢复后的备份。本文用删除状态机、代际墓碑、派生血缘与端到端 canary 验证真正遗忘。