概述
GitHub Actions 是 GitHub 原生提供的 CI/CD(持续集成/持续部署)平台,它让你在仓库中定义自动化流程,在代码推送、PR 合并、发布版本等事件发生时自动执行构建、测试、部署等任务。
核心概念
GitHub Actions 围绕几个核心概念构建,理解它们是掌握后续所有操作的基础。
| 概念 | 类比 | 说明 |
|---|---|---|
| Workflow(工作流) | 流水线图纸 | 一个完整的自动化流程,定义在 .github/workflows/*.yml 文件中 |
| Job(任务) | 流水线上的工位 | Workflow 中的一个任务单元,多个 Job 可并行或串行执行 |
| Step(步骤) | 工位上的每个操作 | Job 中的每一步,可以是运行一条 shell 命令,也可以引用一个 Action |
| Action(动作) | 预制的工具/机器 | 可复用的独立功能块,像积木一样在 Step 中组装使用 |
| Runner(运行器) | 流水线工人 | 执行 Workflow 的服务器,由 GitHub 托管或自行管理 |
| Event(触发器) | 启动按钮 | 触发 Workflow 执行的事件——push 代码、创建 PR、发布版本、定时任务等 |
为什么要学习 GitHub Actions?
- 免运维:无需搭建 Jenkins 服务器,GitHub 原生托管,开箱即用
- 生态丰富:GitHub Marketplace 上有上万现成的 Action,无需重复造轮子
- 与仓库深度集成:PR 状态自动关联、Checks 界面原生展示,协作体验无缝
- 灵活的触发模型:不限于 push/PR,定时、手动、仓库事件、外部 webhook 皆可触发
- 矩阵构建:一行配置即可在多个 OS 和语言版本上并行测试
- 免费额度慷慨:公开仓库完全免费,私有仓库每月 2000 分钟免费
GitHub Actions 与同类工具对比
| 工具 | 托管方式 | 配置格式 | 学习曲线 | 生态规模 |
|---|---|---|---|---|
| GitHub Actions | 云托管(GitHub) | YAML | 低 | ⭐⭐⭐⭐⭐ |
| Jenkins | 自托管为主 | Groovy / UI | 高 | ⭐⭐⭐⭐ |
| GitLab CI | 云托管 / 自托管 | YAML | 中 | ⭐⭐⭐ |
| CircleCI | 云托管 | YAML | 中 | ⭐⭐⭐ |
前置准备
环境要求
| 环境 | 说明 |
|---|---|
| Git | 本地已安装,用于代码推送 |
| GitHub 账号 | 注册于 github.com |
| 一个 Git 仓库 | 本次实验使用 D:\Projects\test,需推送至 GitHub 远端 |
项目结构约定
D:\Projects\test ← 实验仓库根目录
├── .github/
│ └── workflows/ ← 所有 Workflow 文件存放在此(YAML 格式)
├── src/ ← 实验用测试代码
└── ...
注意:
.github/workflows/是 GitHub Actions 的固定目录,只要将.yml文件放入此目录并推送到 GitHub,Actions 就会自动识别并执行。
Workflow 文件的基本骨架
无论多复杂的流水线,它的起点都是这样一个 YAML 文件:
# .github/workflows/文件名.yml
name: Workflow 名称 # 显示在 GitHub Actions 界面上的名称
on: push # 触发事件jobs:job-id: # Job 的唯一标识runs-on: ubuntu-latest # 运行环境steps:- run: echo "Hello World" # 一条 shell 命令就是一个 Step
后面所有章节的实战,都是在这个骨架基础上不断添加功能。
第一部分:Hello World —— 第一个 Workflow
万事开头难,但 GitHub Actions 的 Hello World 出奇地简单。你只需要一个 YAML 文件,推送到仓库,剩下的交给 GitHub。
1. 先看最小的 Workflow 长什么样
创建一个文件 .github/workflows/hello.yml,写入:
name: Hello World
on: [push]jobs:say-hello:runs-on: ubuntu-lateststeps:- run: echo "Hello World!"
只有 9 行。推送这个文件到 GitHub,Actions 就会自动执行。来,逐行拆解每个关键词的意思:
| 行 | 关键词 | 什么意思 |
|---|---|---|
| 1 | name |
给你的流水线起个名字,显示在 GitHub 的 Actions 页面上 |
| 2 | on |
触发器——什么时候开始跑?[push] = 一推送代码就跑 |
| 4 | jobs |
一个 Workflow 可以包含多个 任务,这里只定义一个 |
| 5 | say-hello |
任务 ID,自己取的名字,后面可以用它做依赖 |
| 6 | runs-on |
运行环境——用什么机器跑?ubuntu-latest = Ubuntu 最新版 |
| 7 | steps |
这个任务的所有步骤列表 |
| 8 | run |
执行一条 shell 命令,这里就是 echo "Hello World!" |
关键规律:缩进 + 冒号 + 短横线。YAML 语法就这三个结构。上面 9 行,你已经看懂了 80% 的 Workflow 语法。
2. 理解每个部分在干什么
把上面那个 9 行的 Workflow 翻译成大白话:
name: "这条流水线叫 Hello World"
on: "什么时候跑?——代码推送到仓库时"jobs:say-hello: "我要定义一个叫 say-hello 的任务"runs-on: "用 Ubuntu 的最新版系统来跑"steps: "这个任务分几步走"- run: "第一步:在终端里执行 echo 'Hello World!'"
3. 给它加一个关键动作:检出代码
上面那个 Workflow 有个问题——它没有代码。Runner 上只是一个空目录。
加一行 uses 来引入 GitHub 上现成的工具:
steps:- uses: actions/checkout@v4 # ← 新加的一行- run: ls -la # 现在能列出仓库的文件了
actions/checkout@v4 是 最常用的 Action,作用就是把代码下载到 Runner 上。没有它,后面所有命令都找不到你的文件。
| 参数 | 含义 |
|---|---|
uses |
引用一个现成的 Action,相当于安装了一个工具 |
actions/checkout@v4 |
官方提供的"代码检出"工具,@v4 是版本号 |
4. 再了解一个概念:${{ }} 上下文变量
GitHub Actions 会自动给你提供一些信息,比如谁触发的、什么分支。用 ${{ }} 这个语法来读取它们:
steps:- run: echo "触发者是 ${{ github.actor }}"- run: echo "当前分支是 ${{ github.ref_name }}"
常见的上下文变量
| 写法 | 输出什么 | 例子 |
|---|---|---|
${{ github.actor }} |
谁推送的代码 | PC2005-cloud |
${{ github.repository }} |
仓库全名 | PC2005-cloud/test |
${{ github.ref_name }} |
分支名 | master |
${{ github.sha }} |
这次提交的 ID | b8b2ac7... |
5. 组合起来:我们最终推送的版本
把上面的知识点拼在一起,就是完整的 Hello World Workflow:
name: Hello World
on: [push]jobs:say-hello:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4 # 1. 拉代码- run: echo "🎉 Hello World!" # 2. 打招呼- run: echo "触发者: ${{ github.actor }}" # 3. 谁触发的- run: echo "分支: ${{ github.ref_name }}" # 4. 哪个分支- run: ls -la # 5. 看看有啥文件
6. 看看实际跑出来的效果
把这个文件推送到 GitHub,你会在仓库的 Actions tab 看到这样的输出:
🎉 Hello World! 第一个 GitHub Actions Workflow 触发了!
触发者: PC2005-cloud
分支: master
提交 SHA: b8b2ac79fee7f71de5b8e05ce5b7862b30d99b0d仓库文件列表:
total 24
drwxr-xr-x .git
drwxr-xr-x .github
-rw-r--r-- .gitignore
-rw-r--r-- README.md
7. 小结:这一部分你学会了
| 概念 | 一句话记住 |
|---|---|
name |
Workflow 的名字 |
on: [push] |
推送代码就跑 |
runs-on |
用什么系统跑 |
uses |
引用现成的 Action 工具 |
run |
执行 shell 命令 |
${{ }} |
读取 GitHub 提供的变量 |
🎉 Hello World! 第一个 GitHub Actions Workflow 触发了!
触发者: PC2005-cloud
仓库: PC2005-cloud/test
分支: master
提交 SHA: b8b2ac79fee7f71de5b8e05ce5b7862b30d99b0d仓库文件列表:
total 24
drwxr-xr-x 4 runner runner 4096 .git
drwxr-xr-x 3 runner runner 4096 .github
-rw-r--r-- 1 runner runner 99 .gitignore
-rw-r--r-- 1 runner runner 387 README.md
运行总览
| 指标 | 值 |
|---|---|
| Runner 镜像 | ubuntu-24.04 |
| Runner 版本 | 2.336.0 |
| 总耗时 | 14 秒 |
| Job 状态 | ✅ success |
注意:Runner 的工作目录是
/home/runner/work/<仓库名>/<仓库名>/,每次运行都是全新的环境,运行结束后自动销毁。
6. 小结
第一个 Workflow 虽然简单,但已经包含了 GitHub Actions 的全部核心要素:
- ✅ 一个 YAML 文件定义整个流程
- ✅ 一个 Event(
push)触发执行 - ✅ 一个 Job 在
ubuntu-latest上运行 - ✅ 多个 Step,包含
uses(引用 Action)和run(执行命令) - ✅ GitHub Context 变量注入运行时的动态信息
这就是后续所有复杂 Workflow 的起点。掌握了这个骨架,后面的每一章都是在它之上做加法。
第二部分:触发器实战
触发器就是 Workflow 的"启动按钮"——告诉 GitHub 什么时候开始跑。GitHub 支持几十种触发方式,但最常用的只有三个。
1. 最简单的触发器:on: [push]
你应该还记得第一部分的这个写法:
on: [push] # 只要有代码推送,就跑
方括号 [push] 表示"事件列表",你也可以写成一行多个事件:
on: [push, pull_request] # 推送 或 PR 都触发
2. 让它只在你想要的分支触发
不加限制的话,任何分支的推送都会触发。加个 branches 过滤一下:
on:push:branches: [master] # 只有 master 分支推送才触发
翻译成大白话:
on: "什么时候跑?"push: "有人推送代码时"branches: [master] "但只有推的是 master 分支才跑"
常用的过滤参数
| 参数 | 意思 | 例子 |
|---|---|---|
branches |
只在这些分支触发 | [master, develop] |
branches-ignore |
这些分支不触发 | [gh-pages] |
paths |
只改这些文件才触发 | [src/**] |
paths-ignore |
改这些文件不触发 | [**.md] |
为什么要过滤?如果只改了 README 也要跑一遍 30 秒的 CI,就是浪费资源和时间。
3. 第二个触发器:Pull Request
代码审查是团队协作的核心,每次有人开 PR 或更新 PR,自动跑一次检查:
on:pull_request:branches: [master] # 目标分支是 master 的 PR 才触发
Push 和 PR 有什么区别?
| 触发方式 | 什么时候触发 | 典型用途 |
|---|---|---|
push |
你推送代码到仓库 | 提交后验证代码能不能编译通过 |
pull_request |
你开 PR 或给 PR 加新提交 | 合并前检查新代码有没有问题 |
安全提示:PR 触发器在 fork 仓库场景下 Token 只有只读权限,无法访问 Secrets,这是为了防止恶意 PR 窃取你的敏感信息。
4. 第三个触发器:手动触发
适合"按需执行"的场景——比如部署、数据迁移、清理任务。不需要推送代码,在浏览器里点个按钮就能跑:
on:workflow_dispatch: # 手动触发inputs:environment: # 定义一个输入项description: '部署到哪个环境?'required: truedefault: 'staging'type: choiceoptions:- staging- production
手动触发后,在 Actions 页面点击 Run workflow → 选择参数 → 执行。
┌──────────────────────────────────┐
│ Run workflow │
│ │
│ 部署到哪个环境? [staging ▼] │ ← choice 选项
│ │
│ [✓] 启用调试模式 │ ← boolean 复选框
│ │
│ [Run workflow] │
└──────────────────────────────────┘
5. 三个触发器放在一起
一个 Workflow 可以同时监听多个事件:
on:push:branches: [master] # 推送 master 触发pull_request:branches: [master] # PR 发向 master 触发workflow_dispatch: # 手动触发inputs:environment:type: choiceoptions: [staging, production]
小提示:多种触发方式组合时,用
${{ github.event_name }}来区分是哪个事件触发的。下面会讲。
6. 区分不同的事件:if 条件
Workflow 里的每一步可以通过 if 控制"什么时候执行":
steps:- name: 只看 push 事件的信息if: github.event_name == 'push' # 只有 push 触发时才执行run: |echo "提交者: ${{ github.event.head_commit.author.name }}"echo "提交信息: ${{ github.event.head_commit.message }}"- name: 只看 PR 事件的信息if: github.event_name == 'pull_request' # 只有 PR 触发时才执行run: |echo "PR 标题: ${{ github.event.pull_request.title }}"- name: 只看手动触发的参数if: github.event_name == 'workflow_dispatch'run: echo "环境: ${{ github.event.inputs.environment }}"
常用的条件写法
| 写法 | 含义 |
|---|---|
if: github.event_name == 'push' |
事件类型是 push |
if: github.ref == 'refs/heads/master' |
分支是 master |
if: github.actor != 'dependabot[bot]' |
排除机器人提交 |
if: success() |
上一步成功才执行(默认行为) |
if: failure() |
上一步失败了才执行 |
if: always() |
不管成功失败都执行 |
7. 完整 Workflow
把所有触发器整合到一个文件里,通过 if 区分不同事件:
name: 触发器实战
on:push:branches: [master, develop]paths-ignore: ['**.md']pull_request:branches: [master]workflow_dispatch:inputs:environment:description: '目标环境'required: truedefault: 'staging'type: choiceoptions: [staging, production]jobs:show-trigger:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4- run: echo "📡 触发事件: ${{ github.event_name }}"- if: github.event_name == 'push'run: |echo "分支: ${{ github.ref_name }}"echo "提交者: ${{ github.event.head_commit.author.name }}"- if: github.event_name == 'pull_request'run: |echo "PR 标题: ${{ github.event.pull_request.title }}"- if: github.event_name == 'workflow_dispatch'run: |echo "环境: ${{ github.event.inputs.environment }}"
8. 来看看实际输出
Push 触发(推送代码后自动跑)
📡 触发事件: push
分支: master
提交者: pc2005
提交信息: feat: 添加触发器实战 Workflow
手动触发(在 Actions 页面点按钮)
📡 触发事件: workflow_dispatch
环境: production
调试模式: true
9. 小结
| 你学到的 | 一句话记住 |
|---|---|
on: [push] |
推送就跑 |
branches: [master] |
指定分支才跑 |
paths-ignore |
某些文件改了不跑 |
pull_request |
PR 开了就跑 |
workflow_dispatch |
点按钮就跑 |
if: |
控制哪一步在什么条件下跑 |
第三部分:Job 依赖与矩阵构建
一个真实的 CI 流程通常是:先做代码检查 → 跑测试 → 构建产物 → 发通知。这些任务不可能同时跑,但也不能一个一个等——这就需要理解 Job 之间的依赖和并行关系。
1. 先看两个 Job 怎么串起来
默认情况下,Workflow 里的多个 Job 是同时跑的。但如果你想让 Job B 等 Job A 跑完再跑,就用 needs:
jobs:job-a: # Job A 先跑steps:- run: echo "我是 A"job-b:needs: job-a # 等 job-a 完成后才跑steps:- run: echo "我是 B,A 跑完了才轮到我"
翻译成大白话:
jobs:lint: "一个叫 lint 的任务,先跑"test:needs: lint "test 任务依赖 lint,等它跑完再跑"build:needs: test "build 依赖 test,等 test 跑完再跑"
2. 写一个实际的链式流水线
jobs:lint: # 1. 代码检查steps:- run: echo "🔍 检查代码风格..."- run: echo "检查通过 ✅"build: # 2. 构建(等 lint 完成后)needs: lintsteps:- run: echo "📦 构建项目..."- run: echo "构建完成 ✅"notify: # 3. 通知(等 build 完成后)needs: buildsteps:- run: echo "📬 全部完成!"
执行顺序:
lint ──→ build ──→ notify
(1) (2) (3)
3. 依赖写法小结
| 写法 | 意思 |
|---|---|
不加 needs |
和其他无依赖的 Job 同时跑 |
needs: lint |
等 lint 这一个 Job |
needs: [lint, test] |
等 lint 和 test 都完成 |
注意:如果依赖的 Job 失败了,当前 Job 默认不会执行。可以用
if: always()让它在失败时也执行(比如发通知)。
4. 矩阵:一份配置,跑 N 遍
现在的软件经常需要在多个操作系统、多个语言版本上测试。如果为每个组合都写一个 Job,你会复制出大量重复代码。
矩阵就是用来解决这个问题的:
jobs:test:strategy:matrix:os: [ubuntu-latest, windows-latest] # 2 个 OSnode: [18, 20] # 2 个 Node 版本runs-on: ${{ matrix.os }} # 运行时动态指定steps:- run: echo "跑 ${{ matrix.os }} + Node ${{ matrix.node }}"
拆开看里面发生了什么:
matrix:os: [ubuntu, windows] ← 这一列有 2 个值node: [18, 20] ← 这一列也有 2 个值组合结果 = os × node = 2 × 2 = 4 个 Job 并行跑:Job 1: ubuntu + Node 18Job 2: ubuntu + Node 20Job 3: windows + Node 18Job 4: windows + Node 20
没有矩阵,你得写 4 个几乎一样的 Job;有了矩阵,写 1 个 Job 就够了。
5. 矩阵的几个参数
strategy:matrix:os: [ubuntu-latest, windows-latest]node: [18, 20]fail-fast: false # 一个组合失败了,其他的继续跑max-parallel: 2 # 最多同时跑 2 个(防止把额度跑光)
| 参数 | 默认值 | 说明 |
|---|---|---|
fail-fast |
true |
true:一个失败就取消所有。false:继续跑完其余组合 |
max-parallel |
无上限 | 限制同时跑的 Job 数量 |
6. 三个操作系统 Runner 怎么选
runs-on: ubuntu-latest # Linux(免费额度 1 倍消耗)
runs-on: windows-latest # Windows(2 倍额度消耗)
runs-on: macos-latest # macOS(10 倍额度消耗)
| Runner | 用在哪 | 额度消耗 |
|---|---|---|
ubuntu-latest |
通用构建、Docker | 1× |
windows-latest |
.NET、Windows 桌面应用 | 2× |
macos-latest |
iOS 构建 | 10× |
7. 完整 Workflow
把依赖链和矩阵组合在一起:
name: Job 依赖与矩阵构建
on: [push]jobs:lint:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4- run: echo "🔍 代码检查通过 ✅"test:needs: lintstrategy:matrix:os: [ubuntu-latest, windows-latest]node: [18, 20]runs-on: ${{ matrix.os }}steps:- uses: actions/checkout@v4- run: echo "🧪 ${{ matrix.os }} + Node ${{ matrix.node }} 通过 ✅"build:needs: testruns-on: ubuntu-lateststeps:- run: echo "📦 构建完成 ✅"notify:needs: buildruns-on: ubuntu-lateststeps:- run: echo "📬 全部 Job 执行成功 ✅"
8. 实际运行结果
lint: 🔍 代码检查通过 ✅test: (4 个并行)ubuntu + Node 18 ✅ubuntu + Node 20 ✅windows + Node 18 ✅windows + Node 20 ✅build: 📦 构建完成 ✅notify: 📬 全部 Job 执行成功 ✅
运行总览
| 指标 | 值 |
|---|---|
| Job 总数 | 7(1 lint + 4 test + 1 build + 1 notify) |
| 最大并行数 | 5 |
| 总耗时 | 47 秒 |
| 状态 | ✅ 全部成功 |
9. 小结
| 你学到的 | 一句话记住 |
|---|---|
needs |
Job 之间的"等一等"——A 跑完 B 再跑 |
strategy.matrix |
一次性在所有版本/系统上跑测试 |
${{ matrix.os }} |
运行时取当前组合的值 |
fail-fast |
一个失败要不要停掉其他 |
ubuntu/windows/macos |
三种 Runner,额度消耗不同 |
第四部分:Marketplace Action 实战
前面几部分用的都是 run(执行 shell 命令)。但大多数时候你不需要自己写命令——GitHub Marketplace 上有上万现成的 Action,拿来就能用。
本部分介绍四个最常用、最实用的 Marketplace Action。
1. 第一个 Action:setup-node
要给仓库配 Node.js 环境,完全不需要自己写安装脚本。用 setup-node 一行搞定:
steps:- uses: actions/setup-node@v4with:node-version: 20
翻译成大白话:
uses: actions/setup-node@v4 "我要用 setup-node 这个工具,版本是 v4"
with: "传参数给它"node-version: 20 "Node.js 版本用 20"
这个 Action 自动帮你装好指定版本的 Node.js,还设置了环境变量。之后的 run: npm install 或 run: node xxx 就直接能用。
2. 第二个 Action:cache
每次跑 CI 都重新 npm install 一遍很慢。用 cache 把 ~/.npm 目录缓存起来,下次就能直接从缓存恢复:
steps:- uses: actions/cache@v4id: npm-cache # 给这个步骤起个 ID,方便调试with:path: ~/.npm # 缓存哪个目录?key: npm-${{ hashFiles('package-lock.json') }} # 缓存唯一标识
运行机制
第 1 次跑: 缓存没找到 → npm install → 把 ~/.npm 存起来
第 2 次跑: 缓存命中了 → 直接恢复 ~/.npm,跳过 npm install
hashFiles('package-lock.json') 的意思是:根据 package-lock.json 的内容生成一个指纹。依赖变了,指纹就变了,自动用新缓存。
3. 第三个 Action 组合:upload-artifact + download-artifact
前面学了 needs 让 Job 串起来跑,但有个问题——每个 Job 是独立的环境。Job A 构建好的文件,Job B 里看不到。
ci Job (Runner A) deploy Job (Runner B)
├── 构建完成 ├── 找不到 dist/ 目录!
└── dist/ 在 A 跑完就销毁了 └── 因为这不是同一台机器
Artifact 就是用来解决这个问题的:跨 Job 传文件。
# Job A:上传
- uses: actions/upload-artifact@v4with:name: build-output # 给产物取个名字path: ./dist # 上传 dist/ 目录# Job B:下载
- uses: actions/download-artifact@v4with:name: build-output # 用同一个名字取回来path: ./dist # 放到 dist/ 目录
传递过程:
Job A: 构建 Job B: 部署└─ dist/ ┌─ dist/ ← 拿到同样的文件↓ upload ↑ download[blob 存储] ──────────────►
4. 完整 Workflow 组装
把上面四个 Action 和 Node.js 项目组合成一条流水线:
name: Marketplace Action 实战
on: [push]jobs:ci:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4# 装 Node- uses: actions/setup-node@v4with:node-version: 20# 缓存 npm- uses: actions/cache@v4id: npm-cachewith:path: ~/.npmkey: npm-${{ hashFiles('package-lock.json') }}# 安装、测试、构建- run: npm install- run: npm test- run: npm run build# 上传产物- uses: actions/upload-artifact@v4with:name: build-outputpath: ./distdeploy:needs: ciruns-on: ubuntu-lateststeps:# 下载产物- uses: actions/download-artifact@v4with:name: build-outputpath: ./dist- run: cat ./dist/build-info.json- run: echo "🚀 部署完成!(模拟)"
5. 实际运行结果
📦 缓存 npm: 未命中(第一次跑,会缓存起来)
📦 安装依赖...
🧪 运行测试✅ 测试 1: 基础数学通过✅ 测试 2: lodash 正常加载🎉 全部测试通过!
🔨 构建完成
📤 上传产物 build-output.zip (245 bytes)📥 下载产物 build-output...
📦 已下载构建产物:
{"name": "gh-actions-test","builtAt": "2026-07-28T01:03:59.053Z","builtBy": "GitHub Actions"
}
🚀 部署完成!(模拟)
6. 小结
| Action | 一句话记住 |
|---|---|
actions/setup-node@v4 |
帮你装好 Node.js |
actions/cache@v4 |
缓存依赖,避免每次都重新下载 |
actions/upload-artifact@v4 |
把文件传给另一个 Job |
actions/download-artifact@v4 |
从另一个 Job 接收文件 |
第五部分:自定义 Action
Marketplace 上有上万 Action,但总有你找不到的。当你想把一组常用的步骤打包在不同 Workflow 里复用,就需要写自己的 Action。
1. 什么时候该写自定义 Action?
先看一个例子:你发现多个 Workflow 都在做同样的操作:
# Workflow A
- run: |echo "检查代码..."ls src/echo "文件数: $(find src -type f | wc -l)"# Workflow B(重复同样的逻辑)
- run: |echo "检查代码..."ls src/echo "文件数: $(find src -type f | wc -l)"
重复代码 → 写一个 Action 封装起来 → 到处复用。
2. Composite Action 是什么?
Composite(复合)Action 是最简单的自定义 Action 类型。你不需要学新语言,就是把 steps 装到一个文件里,像积木一样被其他 Workflow 引用。
理解它的目录结构:
.github/actions/质量检查/ ← 这就是你的 Action 目录
└── action.yml ← Action 的定义文件
在 Workflow 中使用:
steps:- uses: ./.github/actions/质量检查/ # 引用本地 Action
3. 写一个最简单的自定义 Action
创建一个 .github/actions/quality-check/action.yml:
name: "Quality Check" # Action 的名字
description: "运行代码质量检查" # 描述inputs: # 这个 Action 接收什么参数src-dir:description: "源码目录"required: truedefault: "./src"outputs: # 这个 Action 能输出什么files-checked:description: "检查的文件数"value: ${{ steps.count.outputs.count }}runs:using: "composite" # 固定写法:复合类型steps: # 里面的 steps 和 Workflow 一样写- name: 检查目录shell: bashrun: |if [ ! -d "${{ inputs.src-dir }}" ]; thenecho "❌ 目录不存在"exit 1fiecho "✅ 目录存在"- name: 统计文件id: countshell: bashrun: |count=$(find "${{ inputs.src-dir }}" -type f | wc -l)echo "count=$count" >> "$GITHUB_OUTPUT"
和 Workflow 的 steps 有什么不同?
| 区别 | Workflow 的 steps | Action 里的 steps |
|---|---|---|
uses |
✅ 可以用 | ✅ 也可以用 |
run |
✅ 可以写 | ✅ 可以写 |
shell |
可以省略,默认 bash | 必须写 shell: bash |
4. 在 Workflow 里用这个 Action
name: 使用自定义 Action
on: [push]jobs:quality:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4 # 必须:先拉代码- uses: ./.github/actions/quality-checkwith:src-dir: ./src # 传参数- name: 读取输出run: echo "文件数: ${{ steps.quality-check.outputs.files-checked }}"
两个重点:
| 规则 | 说明 |
|---|---|
| 必须先 checkout | 自定义 Action 在仓库里,不 checkout 找不到 |
| 路径写法 | uses: ./.github/actions/<目录名>/ |
5. 复用验证:传不同的参数,拿不同的结果
steps:# 检查 src 目录- uses: ./.github/actions/quality-checkwith:src-dir: ./src # 输出: 2 个文件# 检查整个项目- uses: ./.github/actions/quality-checkwith:src-dir: . # 输出: 58 个文件
实际运行结果
✅ 目录 ./src 存在
📊 共检查 2 个文件
📋 Action 输出 => 文件数: 2✅ 目录 . 存在
📊 共检查 58 个文件
📋 Action 输出 => 文件数: 58
同一个 Action,传入不同的 src-dir,产生不同的结果——这就是自定义 Action 的价值:逻辑写一次,到处复用。
6. 小结
| 你学到的 | 一句话记住 |
|---|---|
action.yml |
自定义 Action 的定义文件 |
runs.using: "composite" |
组合多个步骤的 Action |
inputs |
别人传进来的参数,用 ${{ inputs.xxx }} 读取 |
outputs |
自己算完的结果,用 $GITHUB_OUTPUT 输出 |
uses: ./.github/actions/xxx/ |
在 Workflow 里引用本地 Action |
第六部分:CI/CD 部署实战
CI(持续集成)做好了——代码推上去自动构建、自动测试。接下来是 CD(持续部署):让测试通过的代码自动部署到目标环境。
本部分做一个标准的双环境流水线:代码推到 master → 自动部署到 Staging → 手动确认后部署到 Production。
1. 先理解"环境"的概念
在 GitHub Actions 里,一个"环境"(environment)就是一组配置的打包:
Environment: staging├── 环境变量(ENVIRONMENT=staging、DEPLOY_URL=...)└── 谁可以部署(可选)Environment: production├── 环境变量(ENVIRONMENT=production、DEPLOY_URL=...)└── 需要审核才能部署(可选)
用 environment: 把 Job 和某个环境关联起来:
jobs:deploy-staging:environment:name: staging # 关联到 staging 环境
2. 环境变量:env 的三层作用域
# 全局:所有 Job 都能读到
env:APP_NAME: GH Actions Testjobs:deploy-staging:# Job 级:仅这个 Job 能读到(覆盖全局的同名变量)env:ENVIRONMENT: stagingsteps:# Step 级:仅这个 Step 能读到- name: 构建run: echo "环境是 $ENVIRONMENT"env:ENVIRONMENT: staging
| 作用域 | 配置在哪 | 谁看得到 |
|---|---|---|
| 全局 | Workflow 顶层的 env: |
所有 Job |
| Job 级 | jobs.<id>.env: |
这个 Job 里的所有 Step |
| Step 级 | steps[].env: |
只有这一个 Step |
3. 条件部署:什么时候部署到哪个环境
用 if 控制:
jobs:# push 到 master → 自动部署 stagingdeploy-staging:if: github.event_name == 'push'environment:name: staging# 手动选 production → 部署 productiondeploy-production:if: github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'production'environment:name: production
执行路径:
git push master ──→ build → deploy-staging(自动)
手动选 production ──→ build → deploy-production(需审核)
4. 跨 Job 传数据再复习
第三部分学过 needs 和 artifact:
# Job A:构建
build:outputs:version: ${{ steps.set-version.outputs.version }} # 传小数据steps:- uses: actions/upload-artifact@v4 # 传文件with:name: site-${{ github.sha }}path: dist/# Job B:部署
deploy-staging:needs: buildsteps:- run: echo "版本: ${{ needs.build.outputs.version }}" # 读数据- uses: actions/download-artifact@v4 # 读文件
5. 完整 Workflow
把上面的概念拼起来:
name: CI/CD 部署实战
on:push:branches: [master]workflow_dispatch:inputs:target:description: '部署到哪?'required: truedefault: 'staging'type: choiceoptions: [staging, production]env:APP_NAME: GH Actions Test # 全局变量jobs:build:runs-on: ubuntu-latestoutputs:version: ${{ steps.set-version.outputs.version }}steps:- uses: actions/checkout@v4- id: set-versionrun: echo "version=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"- run: node src/build-site.jsenv:ENVIRONMENT: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.target || 'staging' }}- uses: actions/upload-artifact@v4with:name: site-${{ github.sha }}path: ./distdeploy-staging:if: github.event_name == 'push'needs: buildenvironment:name: stagingurl: ${{ github.server_url }}/${{ github.repository }}env:ENVIRONMENT: stagingruns-on: ubuntu-lateststeps:- uses: actions/download-artifact@v4with:name: site-${{ github.sha }}- run: |echo "环境: ${{ env.ENVIRONMENT }}"echo "版本: ${{ needs.build.outputs.version }}"echo "✅ 部署成功"deploy-production:if: github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'production'needs: buildenvironment:name: productionurl: ${{ github.server_url }}/${{ github.repository }}env:ENVIRONMENT: productionruns-on: ubuntu-lateststeps:- uses: actions/download-artifact@v4with:name: site-${{ github.sha }}- run: |echo "环境: ${{ env.ENVIRONMENT }}"echo "版本: ${{ needs.build.outputs.version }}"echo "🏥 健康检查通过"echo "✅ Production 部署完成"
6. 实际运行结果
Push 触发 —— Staging 自动部署
✅ 站点构建完成 环境: staging 版本: 9f8d15f🚀 部署到 Staging环境: staging 版本: 9f8d15f
🔍 冒烟测试...✅ HTML 文件存在✅ CSS 文件存在✅ 部署成功
手动选 Production 触发
🚀 部署到 Production环境: production 版本: 9f8d15f
🏥 健康检查...✅ 服务响应正常✅ Production 部署完成
7. 关于 Environment 审核保护
在仓库 Settings → Environments → production 中,你可以:
- Required reviewers:指定谁可以批准部署到 production
- Wait timer:等待一段时间再部署(用于灰度观察)
- Branch protection:限制只能从特定分支部署
一旦设置了审核规则,手动触发后 Job 会显示 "等待审核",审核人批准后才真正执行。
8. 小结
| 你学到的 | 一句话记住 |
|---|---|
env: |
分三层:全局 → Job → Step |
environment: |
把 Job 关联到一个环境(变量隔离 + 审核保护) |
if: |
控制这个 Job 在什么条件下跑 |
| Staging vs Production | 自动部署到 staging,手动确认后部署到 production |
第七部分:进阶技巧
前面六部分已经覆盖了日常 80% 的需求。这部分是几个"小功能大作用"的技巧,用来解决真实项目中的具体问题。
1. 并发控制:快速推送时自动取消旧的
问题:你快速推送了两次,第一个 Workflow 还在跑,第二个也触发了——两个同时在部署,会打架。
concurrency:group: ${{ github.workflow }}-${{ github.ref }} # 按"工作流名-分支名"分组cancel-in-progress: true # 新的来了,取消旧的
放在 Workflow 文件最上层,和 on:、jobs: 平级:
name: 我的 Workflow
on: [push]concurrency: # ← 加在这里group: ${{ github.workflow }}-${{ github.ref }}cancel-in-progress: truejobs:...
分组策略
| 分组方案 | 效果 |
|---|---|
${{ github.workflow }} |
这个 Workflow 全局只能一个跑 |
${{ github.workflow }}-${{ github.ref }} |
按分支隔离,不同分支互不干扰 |
deploy-${{ github.ref }} |
部署类按分支串行 |
实际验证结果
第 1 次推送 → Run #1 开始跑↓ 第 2 次推送触发(同一分支)
Run #1 自动取消 ❌ Run #2 开始跑 ✅
Run #1 → completed: cancelled ← 被新推送自动取消
Run #2 → completed: success ← 最终成功执行
什么时候必须加:部署类 Workflow 一定要加,不然两个部署同时跑会互相覆盖。
2. Workflow 命令:让日志更好看
GitHub Actions 识别以 :: 开头的特殊命令,用来和 UI 交互。
日志分组:把相关信息折叠起来
steps:- run: |echo "::group::📦 安装依赖" # 开始一个分组echo "安装 lodash..."echo "安装 express..."echo "::endgroup::" # 结束这个分组echo "::group::🧪 测试结果"echo "测试 1: 通过"echo "测试 2: 失败"echo "::endgroup::"
效果:日志里变成可展开/折叠的区块,页面清爽很多。
注释标注:在 PR 上直接显示警告
steps:- run: |echo "::notice title=风格::建议用 const 代替 let"echo "::warning title=性能::循环里别用 console.log"echo "::error title=语法::第 42 行少了个分号"
| 命令 | 颜色 | 显示在哪 |
|---|---|---|
::notice |
蓝色 | PR 的 Checks 标签页 |
::warning |
黄色 | PR 的 Checks 标签页 |
::error |
红色 | PR 的 Checks 标签页 |
3. 错误处理:失败了也不中断
有的步骤失败了不影响大局,比如代码风格检查——你不想因为代码写得丑就不让 CI 通过。
steps:- name: 风格检查(可选)continue-on-error: true # 加了这行,失败了也不中断run: exit 1- name: 它还是会执行run: echo "✅ 上一步失败了,但我继续"
还有三个特殊的 if: 条件:
steps:- if: success() # 默认:前面的步骤都成功才执行run: echo "都成功了"- if: failure() # 前面的步骤有失败的才执行run: echo "有人失败了,我来清理"- if: always() # 不管成功失败,都执行run: echo "我每次都跑"
4. 超时控制:防止 Workflow 跑死
jobs:long-task:runs-on: ubuntu-latesttimeout-minutes: 10 # 超过 10 分钟自动终止
默认超时是 360 分钟(6 小时),建议每个 Job 都设一个合理的超时——防止异常情况白白烧掉免费额度。
5. 完整 Workflow
name: 进阶技巧实战
on: [push]concurrency:group: ${{ github.workflow }}-${{ github.ref }}cancel-in-progress: truejobs:workflow-commands:runs-on: ubuntu-lateststeps:- run: |echo "::group::📦 安装"echo "安装完成"echo "::endgroup::"echo "::warning title=性能::请优化大循环"timeout-demo:runs-on: ubuntu-latesttimeout-minutes: 1steps:- run: echo "✅ 1 分钟内完成"error-handling:runs-on: ubuntu-lateststeps:- name: 可选步骤continue-on-error: truerun: exit 1- name: 仍然执行run: echo "✅ 继续"- name: 总发送通知if: always()run: echo "📬 通知发送"
第八部分:实战案例 —— 个人博客部署流水线
前面七个部分都是刻意设计的示例。这一部分看一个真实的生产级 Workflow——我的个人博客就是这样部署的。
1. 先看完整源码
name: Deploy to GitHub Pageson:push:branches: [master, data]jobs:deploy:runs-on: ubuntu-latestpermissions:contents: writesteps:- uses: actions/checkout@v4with:ref: masterpath: source- uses: actions/checkout@v4with:ref: datapath: data-tmp- name: 合并数据到源码run: |rm -rf data-tmp/.gitmkdir -p source/publiccp -r data-tmp/. source/public/data/rm -rf data-tmp- uses: actions/setup-node@v4with:node-version: 22- name: 安装依赖working-directory: ./sourcerun: npm ci- name: 构建静态站点working-directory: ./sourcerun: npm run build:staticenv:NEXT_PUBLIC_IS_STATIC: "true"- name: 自动生成 CNAMErun: |node -e "const fs = require('fs');const src = fs.readFileSync('source/lib/siteConfig.ts', 'utf-8');const blog = (src.match(/blog:\s*\"([^\"]+)\"/)||[])[1] || '';const hasDomain = /hasDomain:\s*true/.test(src);if (blog && hasDomain) fs.writeFileSync('source/out/CNAME', blog);"- uses: peaceiris/actions-gh-pages@v4with:github_token: ${{ secrets.GITHUB_TOKEN }}publish_dir: ./source/out
2. 为什么有两个分支?
这是这个 Workflow 最精彩的设计。它不是单个分支,而是一个双分支策略:
master 分支(源码) data 分支(数据)├── pages/ ├── posts/ ← 博客文章├── components/ ├── images/ ← 图片资源├── lib/siteConfig.ts └── ...└── package.json│ │└─────────┬──────────────────┘↓Workflow 合并构建↓GitHub Pages 部署
| 分支 | 里面有什么 | 谁在改 |
|---|---|---|
master |
源码、组件、样式、配置文件 | 开发者(偶尔修改) |
data |
博客文章、图片 | 写作者(日常更新) |
为什么这样设计?
写博客的人只需要推送 data 分支(git add → commit → push data),不需要碰源码、不需要理解构建流程。而源码在 master 上独立维护,两者互不干扰——哪个分支推送了,Workflow 都会触发部署。
3. 双次检出:一个 Runner 操作两个分支
关键技巧:用两次 actions/checkout,设不同的 path:
- uses: actions/checkout@v4with:ref: master # 检出 master 分支path: source # 放到 source/ 目录- uses: actions/checkout@v4with:ref: data # 检出 data 分支path: data-tmp # 放到 data-tmp/ 目录
| 参数 | 效果 |
|---|---|
ref |
指定检出的分支 |
path |
放到工作目录的哪个子目录 |
两个目录互不干扰,之后把 data-tmp/ 的内容合并到 source/public/data/:
rm -rf data-tmp/.git # 去掉 .git,只复制内容
cp -r data-tmp/. source/public/data/ # 合并到 public/data/
数据文件进了
public/目录,会被 Next.js 构建时直接打包——不依赖任何 API 请求,天然 CDN 加速。
4. working-directory:指定命令在哪跑
- name: Build static siteworking-directory: ./source # 先在 source/ 目录下执行run: npm run build:static
没有 working-directory 你就得写 cd source && npm run build:static。这个参数在多个目录协作时非常好用。
5. 自动生成 CNAME
GitHub Pages 的自定义域名是通过 CNAME 文件配置的。这段脚本绕开了手动创建——直接从配置文件中自动提取:
- name: 自动生成 CNAMErun: |node -e "const src = fs.readFileSync('source/lib/siteConfig.ts', 'utf-8');const blog = (src.match(/blog:\s*\"([^\"]+)\"/)||[])[1] || '';const hasDomain = /hasDomain:\s*true/.test(src);if (blog && hasDomain) fs.writeFileSync('source/out/CNAME', blog);"
做了什么:读取 siteConfig.ts → 正则匹配 blog 字段的域名 → 如果有自定义域名 → 往构建输出目录写入 CNAME 文件。
这样域名配置在源码里统一管理。改域名只需改
siteConfig.ts,下次推送自动生效。
6. 部署到 GitHub Pages
- uses: peaceiris/actions-gh-pages@v4with:github_token: ${{ secrets.GITHUB_TOKEN }}publish_dir: ./source/out
peaceiris/actions-gh-pages 是社区最流行的 Pages 部署 Action。它自动把 publish_dir 推送到 gh-pages 分支,GitHub Pages 检测到该分支有更新就会部署。
7. 从实验到生产——你能带走什么
对比前面练习的 Workflow 和这个生产级的 Workflow:
| 进阶点 | 练习阶段 | 生产阶段 |
|---|---|---|
| 分支策略 | 单分支 | 源码/数据双分支隔离 |
| 多分支操作 | 一次 checkout | 两次 + path |
| 配置管理 | 硬编码 | 从源码自动读取 |
| 构建产物 | 仅验证 | 直接部署上线 |
但是你会发现:生产级 Workflow 里的语法(checkout、setup-node、run、env、with),你全部已经在前七部分学过了。
核心感悟:Workflow 的真正威力不在于语法技巧,而在于把项目架构思考转化为自动化流程——双分支策略、数据与代码分离、配置驱动部署,这些都是架构设计在 CI/CD 层面的自然延伸。
- 读取
siteConfig.ts配置文件 - 用正则提取
blog字段值(域名) - 检查
hasDomain是否为true - 满足条件则自动生成
CNAME文件
设计智慧:域名配置放在源码中集中管理,避免部署流程与配置脱节。修改域名只需改
siteConfig.ts,下次推送自动生效。
5. 部署到 GitHub Pages
- uses: peaceiris/actions-gh-pages@v4with:github_token: ${{ secrets.GITHUB_TOKEN }}publish_dir: ./source/out
| 参数 | 说明 |
|---|---|
publish_dir |
要发布的目录(相对于仓库根) |
github_token |
GitHub 自动提供的 Token,无需手动创建 |
| 发布目标 | 默认推送到 gh-pages 分支 |
这是社区最流行的 Pages 部署 Action。对比之前实验用的
actions/deploy-pages(官方),peaceiris/actions-gh-pages的优点是无需启用 Pages 预览版功能,兼容性更广。
6. 启动触发器
on:push:branches: [master, data]
| 分支 | 推送触发场景 |
|---|---|
master |
修改源码、组件、样式等代码变更 |
data |
写新文章、更新图片等数据变更 |
这种设计意味着写作者只需要了解 Git 基本操作(git add / commit / push data),不需要接触 CI/CD 配置——流水线的复杂度被 Workflow 封装了。
7. 小结:从实验到生产的距离
对比本书前面的实验 Workflow,这个生产环境 Workflow 引入了几个关键升级:
| 进阶点 | 实验阶段 | 生产阶段 |
|---|---|---|
| 分支策略 | 单分支 | 源码/数据双分支隔离 |
| 多分支操作 | 一个 checkout | 两个 checkout + path |
| 配置文件 | 硬编码 | siteConfig.ts 自动解析 |
| 构建产物 | 仅验证 | 直接部署到 Pages |
| Token | 可省略 | ${{ secrets.GITHUB_TOKEN }} |
核心感悟:Workflow 的真正威力不在于语法技巧,而在于把项目架构思考转化为自动化流程——双分支策略、数据与代码分离、配置驱动部署,这些都是架构设计在 CI/CD 层面的自然延伸。
总结
九部分速查
| 部分 | 你学会了 | 练习文件 |
|---|---|---|
| Part 1: Hello World | 第一个 Workflow、runs-on、uses、run、${{ }} |
hello.yml |
| Part 2: 触发器 | push、pull_request、workflow_dispatch、if 条件 |
triggers.yml |
| Part 3: Job 与矩阵 | needs 依赖链、strategy.matrix 多版本并行 |
jobs-matrix.yml |
| Part 4: Marketplace | setup-node、cache、upload/download-artifact |
marketplace.yml |
| Part 5: 自定义 Action | Composite Action、inputs、outputs、本地引用 |
custom-action.yml |
| Part 6: CI/CD 部署 | env 三级作用域、environment 环境管理、Staging/Production |
deploy.yml |
| Part 7: 进阶技巧 | 并发控制、:: 命令、continue-on-error、超时 |
advanced.yml |
| Part 8: 实战案例 | 双分支策略、双次检出、CNAME 自动生成、生产级部署 | 博客仓库 |
一条流水线的标准模式
触发 → 检出 → 装环境 → 缓存 → 构建 → 测试 → 传产物 → 部署↓矩阵并行
下一步可以学什么
| 方向 | 为什么值得学 |
|---|---|
| Docker Action | 自定义 Action 的进阶——适合需要特殊系统环境的场景 |
| OIDC 云认证 | 不用密钥文件,直接部署到 AWS/Azure/GCP,更安全 |
| 自托管 Runner | 在你的服务器上跑 Workflow,可以访问内网资源 |
| Script Injection 防护 | 当 ${{ }} 里拼接用户输入时,如何防止被攻击 |
