阶段一 · CI 流水线与制品

GitHub Actions 实战

一句话总结

GitHub Actions 用一个 YAML 文件,把第 01 篇讲的"流水线 / 阶段 / 质量门禁"变成真实可跑的自动化;
掌握它的核心就是理清事件触发 → Job → Step这条主线,再叠加缓存,矩阵,Secret,审批等加速与安全机制.

前置回顾

第 01 篇我们建立了词汇:流水线由阶段(Stage)串成,阶段间有质量门禁,设计原则是快速失败.本篇就是把这些抽象概念,落到一个具体工具上跑起来.读的时候不妨随时对照:这段 YAML 对应的是 01 里的哪个概念?

为什么从 GitHub Actions 入手

CI/CD 工具有很多:Jenkins,GitLab CI,CircleCI,GitHub Actions...选 GitHub Actions 作为入门,理由很实际:

  • 零搭建:不用自己装服务器(对比 Jenkins 要维护一台机器),代码托管在 GitHub 上就能直接用
  • 声明即配置:整条流水线就是仓库里一个 YAML 文件,跟代码一起版本化,一起 review
  • 生态丰富:别人写好的步骤(Action)可以直接复用,像搭积木

更重要的是,它的核心模型(事件触发,Job,Step,缓存,矩阵)在所有 CI 工具里都是相通的.学会它,换到 GitLab CI 也只是语法差异.

核心模型:四层结构

GitHub Actions 的概念有四层,从大到小嵌套.先建立这个骨架,后面所有语法都往里填:

┌────────────────────────────────────────────┐
│ Event 事件<br/>(push / pull_request / 定时...) │
└────────────────────────────────────────────┘
│ 触发

┌───────────────────────────────┐
│ Workflow 工作流<br/>(一个 .yml 文件) │
└───────────────────────────────┘


┌────────────────────────┐
│ Job A<br/>(在一台全新虚拟机上跑) │
└────────────────────────┘


┌──────────────────────┐
│ Job B<br/>(默认与 A 并行) │
└──────────────────────┘
┌──────────────┐
│ Step 1: 检出代码 │
└──────────────┘
┌─────────────┐
│ Step 2: 装依赖 │
└─────────────┘
┌─────────────┐
│ Step 3: 跑测试 │
└─────────────┘
概念 是什么 对应 01 篇
Event 事件 触发流水线的动作:push,提 PR,定时,手动点按钮 "自动触发"
Workflow 工作流 一条完整流水线,一个 YAML 文件,放在 .github/workflows/ 流水线(Pipeline)
Job 作业 一组 Step,跑在一台** 全新的**虚拟机(Runner)上;Job 间默认并行 阶段的载体
Step 步骤 Job 里的一条具体命令或一个复用的 Action 阶段内的动作

最反直觉的一点:每个 Job 都是一台全新的空白机器

Job A 装好的依赖,编译出的文件,Job B 看不到--因为它们在不同的虚拟机上.这解释了后面两件事:

  1. 为什么需要缓存(每次都从零装依赖太慢)
  2. 为什么 Job 之间传文件要用专门的artifacts 上传/下载机制

记住"Job = 一次性的干净环境",很多设计就讲得通了.

第一个 Workflow:看懂每一行

下面是一个最小但完整的 CI 工作流,假设是个 Node.js 项目.逐行注释:

name: CI                          # 工作流名称(显示在 GitHub Actions 页面)

on: # ← Event:什么时候触发
push:
branches: [main] # 推送到 main 分支时
pull_request: # 任何 PR 时
branches: [main]

jobs:
test: # ← Job 的 id(自定义)
runs-on: ubuntu-latest # 在哪种 Runner 上跑(GitHub 提供的虚拟机)
steps: # ← Step 列表,从上到下顺序执行
- uses: actions/checkout@v4 # 复用官方 Action:把代码检出到 Runner
- uses: actions/setup-node@v4
with: # 给 Action 传参数
node-version: '20'
- run: npm ci # 自己写的 shell 命令:装依赖
- run: npm test # 跑测试 -- 这就是 01 篇说的"质量门禁"

把这个文件放到仓库的 .github/workflows/ci.yml,push 一下,GitHub 就会自动跑起来.两种 Step 写法值得记住:

  • uses: -- 复用别人写好的 Action.actions/checkout@v4 表示用官方的 checkout,版本锁 v4
  • run: -- 直接执行 shell 命令,跟你在终端敲的一样

uses调用现成的库函数(别人封装好的能力),run自己写一行代码.一个 Job 就是这两种 Step 的混搭.

控制 Job 的顺序与依赖

默认情况下,一个 Workflow 里的多个 Job 并行 跑(为了快).但有时有先后依赖--比如"必须先测试通过,再构建镜像".用 needs 声明依赖:

jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo "跑测试"

build:
needs: test # ← 等 test 成功后才开始;test 失败则 build 直接跳过
runs-on: ubuntu-latest
steps:
- run: echo "构建镜像"

lint:
runs-on: ubuntu-latest # 没写 needs → 与 test 并行
steps:
- run: echo "静态检查"

这正好对应第 01 篇的"快速失败":testlint 这种又快又能拦问题的检查并行先跑,build 这种耗时的放在它们后面,前面挂了就根本不浪费时间构建.

缓存:别每次都从零装依赖

前面说过每个 Job 是全新机器,意味着每次都要重新 npm ci / mvn dependency 下载一堆依赖,慢且浪费.缓存 把这些不常变的东西存起来,下次直接复用:

steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # setup-node 内置缓存,自动缓存 node_modules 相关
- run: npm ci
- run: npm test

缓存的工作原理是按 key 命中:用依赖清单文件(如 package-lock.json)的哈希当 key,清单没变就命中缓存,跳过下载;清单一变(加了新依赖)key 就变,自动重新下载并更新缓存.

缓存 vs 制品:别搞混

两者都是"存东西",但用途相反:
缓存(cache)跨工作流运行复用的,可重建的中间产物(依赖包),目的是提速;
制品(artifacts)本次运行要保留或在 Job 间传递的产出(测试报告,构建好的二进制),目的是留存与交付.
一句话:缓存是"为了快,丢了也能重建",artifacts 是"这是成果,不能丢".

矩阵构建:一份配置,多种组合

假设你的库要同时支持 Node 18,20,22 三个版本,还要在 Linux 和 Windows 上都验证.难道写 6 个几乎一样的 Job?矩阵(Matrix) 让你一份配置自动展开成多个并行 Job:

jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 20, 22]
# ↑ 自动展开成 2 × 3 = 6 个并行 Job,每个组合各跑一次
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm ci
- run: npm test

${{ ... }} 是 GitHub Actions 的表达式语法,用来引用变量.matrix.os 在每个展开的 Job 里取到不同的值.矩阵是"用并行换覆盖面"的典型手段--6 个组合同时跑,总耗时约等于跑一个.

Secret 与 OIDC:不要把密钥写进代码

流水线常常需要敏感信息:推镜像要登录凭证,部署要云账号密钥.绝对不能 把这些明文写进 YAML(YAML 是进仓库的,等于公开).

用 Secret 注入密钥

在 GitHub 仓库的 Settings → Secrets 里配置好密钥后,在 YAML 里用 secrets 上下文引用:

steps:
- run: docker login -u user -p ${{ secrets.REGISTRY_TOKEN }}
# REGISTRY_TOKEN 存在 GitHub 后台,日志里会被自动打码成 ***

更好的方式:OIDC 免密钥

长期存放的密钥本身就是风险:会泄露,要轮换,忘了删.更现代的做法是 OIDC(OpenID Connect)--GitHub Actions 运行时,临时向云厂商出示一张"我确实是某仓库某分支触发的"的短期身份令牌,云厂商验证后发放临时凭证,用完即失效.

优先用 OIDC 而非长期密钥

OIDC 的本质是从"持有长期密码"转向"按需换取临时通行证".没有长期密钥躺在配置里,自然就没有泄露和轮换的烦恼.各大云厂商(AWS / GCP / Azure)和 GitHub Actions 都原生支持.这与第 04 篇的"供应链安全"是同一种思路:减少静态机密的存在面.

环境与审批:守住生产那道门

回到第 01 篇的关键区分:持续交付持续部署的差别,就在生产发布前那道人工审批.GitHub Actions 用 Environment(环境) 来实现这道门:

jobs:
deploy-prod:
runs-on: ubuntu-latest
environment: production # ← 绑定名为 production 的环境
steps:
- run: ./deploy.sh

在 GitHub 后台给 production 环境配置保护规则(protection rules)后,这个 Job 跑到时会暂停并等待指定的人点"Approve",批准后才继续.这就把抽象的"人工审批关卡"变成了一个真实的按钮.

同一条流水线,加不加这道门决定了你是"交付"还是"部署":

... → 部署 staging(自动) → [environment: production] → 部署 prod

配了审批规则 → ⏸ 等人点 Approve = 持续交付
没配审批规则 → 直接放行 = 持续部署

可复用 Workflow:别在每个仓库复制粘贴

当你有十几个微服务,每个都需要几乎一样的"测试 + 构建 + 推镜像"流水线,复制粘贴会变成维护噩梦--改一处要改十几个文件.可复用 Workflow(Reusable Workflow) 把公共流程抽出来,各仓库调用它:

# 公共仓库里:.github/workflows/reusable-ci.yml
on:
workflow_call: # ← 声明"我是被调用的",而非被事件触发
inputs:
node-version:
required: true
type: string
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: echo "用 Node ${{ inputs.node-version }} 构建"
# 业务仓库里:调用上面的可复用 workflow
jobs:
ci:
uses: my-org/shared/.github/workflows/reusable-ci.yml@main
with:
node-version: '20'

这本质上是把"流水线"也做了函数封装--定义一次,处处调用,改一处生效全部.和第 06 篇 Helm 模板化配置的动机如出一辙:消除重复,集中维护.

快速回顾

  • 四层模型:Event 触发 → Workflow(一个 YAML)→ Job(全新虚拟机,默认并行)→ Step(uses 复用 / run 命令)
  • needs 声明 Job 依赖,把快检查放前面实现"快速失败"
  • 缓存按依赖清单哈希命中,加速;它与**制品(artifacts)**用途不同,前者求快,后者求留存
  • 矩阵一份配置展开成多组合并行,用并行换覆盖面
  • Secret 注入密钥不进代码;更优解是 OIDC 临时凭证,免长期密钥
  • Environment + 审批规则实现生产发布前的人工门--决定了你是持续交付还是持续部署
  • 可复用 Workflow 把公共流水线封装成"函数",消除多仓库重复

Action 后面的 @v4 一定要写吗?能用 @main 吗?

强烈建议锁版本(@v4 甚至锁到具体 commit SHA).用 @main 意味着别人一更新,你的流水线行为就可能悄悄变化甚至被注入恶意代码--这是真实存在的供应链攻击面(第 04 篇详谈).锁版本让构建可复现,这正是第 01 篇"确定,可复现"的要求.

Runner 是什么?要自己准备机器吗?

Runner 就是真正执行 Job 的机器.GitHub 提供托管 Runner(runs-on: ubuntu-latest),用完即销毁,无需你维护.如果有特殊需求(内网访问,特定硬件,更强算力),也可以挂自托管 Runner(自己的机器),但要自己负责安全和维护.入门用托管的即可.

动手练习

  1. 创建首个 Workflow:给一个自己的 GitHub 仓库新建 .github/workflows/ci.yml,实现 push 时自动跑测试,观察 Actions 页面的执行过程.
  2. 验证质量门禁:故意写一个会失败的测试,push,确认流水线变红并阻止--亲眼看到"质量门禁"生效.
  3. 体验矩阵构建:加一个矩阵,让测试在两个语言版本上并行跑,观察是否真的展开成多个并行 Job.
  4. 验证快速失败:把 testbuildneeds 串起来,故意让 test 失败,确认 build 被跳过而非照常执行.
  5. 体验审批门(进阶):配一个 environment: production 的部署 Job 并加审批规则,体验"流水线停下来等你点批准"的感觉.