#DevOps

GitHub Actions CI/CD 实战

GitHub Actions 是开发者最易上手的 CI/CD,但写好也有讲究。本文讲清 workflow 结构、触发器、job/step/matrix、缓存与 artifact、密钥管理、并发与成本,以及一份"前端 + 后端 + 部署"的实战 workflow,让 CI 既快又稳又不烧额度。

本文要点

  • 主题聚焦:DevOps
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2026年7月2日·更新于 2026年7月23日

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(可复用的步骤单元)。

yaml
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。

yaml
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 时发版。

yaml
- 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 缓存,开个参数就行:

yaml
- uses: actions/setup-node@v4
  with:
    node-version: '20'
    cache: 'npm'           # 自动缓存 ~/.npm

pnpm 项目要手动配一下:

yaml
- 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 极其隐蔽,你会在某次升级依赖后莫名其妙地测到旧版本的代码:

yaml
- 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 拉过去部署:

yaml
- 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 命令的字符串里:

yaml
- name: Deploy
  env:
    DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
  run: ./deploy.sh   # 脚本内部读 $DEPLOY_KEY,日志里会自动打码

环境(environments) 是进阶武器。给 production 看护一套更严的 secret,并要求人工审批、限制分支:

yaml
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 应该被取消,只跑最新的:

yaml
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

下面这份是我现在的标配,质量检查 + 测试 + 构建部署,带成本控制。删掉敏感信息直接能用:

yaml
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

相关推荐