切换语言
切换主题

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 流水线的骨架,配置得当能让你的工作流更安全、更高效。

触发器:什么时候运行

触发器决定了工作流什么时候启动。最常用的两个:pushpull_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,builddeployNODE_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-nodecache 会自动处理,不需要额外配置。主要点是 package-lock.jsonnode-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 memoryNode 内存不足设置 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-nodecache 参数跟你的包管理器匹配(npm/pnpm/yarn)。

结论

说了这么多,其实就几个核心点:一个 YAML 文件就能搭起 CI 流水线;权限声明遵循最小原则;环境变量分三层管理;依赖缓存能砍掉一半安装时间;Matrix 策略让多版本并行测试变得简单。

复制本文第一章的工作流模板,改改 Node 版本和项目命令,就能给你的项目加上 CI。先跑起来,再慢慢调整。建议你尝试一下 Matrix 策略,即使只测两个 Node 版本,也能体会到并行测试带来的效率变快——那种几个绿色勾同时出现的感觉,挺爽的。

如果你在使用 GitHub Actions 时遇到问题,可以在评论区留言。我会把常见问题补充到第四章的速查表里,帮助更多人少踩坑。

搭建 GitHub Actions CI 流水线

从零开始搭建一条完整的 CI 流水线,实现自动化构建和测试

⏱️ 预计耗时: 30 分钟

  1. 1

    步骤1: 创建工作流目录

    在项目根目录创建 `.github/workflows/` 目录,用于存放所有工作流配置文件。
  2. 2

    步骤2: 编写基础 CI 配置

    创建 `ci.yml` 文件,配置触发条件(push/PR)、权限(最小原则)、Job 步骤(checkout、setup-node、install、test、build)。
  3. 3

    步骤3: 启用依赖缓存

    在 `setup-node` 步骤添加 `cache: 'npm'` 参数,自动缓存 npm 依赖,加速后续构建。
  4. 4

    步骤4: 配置 Matrix 多版本测试

    添加 `strategy.matrix` 配置,指定要测试的 Node 版本列表(如 [16, 18, 20]),实现并行测试。
  5. 5

    步骤5: 提交并观察构建结果

    提交配置文件,push 到 GitHub,打开 Actions 页面查看构建状态和日志。

常见问题

GitHub Actions 每月免费额度是多少?
私有仓库每月免费 2000 分钟(Linux Runner),公开仓库无限制。Windows Runner 消耗是 Linux 的 2 倍,macOS 是 10 倍。
CI 工作流应该在哪些分支触发?
建议只在 main/master 分支和 PR 指向 main 时触发。开发分支可以跳过 CI,节省资源。用 `paths` 过滤器排除文档变更。
为什么推荐用 npm ci 而不是 npm install?
npm ci 更快、更可靠:它严格按照 package-lock.json 安装,不修改锁文件,适合 CI 环境。npm install 可能更新依赖版本,导致不确定的构建结果。
Matrix 策略能减少多少构建时间?
实测数据:测试 3 个 Node 版本,串行需要 12 分钟,Matrix 并行只需 4 分钟(取决于最慢的一个版本),减少 60%+ 时间。
缓存为什么有时候不生效?
缓存基于 `package-lock.json` 的哈希值。如果锁文件变了,缓存失效。第一次跑没缓存是正常的。确保 `cache` 参数跟包管理器匹配(npm/pnpm/yarn)。
如何在 CI 中使用敏感信息(API Key、SSH 密钥)?
使用 GitHub Secrets:在仓库设置里添加密钥,工作流里用 `${{ secrets.KEY_NAME }}` 引用。Secrets 在日志里自动隐藏,不会泄露。

15 分钟阅读 · 发布于: 2026年4月6日 · 修改于: 2026年4月20日

评论

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