Learn
GitHub Actions/18-real-world-ci-cd

实战:完整 CI/CD 流水线

恭喜你走到这里!前 17 章我们已经认识了事件触发、作业与步骤、矩阵、缓存、Artifacts、并发、环境审批与安全。本章要把它们全部串成一条线——以一个典型的 Node/前端项目为例,设计一条"接近生产"的 CI/CD 流水线,并给出一份逐段讲解的综合 workflow。

学完这一章,你手里就拿到了一张"从代码到上线"的完整地图。

1. 我们要解决什么:一条线的全景

先想清楚这条流水线要干什么。对一个前端项目来说,一条像样的流水线应当覆盖:

  1. 触发:有人 push、有人开 PR、或者手动触发时,就跑。
  2. CI(持续集成):拉代码、装依赖、lint、跑测试——而且是多版本矩阵地跑,确保兼容性。
  3. 构建:测试全过之后,打包出 dist/,作为 artifact 留存。
  4. 部署:只有 main 分支才允许部署;生产环境要人工审批;用 concurrency 防撞车;用 OIDC/secret 对接云。
  5. 多环境:staging 自动部署,production 审批后才部署。
  6. 发布:打 v* tag 时,自动出 GitHub Release。

下面我们一段一段把它落成 YAML。

2. 触发与权限:入口先定好

先从文件头开始:定义名称、触发条件,以及默认最小权限。

name: CI/CD Pipeline
 
on:
  push:
    branches: [main]
    tags: ['v*']
  pull_request:
  workflow_dispatch:
 
permissions:
  contents: read

要点对照前面的章节:

  • on.push.branches: [main] 与 on.pull_request:对应第 2 章"事件触发"——push 和 PR 都会启动流水线。
  • on.push.tags: ['v*']:为第 6 节的"发布"做准备,打版本 tag 时也要跑。
  • on.workflow_dispatch:允许你在 Actions 页面手动点一下就触发,方便运维紧急重跑。
  • permissions: contents: read:呼应第 17 章,入口就收窄,需要写权限的 job 再单独放开。
ℹ️为什么 tags 要用引号

'v*' 里的 * 是 YAML 里的特殊字符(别名标记),写在 Flow 风格里容易出问题。用单引号包成字符串是稳妥写法,避免 YAML 解析歧义。这类细节是"能跑"和"编译失败"的分界。

3. CI 阶段:矩阵跑 lint 与 test

CI 是整条线的地基。我们用 matrix(矩阵) 在 Node 18 / 20 / 22 三个版本上同时验证,再借助缓存加速 npm ci。

jobs:
  ci:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - name: 设置 Node 并启用 npm 缓存
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npm test

逐段对照:

  • strategy.matrix.node-version: [18, 20, 22]:第 7 章矩阵——同一个 job 自动展开成 3 个并行运行,覆盖多个 Node 版本。
  • actions/setup-node@v4 的 cache: npm:第 9 章缓存——自动按 package-lock.json 缓存 node_modules,大幅缩短 npm ci。
  • npm ci:比 npm install 更严格、更快,且要求 lock 文件存在,适合 CI。
  • lint 与 test:质量门禁,挂了就红灯,后面部署根本不会触发。
💡矩阵里也能加操作系统

想更狠一点,可以把 OS 也并进来:

strategy:
  matrix:
    node-version: [18, 20, 22]
    os: [ubuntu-latest, windows-latest]

这会展开成 3×2 = 6 个组合。注意不是所有项目都要跨 OS,按需开启即可,避免白白烧运行分钟数。

4. 构建:产出 artifact 留给部署

CI 全绿后,进入构建,并把产物上传为 artifact,供后续部署下载——这样部署 job 不必再构建一次,也更可复现。

  build:
    needs: ci
    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 build
      - name: 上传构建产物
        uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/

对照要点:

  • needs: ci:第 4 章作业依赖——CI 不过,构建不启动。
  • upload-artifact@v4 的 path: dist/:第 10 章 Artifacts——把打包结果存起来,部署阶段用同名 artifact 取回,保证"测的是这份、上的也是这份"。

5. 部署:if 守门 + 环境审批 + 并发控制

部署是整条线里最敏感的一步。我们用三道闸:

  1. if: 只让 main 分支部署(tag 触发时不重复部署)。
  2. environment: production 触发人工审批门禁(第 16 章)。
  3. concurrency: deploy 防止并发部署互相覆盖(第 16 章)。
  deploy:
    needs: build
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production
    concurrency: deploy
    permissions:
      id-token: write
      contents: read
    steps:
      - name: 下载构建产物
        uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/
      - name: 用 OIDC 换取云临时凭证
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
          aws-region: ap-east-1
      - run: aws s3 sync ./dist s3://my-bucket --delete

逐段对照:

  • if: github.ref == 'refs/heads/main':第 4 章条件执行——只有推到 main 才部署,feature 分支和 tag 触发都不会误上线。
  • environment: production + concurrency: deploy:第 16 章组合拳——生产部署需审批、且同一时刻只有一个。
  • permissions: id-token: write:第 17 章 OIDC——只在这个 job 放开换取云凭证所需的权限,其余 Job 仍是只读。
  • download-artifact:取回第 4 节上传的 dist/,部署的就是刚测过的同一份产物。
  • aws s3 sync:这里用 OIDC 思路对接云(实际字段以官方 action 为准),仓库里不存长期密钥。
⚠️if 与 environment 要配合使用

if 决定"要不要部署",environment 决定"部署前要不要审批"。二者职责不同:只写 if 不写 environment,生产部署就没有人工确认;只写 environment 不写 if,任何分支都能触发审批。把 if: github.ref == 'refs/heads/main' 与 environment: production 一起用,才同时守住了"分支"和"审批"两道门。

6. 多环境:staging 自动、production 审批

真实团队通常有多个环境。区别只在于:staging 自动部署、production 审批部署。我们可以用两个 job(或一个 job 两个 environment)来演示。

  deploy-staging:
    needs: build
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: staging        # 无审批,自动过
    concurrency: deploy-staging
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/
      - run: ./deploy.sh staging
 
  deploy-production:
    needs: deploy-staging       # 先过 staging,再上生产
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production     # 有审批门禁
    concurrency: deploy-production
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/
      - run: ./deploy.sh production

要点:deploy-production 用 needs: deploy-staging 形成环境升级链——staging 成功且生产审批通过后,才真正上线。每个环境用各自的 concurrency 组,互不干扰。

7. 发布:打 tag 自动出 Release

当开发者推送一个 v1.2.0 这样的 tag,我们希望自动生成 GitHub Release。这里用社区常用的 softprops/action-gh-release 一笔带过(以官方文档为准),注意它需要 contents: write。

  release:
    needs: build
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/
      - name: 创建 GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          files: dist/**

对照:if: startsWith(github.ref, 'refs/tags/v') 只在打 v* tag 时触发;permissions: contents: write 仅此 job 放开写权限(第 17 章最小权限)。

8. 综合 workflow 一览

把上面各段拼起来,就是一条接近生产的流水线(OIDC 与 release 的具体字段以官方 action 文档为准):

name: CI/CD Pipeline
 
on:
  push:
    branches: [main]
    tags: ['v*']
  pull_request:
  workflow_dispatch:
 
permissions:
  contents: read
 
jobs:
  ci:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npm test
 
  build:
    needs: ci
    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 build
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/
 
  deploy-staging:
    needs: build
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: staging
    concurrency: deploy-staging
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/
      - run: ./deploy.sh staging
 
  deploy-production:
    needs: deploy-staging
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production
    concurrency: deploy-production
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/
      - name: 用 OIDC 换取云临时凭证
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
          aws-region: ap-east-1
      - run: aws s3 sync ./dist s3://my-bucket --delete
 
  release:
    needs: build
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/
      - uses: softprops/action-gh-release@v2
        with:
          files: dist/**

这份 YAML 的每一块,都呼应了前面某一章:事件触发(第 2 章)、作业与步骤(第 3–4 章)、矩阵(第 7 章)、缓存(第 9 章)、Artifacts(第 10 章)、并发与环境审批(第 16 章)、安全与 OIDC(第 17 章)。

9. 小结:从事件到上线的完整链路地图

至此,整门课的知识点已经连成一张图:

push / PR / 手动  ──►  checkout 拉代码
                         │
                         ▼
                   setup-node + 缓存 npm + npm ci
                         │
                         ▼
              矩阵 lint & test(Node 18/20/22)
                         │ needs: ci
                         ▼
                    build → 上传 dist/ artifact
                         │ needs: build
            ┌────────────┼─────────────┐
            ▼            ▼             ▼
      deploy-staging  deploy-prod   release
      (自动,无审批)  (审批+并发+OIDC) (打 v* tag)

一句话收束:事件触发 → 拉代码/装环境 → 矩阵测试 → 构建 → 审批 → 部署 → (打 tag)发布。你已经掌握了设计一条生产级流水线所需的全部积木。

进阶学习路径建议

如果你想继续深入,下面几个方向最值得投入:

  • 自托管 Runner / Larger Runners:当公共 runner 不够快、或需要特定硬件/内网访问时,用自己托管的机器跑 job。
  • 可复用模板仓库(Reusable workflows):把通用流水线抽成 workflow_call 模板,多个仓库复用,避免复制粘贴。
  • 用 Actions 做 issue/PR 自动化:标签管理、自动回复、stale bot、自动合并等,把协作用 Git 也自动化起来。
  • 监控与告警:把流水线失败接到 Slack/飞书/邮件,配合 workflow_run 事件做状态汇总。

感谢你一路学完《GitHub Actions》这门课。从第一次 on: push 到写出一条带审批、带 OIDC、带矩阵的安全流水线,你已经具备把"自动化"真正落到团队生产里的能力。去把你的项目接上 Actions 吧,实践是巩固这些知识最好的方式。祝发布顺利,永不失联!

10. 练习

🎯练习

1. 在上面的综合 workflow 里,如果想"只在 push 到 main 且不是 tag 触发时才部署",deploy-production 的 if: 该怎么写?(提示:github.ref 与 github.ref_type)

2. 为什么 build job 上传的 artifact 要在 deploy job 里重新 download-artifact,而不是让 deploy 自己再 npm run build 一次?

3. 综合 workflow 中,release job 的 permissions 为什么必须放开成 contents: write,而顶层却写的是 contents: read?

4. 如果要新增一个 deploy-staging 到 deploy-production 之间的"回归测试"job,且要求它只在 staging 部署成功后、生产审批前运行,请写出它与相邻 job 的 needs 关系。