GitHub Actions CI 流水线实战:从零搭建自动化构建测试
凌晨三点,手机震动。同事发来消息:“生产环境挂了,你昨天合并的那段代码有问题。”
我脑子嗡的一声。明明本地跑过测试了啊?后来翻日志才发现,本地 Node 版本是 20,测试环境用的是 18,某个 API 行为不一致导致的 bug。那一刻我只想骂人——如果有个 CI 流水线在合并前自动跑一遍测试,这事儿根本不会发生。
说实话,手动跑测试这事儿,十个开发者里有九个会忘。另一个不会忘的,大概率是被坑过。GitHub Actions 就是来解决这个问题的:push 代码后自动构建、自动测试、自动部署。不用你操心,机器帮你搞定。
这篇文章我会带你从零搭建一条完整的 CI 流水线。包括:一个可以直接复制使用的工作流模板、Matrix 策略实现多版本并行测试(能把构建时间砍掉一半以上)、还有我踩过的坑和实战经验。准备好了吗?我们开始。
第一章:GitHub Actions 快速上手
GitHub Actions 是什么
简单说,GitHub Actions 就是 GitHub 内置的自动化平台。你在本地 push 代码,它在云端帮你跑测试、构建、部署——全自动的。
以前做 CI/CD,得自己搭 Jenkins 服务器,配置、维护、升级全是活儿。GitHub Actions 厉害的地方在于:不用管服务器,不用装软件,仓库里丢个 YAML 文件就完事。而且每个月免费送你 2000 分钟(公开仓库无限制),对个人项目和小团队来说绰绰有余。
跟 Jenkins、Travis CI 比起来,GitHub Actions 的优势挺明显的:跟 GitHub 仓库深度集成,PR 里直接看构建状态;配置简单,不用学 Groovy 语法;生态丰富,官方 Marketplace 上有上万个现成的 Action 可以直接用。缺点也有:被 GitHub 绑定,想迁移到 GitLab 得重写配置;复杂的企业级流水线,可能还是 Jenkins 更灵活。但对大多数项目来说,GitHub Actions 足够了。
核心概念速览
刚开始学 GitHub Actions,很容易被几个概念绕晕。我用大白话解释一下:
Workflow(工作流):一个 YAML 文件,定义了一整套自动化流程。比如”每次 push 到 main 分支就跑测试”,这就是一个工作流。放在 .github/workflows/ 目录下。
Job(作业):工作流里的一组步骤。多个 Job 可以并行执行,也可以设置依赖关系。比如先跑”测试”Job,再跑”部署”Job。
Step(步骤):Job 里的具体操作,按顺序一个一个执行。可以是执行一行命令(npm test),也可以是调用别人写好的 Action(actions/checkout@v4)。
Runner(运行器):执行 Job 的虚拟机。GitHub 提供三种:ubuntu-latest(Linux)、windows-latest(Windows)、macos-latest(macOS)。你也可以用自己的服务器,不过大部分情况用官方的就够了。
打个比方:Workflow 是一本剧本,Job 是剧本里的几场戏,Step 是每场戏里的具体动作,Runner 就是演这场戏的演员。
你的第一个 CI 工作流
别想太多,先跑起来再说。在你的项目根目录创建 .github/workflows/ci.yml,把下面这段代码贴进去:
name: CI Pipeline # 工作流名称,显示在 GitHub Actions 页面
on:
push:
branches: [main] # push 到 main 分支时触发
pull_request:
branches: [main] # PR 指向 main 分支时触发
permissions:
contents: read # 最小权限原则:只给读权限
jobs:
build:
runs-on: ubuntu-latest # 使用最新的 Ubuntu 环境
timeout-minutes: 15 # 超时时间,防止卡死
steps:
- name: Checkout code
uses: actions/checkout@v4 # 拉取代码
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20 # 使用 Node.js 20
cache: 'npm' # 启用 npm 缓存
- name: Install dependencies
run: npm ci # 安装依赖,ci 比 install 更快更可靠
- name: Run tests
run: npm test # 跑测试
- name: Build
run: npm run build # 构建
这段代码做了什么?
第一部分 on 定义触发条件:push 到 main 分支、或者 PR 指向 main 分支时触发。第二部分 permissions 声明权限,遵循最小权限原则,只给 contents: read,防止工作流意外修改仓库。第三部分是核心:一个叫 build 的 Job,在 Ubuntu 上跑,依次执行拉代码、装 Node、装依赖、跑测试、构建这几步。
提交这段代码,push 到 GitHub,然后打开仓库的 Actions 页面。你会看到一个绿色的小圆圈在转——那是 Runner 在执行你的工作流。等几分钟,如果全变成绿色的勾,恭喜你,你的第一条 CI 流水线跑通了。
如果出现红叉怎么办?点进去看日志,每一步的输出都清清楚楚。90% 的情况是依赖安装失败或者测试本身有问题,跟 CI 配置关系不大。
第二章:CI 流水线核心配置
第一章的工作流能跑起来,但离真正好用还有距离。这一章我们聊聊四个核心配置:触发器、权限管理、环境变量、依赖缓存。这些是 CI 流水线的骨架,配置得当能让你的工作流更安全、更高效。
触发器:什么时候运行
触发器决定了工作流什么时候启动。最常用的两个:push 和 pull_request。
on:
push:
branches: [main, dev] # push 到 main 或 dev 分支时触发
paths:
- 'src/**' # 只有 src 目录下的文件变化才触发
- 'package.json' # package.json 变化也触发(依赖更新)
pull_request:
branches: [main] # PR 指向 main 时触发
paths 过滤器特别实用。比如你的项目有文档目录 docs/,改文档不应该触发 CI。加了 paths 配置后,只有代码变化才会跑构建,省资源也省时间。
除了 push 和 PR,还有几种触发方式:
schedule:定时任务,用 cron 表达式。比如每天凌晨跑一次构建:
on:
schedule:
- cron: '0 0 * * *' # 每天 UTC 0点(北京时间8点)
我有个项目用这个做定期依赖检查:每天跑 npm outdated,检测过期的包,自动发邮件提醒。
workflow_dispatch:手动触发。有时候你想跑一次构建但不 push 代码(比如测试某个配置),这个就派上用场了。在 Actions 页面会显示一个 “Run workflow” 按钮,点一下就能手动跑。
on:
workflow_dispatch: # 手动触发,不需要额外配置
权限管理:安全第一
GitHub Actions 默认会给工作流一个 GITHUB_TOKEN,这个 token 能读写仓库、创建 PR、甚至推送代码。听起来方便,但也意味着风险:如果工作流被恶意利用,攻击者能拿到你仓库的写权限。
2021 年有个安全事件,某个开源项目的 CI 工作流被利用,攻击者通过伪造的 PR 提交恶意代码。教训很惨痛。所以现在 GitHub 推荐一个原则:显式声明最小权限。
permissions:
contents: read # 只能读仓库内容,不能写
pull-requests: write # 如果需要创建 PR,单独声明
对于纯 CI 流水线(只跑测试和构建),contents: read 就够了。如果工作流需要发布 Release、评论 PR 之类的操作,再按需要添加权限。
一个实用技巧:在仓库设置里把默认权限改成 “Read repository contents permission”,这样所有工作流默认只有读权限,需要写的单独声明。多一道防线,少一份风险。
环境变量:分层管理
环境变量有三个层级:workflow 级、job 级、step 级。级别越低,作用范围越小,但能覆盖上一级的值。
env:
NODE_ENV: production # workflow 级,所有 job 都能用
CI: true # 很多工具检测到这个变量会调整行为
jobs:
build:
env:
BUILD_TARGET: web # job 级,只在 build job 里有效
steps:
- name: Run custom script
env:
MY_VAR: hello # step 级,只在这一步有效
run: echo $MY_VAR
为什么要分层?举个例子:你的项目有多个 Job,build 和 deploy。NODE_ENV 两个 Job 都需要,放在 workflow 级。但 BUILD_TARGET 只有 build Job 用,放 job 级更清晰。某个步骤需要临时变量(比如某个脚本的参数),放 step 级。
敏感信息怎么处理?千万别直接写在 YAML 里。GitHub 提供了 Secrets 功能:在仓库设置里添加密钥(比如 API_KEY),工作流里用 ${{ secrets.API_KEY }} 引用。Secrets 在日志里会被自动隐藏,不会泄露。
steps:
- name: Deploy to server
env:
SSH_KEY: ${{ secrets.SSH_KEY }} # 从 Secrets 引用
run: |
echo "$SSH_KEY" > private.key
ssh -i private.key user@server 'deploy.sh'
依赖缓存:加速构建
如果你的项目依赖很多(几百个 npm 包),每次 CI 都从头安装,时间会很长。我的一个项目,装依赖要 3 分钟,跑测试 1 分钟,装依赖占了 75% 时间。
GitHub Actions 提供了缓存机制,能把安装好的依赖存起来,下次跑直接用。最简单的方式:setup-node 自带缓存。
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm' # 自动缓存 npm 依赖
加一行 cache: 'npm',第一次跑会正常安装,同时把 node_modules 存到缓存里。第二次跑的时候,如果 package-lock.json 没变,直接从缓存取,安装时间从 3 分钟变成 10 秒。
如果你用 pnpm 或 yarn,改成 cache: 'pnpm' 或 cache: 'yarn'。
更精细的缓存可以用 actions/cache:
- name: Cache dependencies
uses: actions/cache@v4
with:
path: ~/.npm # npm 全局缓存目录
key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-
key 是缓存的唯一标识。这里用了 package-lock.json 的哈希值:锁文件变了,缓存就失效,重新安装。restore-keys 是备用策略:如果精确匹配没找到,找同类缓存先恢复一部分。
缓存命中率很高的话,构建速度能变快不少。我有个项目实测:无缓存 4 分钟,有缓存 1.5 分钟。每天跑 20 次构建,省下来的时间够写一篇博客了。
第三章:Matrix 策略:并行测试
这是我最喜欢的 GitHub Actions 功能,也是这篇文章的核心卖点。Matrix 策略能把一个 Job 变成多个并行执行的 Job,同时测试不同版本、不同操作系统。一次 push,几秒钟内启动十几个构建任务,全部并行跑,总时间比串行执行少一半以上。
Matrix 是什么
打个比方:你要测试项目在 Node 16、18、20 三个版本下是否正常。传统做法是写三个 Job,或者在一个 Job 里依次切换版本测试。这样要么配置冗余,要么时间很长。
Matrix 就像一个表格:横轴是 Node 版本,纵轴是操作系统,每个格子就是一个独立的测试任务。GitHub Actions 会自动生成所有组合,并行执行。
strategy:
matrix:
node: [16, 18, 20]
os: [ubuntu-latest, windows-latest]
这段配置会生成 6 个 Job:Node 16 在 Ubuntu 上、Node 16 在 Windows 上、Node 18 在 Ubuntu 上……以此类推。全跑完的总时间,取决于最慢的一个 Job,而不是所有 Job 加起来。
版本矩阵:多 Node 版本测试
我有个项目遇到过这种问题:开发用的是 Node 20,某天用户报告说在 Node 18 上跑不了。查了一圈发现是某个 API 在 Node 18 下行为不一样。如果早点做多版本测试,这 bug 根本不会发出去。
用 Matrix 做多版本测试很简单:
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false # 一个版本失败了,其他版本继续跑
matrix:
node-version: [16, 18, 20, 22] # 测试这四个版本
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }} # 动态使用 matrix 里的版本
cache: 'npm'
- run: npm ci
- run: npm test
主要点解释一下:
matrix.node-version定义要测试的版本列表${{ matrix.node-version }}在 steps 里引用,每个 Job 会拿到不同的值fail-fast: false表示一个版本失败后,其他版本不中断继续跑。默认是true,一个失败全部停止。做兼容性测试时建议关掉,这样你能看到所有版本的测试结果。
include 和 exclude:有时候某些组合不需要测试,或者需要额外配置,可以用这两个关键字。
strategy:
matrix:
node-version: [16, 18, 20]
os: [ubuntu-latest, windows-latest]
exclude:
- node-version: 16 # 不测试 Node 16 + Windows 组合
os: windows-latest
include:
- node-version: 20 # Node 20 在 macOS 上额外跑一次
os: macos-latest
exclude 排掉不需要的组合,include 添加额外的组合。灵活控制测试范围。
OS 矩阵:跨平台测试
如果你的项目可能在不同操作系统上运行(比如命令行工具),OS 矩阵就很有用。
jobs:
test:
runs-on: ${{ matrix.os }} # 动态指定操作系统
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18, 20]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm test
这里要注意几点:
平台差异:Windows 和 Linux 的文件路径不一样(\ vs /),某些命令行工具行为也不同。跨平台测试能帮你提前发现这些问题。
成本控制:GitHub Actions 对不同操作系统的收费不一样。Linux 免费 2000 分钟/月,Windows 是 Linux 的 2倍消耗,macOS 是 10 倍。如果你用 macOS Runner 测试,每月额度很快用完。
省钱策略:
- 只在必要时测 macOS(比如项目真的要在 macOS 上用)
- 把 macOS 测试放在单独的工作流,用
workflow_dispatch手动触发 - 公开仓库无限制,如果项目能公开,建议公开
性能改进技巧
Matrix 虽然能并行,但不是无限的。GitHub 默认会限制并行数,防止资源耗尽。你可以手动控制:
strategy:
max-parallel: 4 # 最多同时跑 4 个 Job
matrix:
node-version: [16, 18, 20, 22]
如果你的 Matrix 组合很多(比如 10+ 个 Job),设置 max-parallel 能避免一下子跑太多,减少对免费额度的消耗。
缓存命中加速:Matrix 里每个 Job 都有自己的缓存。setup-node 的 cache 会自动处理,不需要额外配置。主要点是 package-lock.json 和 node-version 都稳定的情况下,缓存命中率很高。
减少不必要步骤:有些步骤在 Matrix 里每个 Job 都跑,但其实没必要。比如代码检查(lint)通常不需要多版本测试:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run lint # lint 只在 Node 20 上跑一次
test:
needs: lint # lint 成功后才跑 test
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16, 18, 20]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm test
这样 lint 只跑一次,test 跑三个版本。效率更高。
实测数据:我的一个项目,不用 Matrix 时串行测 3 个版本要 12 分钟。用了 Matrix 并行跑,总时间 4 分钟(取决于最慢的一个版本)。省了 8 分钟,每天跑 10 次构建,一个月省下的时间够看一部电影了。
第四章:实战经验与故障排查
前面三章讲了怎么配置 CI 流水线,这章我整理了一份”速查清单”——安全、性能、故障排查三个维度。遇到问题时直接翻这里,能省不少时间。
安全实战清单
| 实践 | 说明 | 示例 |
|---|---|---|
| 显式声明 permissions | 不要依赖默认权限,明确声明需要什么权限 | permissions: { contents: read } |
| 使用 Secrets 存敏感信息 | API Key、SSH 密钥等不要硬编码 | ${{ secrets.API_KEY }} |
| 限制触发分支 | 不要在所有分支都跑 CI,只跑需要的 | branches: [main] |
| 用 SHA 引用 Action | 用具体 commit SHA 而非版本标签,防止被篡改 | actions/checkout@b4ffde65f46336ab88eb53be808477a39b6bc2b1 |
| 设置 timeout | 防止卡死浪费额度 | timeout-minutes: 15 |
最后一个很多人忽略:Action 的版本引用。官方 Action 通常用 @v4 这种标签,方便升级。但标签是可以被修改的——理论上有人可以把 @v4 指向恶意代码。用 SHA 引用(@b4ffde65f...)虽然升级麻烦,但更安全。对于生产环境的项目,建议用 SHA。
性能改进清单
| 技巧 | 效果 | 配置方式 |
|---|---|---|
| 启用依赖缓存 | 节省 50%+ 安装时间 | cache: 'npm' |
| 用 npm ci 而非 install | 安装更快、更可靠 | run: npm ci |
| 设置 timeout-minutes | 防止卡死浪费额度 | timeout-minutes: 15 |
| Matrix 并行执行 | 减少总时间 60%+ | strategy.matrix |
| 用 concurrency 取消重复构建 | 同一分支只跑最新的 | concurrency.group: ${{ github.ref }} |
concurrency 这个挺实用:你在同一个分支连续 push 了 5 次,默认会跑 5 个构建。加了 concurrency 配置后,前面 4 个会被取消,只跑最后一次。
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true # 取消正在跑的旧构建
常见问题速查表
| 错误信息 | 原因 | 解决方案 |
|---|---|---|
Permission denied | 权限不足 | 检查 permissions 配置,添加需要的权限 |
Cache not found | 缓存键不匹配 | 检查 cache key,确保 package-lock.json 没变 |
npm ERR! network | 网络超时 | 增加超时时间或用国内镜像 |
Out of memory | Node 内存不足 | 设置 NODE_OPTIONS=--max_old_space_size=4096 |
EACCES permission denied | 文件权限问题 | 在脚本开头加 chmod +x script.sh |
Error: Cannot find module | 依赖没装完 | 确保 npm ci 执行成功,检查错误日志 |
几个常见场景的处理方式:
网络超时:有时候 GitHub Runner 连 npm registry 很慢。可以在 .npmrc 里配置镜像:
- name: Configure npm registry
run: echo "registry=https://registry.npmmirror.com" > .npmrc
内存不足:大型项目构建时 Node 可能爆内存。加个环境变量:
env:
NODE_OPTIONS: --max_old_space_size=4096 # 给 Node 分配 4GB 内存
缓存不生效:第一次跑没缓存是正常的。确保 package-lock.json 存在(npm ci 需要锁文件),而且 setup-node 的 cache 参数跟你的包管理器匹配(npm/pnpm/yarn)。
结论
说了这么多,其实就几个核心点:一个 YAML 文件就能搭起 CI 流水线;权限声明遵循最小原则;环境变量分三层管理;依赖缓存能砍掉一半安装时间;Matrix 策略让多版本并行测试变得简单。
复制本文第一章的工作流模板,改改 Node 版本和项目命令,就能给你的项目加上 CI。先跑起来,再慢慢调整。建议你尝试一下 Matrix 策略,即使只测两个 Node 版本,也能体会到并行测试带来的效率变快——那种几个绿色勾同时出现的感觉,挺爽的。
如果你在使用 GitHub Actions 时遇到问题,可以在评论区留言。我会把常见问题补充到第四章的速查表里,帮助更多人少踩坑。
搭建 GitHub Actions CI 流水线
从零开始搭建一条完整的 CI 流水线,实现自动化构建和测试
⏱️ 预计耗时: 30 分钟
- 1
步骤1: 创建工作流目录
在项目根目录创建 `.github/workflows/` 目录,用于存放所有工作流配置文件。 - 2
步骤2: 编写基础 CI 配置
创建 `ci.yml` 文件,配置触发条件(push/PR)、权限(最小原则)、Job 步骤(checkout、setup-node、install、test、build)。 - 3
步骤3: 启用依赖缓存
在 `setup-node` 步骤添加 `cache: 'npm'` 参数,自动缓存 npm 依赖,加速后续构建。 - 4
步骤4: 配置 Matrix 多版本测试
添加 `strategy.matrix` 配置,指定要测试的 Node 版本列表(如 [16, 18, 20]),实现并行测试。 - 5
步骤5: 提交并观察构建结果
提交配置文件,push 到 GitHub,打开 Actions 页面查看构建状态和日志。
常见问题
GitHub Actions 每月免费额度是多少?
CI 工作流应该在哪些分支触发?
为什么推荐用 npm ci 而不是 npm install?
Matrix 策略能减少多少构建时间?
缓存为什么有时候不生效?
如何在 CI 中使用敏感信息(API Key、SSH 密钥)?
15 分钟阅读 · 发布于: 2026年4月6日 · 修改于: 2026年4月20日
相关文章
Nginx SSL/TLS 配置实战:从 HTTPS 证书到 A+ 安全加固
Nginx SSL/TLS 配置实战:从 HTTPS 证书到 A+ 安全加固
Supabase Edge Functions 实战:Deno 运行时与 TypeScript 开发指南
Supabase Edge Functions 实战:Deno 运行时与 TypeScript 开发指南
Docker 多阶段构建实战:生产镜像从 1GB 瘦身到 10MB

评论
使用 GitHub 账号登录后即可评论