GitHub Actions 是“事件触发 Workflow,Workflow 编排 Job,Job 在 Runner 上顺序执行 Step”的自动化系统。本篇建立 CI 的执行心智模型,掌握 YAML 原语、依赖关系、矩阵、并发、Cache 与 Artifact 的边界;完整的 CI 质量门配置见 Actions CI 配置方法,凭据与不可信输入的安全专题见 Actions 安全与排障。
Actions 不是“把一段 shell 放到 GitHub 上运行”这么简单。一个运行由事件、配置版本、权限、Job 拓扑和 Runner 环境共同决定:事件选择何时启动,Workflow 文件描述要做什么,Job 决定隔离与并行边界,Step 决定具体动作,Runner 提供操作系统和工具。
1. Workflow 文件与事件#
1.1 文件位置和顶层结构
Workflow 必须放在仓库根目录的 .github/workflows/*.yml 或 .github/workflows/*.yaml 下。文件名只是人类可读的标识;GitHub 在 Actions 页面按 name 显示 Workflow。
顶层键可按职责记忆:name 是 UI 名称,on 是事件和过滤器,permissions 是 Token scope,env/defaults 是运行默认值,concurrency 是并发控制,jobs 是执行拓扑;它们分别解决显示、触发、权限、环境、排队和执行问题。
1.2 push、pull_request 与 workflow_dispatch
push:某个分支或标签收到新的提交时运行。它适合检查已经进入远程分支的提交,例如默认分支上的持续检查。未设置branches、branches-ignore、tags或tags-ignore时,分支和标签 push 都可能触发;需要按范围明确过滤。pull_request:Pull Request 发生指定活动时运行,适合在合并前验证变更。这里的branches匹配的是 PR 的目标分支,而不是贡献者的 head branch。默认活动类型包括常见的打开、更新和重新打开等,若只想响应某些变化应显式设置types。workflow_dispatch:手动运行 Workflow,可声明inputs让执行者选择版本、目标或开关。它解决“现在运行一次”的需求,不是定时任务,也不会自动替代push/pull_request。要在网页中手动选择,Workflow 文件必须已存在于默认分支。
最常见的 CI 起点是同时监听 push 和 pull_request:前者验证分支上的提交,后者给 PR 提供合并前检查。两者可能为同一个提交产生两次运行,这是事件设计的结果,不是 GitHub 重复执行同一个 Step。
1.3 workflow_call:可复用 Workflow 的调用契约
workflow_call 不是“再加一种手动运行方式”,而是把一个 Workflow 声明为可被其他 Workflow 调用的组件。被调用文件通常只定义输入、输出和必要的 secrets;调用方通过 Job 级 uses 传入参数。它适合多个仓库或多个入口共享同一套 CI 编排,但会增加调用契约和版本管理成本。
被调用 Workflow 的最小声明可以写成:
on:
workflow_call:
inputs:
command:
required: true
type: string
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Display command input without executing it
env:
COMMAND_LABEL: ${{ inputs.command }}
run: printf '%s\n' "$COMMAND_LABEL"调用方把它当作一个 Job,并负责真正的触发事件:
jobs:
reusable:
uses: ./.github/workflows/reusable-checks.yml
with:
command: git diff --check这里的 command 仅作为显示数据,不会执行它。不要把调用者输入直接插入 run 或交给 eval;需要选择检查命令时应使用固定命令或经审查的允许列表。被调用 Workflow 只有 workflow_call 时,不会因为普通 push 自动运行;输入类型必须在契约中声明,输出可通过 Job outputs 暴露给调用方。更复杂的复用策略留给 CI 配置专题。
2. Job、Runner、Step、Action 与 Shell#
2.1 四层执行模型
可以把一个 CI Run 想成下面的执行树:
Event
└── Workflow(一个 YAML 文件的一次运行)
├── Job A → Runner A → Step 1 → Step 2
└── Job B → Runner B → Step 1 → Step 2| 原语 | 心智模型 | 关键字段 |
|---|---|---|
| Workflow | 由一个 YAML 文件定义的一组自动化规则和本次运行 | name、on、jobs |
| Job | 一个隔离的执行单元;不同 Job 默认并行 | runs-on、needs、strategy、steps |
| Runner | 承载一个 Job 的虚拟机或自托管执行环境 | runs-on、runner context |
| Step | Job 内按顺序执行的最小编排单位 | run、uses、with、env、if |
| Action | 可复用的打包逻辑,可被 Step 调用 | uses: owner/repo@ref |
| Shell | 执行 run 文本的命令解释器 |
defaults.run.shell、steps[*].shell |
Job 之间默认可以并行,每个 Job 通常获得独立 Runner 和工作目录。一个 Job 的文件修改不会自然出现在另一个 Job;需要用 Artifact、外部存储或重新 checkout/构建来传递。Job 内的 Step 按声明顺序执行,前一步失败后后续 Step 默认跳过,除非使用条件表达式或 continue-on-error 改变行为。
2.2 run 与 uses 的区别
run把多行或单行命令交给 Runner 的 shell。它适合项目自己的检查命令,例如nix flake check、git diff --check或npm test。uses调用一个 Action。Action 可以是 JavaScript Action、Docker Action 或 Composite Action;它通过with接收输入,通过 outputs 暴露结果。- Step 级
uses和 Job 级uses不一样:前者调用一个动作,后者调用一个workflow_call可复用 Workflow。
Action 的引用形状是 owner/repository@ref。CI 中更可审查的写法是固定到完整 commit SHA:
steps:
- name: Check out source
uses: actions/checkout@<full-commit-sha>这里的 <full-commit-sha> 是刻意保留的占位符,不是可直接复制的版本号;实际使用时应从可信上游和项目的更新记录填入真实完整 SHA,并在同一行写可读的版本注释。本文其他 Action 示例也使用这个占位符,不输出任何凭据。第三方 Action 的信任、权限与不可信输入边界见 06,不在此处展开完整安全专题。
2.3 Runner 与工作目录
runs-on 选择 Runner 标签,例如 ubuntu-latest。latest 是 GitHub 管理的移动标签,镜像中的预装工具会随平台更新;它适合快速建立 CI,但不是完全冻结的执行环境。需要更强可复现性时,应结合项目实际支持范围选择固定镜像或容器,并把工具版本写进项目配置。
Runner 会为 Job 准备 $GITHUB_WORKSPACE,checkout Action 通常把仓库放到这里。Job 内 Step 共享工作目录,因此前一个 Step 生成的文件通常可被后一个 Step 使用;但 Step 的 shell 进程不是同一个进程,export TEMP=value 不会自动成为下一 Step 的环境变量。跨 Step 传环境值时使用:
echo "TEMP=value" >> "$GITHUB_ENV"跨 Step 传结构化的小结果时使用:
echo "version=1.2.3" >> "$GITHUB_OUTPUT"3. Contexts、Expressions、环境变量与默认值#
3.1 两种插值语言
Actions YAML 中常见的两种“变量”来自不同阶段:
env:
MODE: ci
jobs:
check:
runs-on: ubuntu-latest
steps:
- run: echo "event=${{ github.event_name }} mode=$MODE"${{ ... }}是 GitHub Actions 的表达式,在 Workflow 编排阶段读取 Context 或调用函数。$MODE是 Runner shell 在执行run时展开的环境变量。
不要把这两种语法互换:github.event_name 不能直接写成 shell 变量,$MODE 也不是 GitHub 表达式。if 条件经常可以省略外层 ${{ }},但在复杂条件中保留它更清晰。
3.2 常用 Context
| Context | 典型内容 | 用法示例 |
|---|---|---|
github |
仓库、分支、事件名、提交和运行信息 | ${{ github.event_name }} |
inputs |
workflow_dispatch 或 workflow_call 的输入 |
${{ inputs.command }} |
vars |
仓库、组织或环境级配置变量 | ${{ vars.RUNTIME }} |
env |
YAML 中当前可见的环境变量 | ${{ env.MODE }} |
matrix |
当前矩阵组合的值 | ${{ matrix.os }} |
needs |
已完成依赖 Job 的 outputs 和结果 | ${{ needs.build.outputs.version }} |
steps |
当前 Job 中带 id 的 Step outputs 和结论 |
${{ steps.meta.outputs.version }} |
runner |
操作系统、临时目录和 Runner 信息 | ${{ runner.os }} |
Context 的可用范围取决于所在键和执行阶段;不是所有 Context 在所有位置都存在。尤其是 matrix 只在定义了矩阵的 Job 中有意义,needs 只包含显式依赖的 Job。读取事件载荷时使用 github.event,不要假设所有事件有相同字段。
3.3 env、defaults 与 GITHUB_*
env 可以放在 Workflow、Job 或 Step 层,越内层越具体;同名变量通常由更窄作用域覆盖。defaults.run 可以统一 shell 和工作目录:
defaults:
run:
shell: bash
working-directory: .GitHub 还提供许多默认环境变量,例如 GITHUB_WORKSPACE、GITHUB_SHA、GITHUB_REF 和 GITHUB_RUN_ID。它们描述本次运行,不应硬编码成某个分支或提交。默认变量和 Context 的准确可用范围以官方参考为准;本篇只依赖最稳定的事件名、分支和输出机制。
permissions 与 env 解决不同问题:前者控制 GITHUB_TOKEN 的 GitHub API 权限,后者只设置进程可见的值。一旦在 Workflow 或 Job 显式写出 permissions 映射,未列出的权限范围就变成 none;例如:
permissions:
contents: read表示该层只请求 contents 的读取权限,其他未列出的 scope 不授予权限。具体 token、Fork PR、Secrets 和第三方 Action 风险留给 06。
4. 依赖、Outputs、Matrix 与 Concurrency#
4.1 用 needs 表达 Job 依赖
没有 needs 时,Jobs 之间没有数据或顺序依赖,平台可以并行运行。声明 needs 后,依赖 Job 成功完成,当前 Job 才会开始;依赖失败或跳过时,后续 Job 默认也跳过。
jobs:
lint:
runs-on: ubuntu-latest
steps:
- run: ./lint.sh
test:
needs: lint
runs-on: ubuntu-latest
steps:
- run: ./test.shneeds 表达的是拓扑约束,不会自动传文件或环境。需要传小型字符串时用 Job outputs:
jobs:
meta:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.value }}
steps:
- id: version
run: echo "value=1.2.3" >> "$GITHUB_OUTPUT"
show:
needs: meta
runs-on: ubuntu-latest
steps:
- run: echo "version=${{ needs.meta.outputs.version }}"Outputs 适合短值或状态,不适合传大文件。大文件和测试报告应考虑 Artifact;依赖下载应考虑 Cache。
4.2 Matrix:把一个 Job 展开成多个组合
strategy.matrix 为一个 Job 声明一组变量;每种组合都会成为独立 Job。两个维度的组合数是笛卡尔积,成本和失败面也会随组合数增长:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runtime: ["3.11", "3.12"]
steps:
- run: echo "${{ matrix.os }} / ${{ matrix.runtime }}"上例展开为 4 个 Job。include 可补充特殊组合,exclude 可移除不支持的组合。fail-fast: false 让其他组合继续提供结果,适合需要完整兼容性报告的 CI;小项目不应为了“看起来完整”而无依据地添加矩阵。后文的主题 CI 教学示例使用单一 flake Job,不能描述成 matrix CI。
4.3 Concurrency:取消过时运行
concurrency 可以放在 Workflow 或 Job 层,用一个字符串或表达式建立并发组。常见 CI 策略是同一分支的新提交到来后取消旧的进行中运行:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true这减少重复构建,但只适合旧结果确实不再有价值的检查。发布、迁移或必须完整执行的任务不应盲目取消。并发组应包含 github.workflow 等区分字段,避免不同 Workflow 因为同名组互相取消。
5. Cache 与 Artifact:两个相反的边界#
5.1 Cache 是加速依赖
Cache 保存可重新生成的依赖或工具下载结果,目标是缩短后续运行时间。典型输入是包管理器缓存目录、锁文件哈希和 Runner 操作系统:
- name: Cache dependencies
uses: actions/cache@<full-commit-sha>
with:
path: ~/.cache/example
key: ${{ runner.os }}-deps-${{ hashFiles('**/lockfile') }}
restore-keys: |
${{ runner.os }}-deps-Cache 命中不是正确性的前提;缓存丢失时,Job 应能重新下载并通过。Key 变化会产生新的缓存,restore-keys 可在没有精确命中时恢复较宽的旧缓存。不要把 Cache 当成可靠的跨 Job 文件传输,也不要把凭据、动态构建结果或必须审计的产物放进去。
5.2 Artifact 是运行产物
Artifact 保存某次 Workflow Run 生成的文件,例如测试报告、覆盖率、打包目录或调试日志;它可以在后续 Job 下载,也可在运行结束后从 Actions 页面取得。基本形状是:
- name: Upload report
uses: actions/upload-artifact@<full-commit-sha>
with:
name: test-report
path: reports/另一个 Job 可以用 actions/download-artifact 下载同一运行中的产物。Artifact 的语义是“保存和共享这次运行产生的输出”,不是“让下一次运行更快”。
一句话区分:Cache 是可丢失、可重建的依赖加速;Artifact 是这次运行的输出保存或跨 Job 传递。后文的 nix.yml 教学示例两者都没有。
6. 一个完整的最小 CI YAML#
下面的示例只依赖 Git 自身,适合展示执行原语:它在默认分支的 push、目标为默认分支的 PR 或手动运行时 checkout 代码,并检查已提交补丁的空白错误。<full-commit-sha> 必须替换为真实完整 SHA 后才能作为实际 Workflow 使用。
name: Minimal CI
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
check:
name: Repository checks
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- name: Check out source
uses: actions/checkout@<full-commit-sha>
with:
fetch-depth: 2
persist-credentials: false
- name: Check committed patch whitespace
run: |
if git rev-parse --verify HEAD^ >/dev/null 2>&1; then
git diff --check HEAD^ HEAD
else
git diff-tree --root -r --check HEAD
fi逐段阅读:name/on 定义显示名和三种入口;permissions 只给 checkout 所需的 contents: read,其他未列出的 scope 为 none;concurrency 取消同组过时运行;唯一的 check Job 在 Ubuntu Runner 上以 Bash 运行 checkout 与已提交补丁的空白检查。普通 git diff --check 在干净 checkout 中通常没有内容可检查。这里取两层历史:PR 默认合并提交可与第一父提交比较;push/手动入口只检查所选提交相对父提交的差异,不覆盖一次多提交 push 的完整范围。正式项目若要检查完整 push,应按事件的 before/after SHA 获取足够历史并处理新分支的全零 before 值。真实项目可在最后追加格式化、求值、构建和测试命令,但此示例没有 needs、outputs、matrix、cache 或 artifact。
这个示例有意不添加 strategy.matrix、actions/cache、upload-artifact 或语言运行时安装。先让最小检查稳定,再根据实际瓶颈增加并行、缓存或产物保存。
7. 事件过滤的陷阱#
7.1 分支与路径过滤的语义
push.branches匹配被 push 的分支;pull_request.branches匹配 PR 的目标分支。- 同一事件不能同时使用
branches和branches-ignore;要同时包含和排除时,在branches中按顺序使用!模式。 - 同一事件不能同时使用
paths和paths-ignore。如果同时配置分支过滤和路径过滤,两个条件都满足才会运行。 paths的正负模式按书写顺序处理;后出现的负模式可排除此前匹配的路径,后出现的正模式也可重新包含。- 路径过滤不用于 tag push。只写
tags或tags-ignore时,未定义的 branch 侧不会触发,反之亦然。
过滤过窄会产生“代码改了但没有检查”的假绿;过滤过宽则浪费 Runner 时间。更危险的是,被 branch/path/commit message 过滤跳过的 Workflow,其关联 Check 可能保持 Pending,Required Checks 因此无法合并。设置过滤器前先列出哪些变更必须触发,再确认每类变更的检查状态。
7.2 事件不等于代码版本
同一个仓库状态在不同事件中有不同上下文:push 代表分支上的提交,pull_request 代表 PR 更新,手动运行要求 Workflow 在默认分支存在,但具体运行可选择允许的分支或标签 ref。不要只从 github.ref 猜测目标分支;需要时同时查看 github.event_name、github.ref 和事件载荷中的 PR 字段。
Fork PR、Token、Secrets 和 pull_request_target 的风险涉及信任边界,统一放到 Actions 安全与排障。本篇只保留一个原则:事件过滤解决“何时运行”,不自动解决“谁提供了要执行的代码”。
8. 示例:主题项目的 nix.yml#
假设主题项目将 Workflow 放在 .github/workflows/nix.yml,教学示例采用如下结构,不代表个人仓库的当前配置:
| 项目 | 示例配置 |
|---|---|
| 触发事件 | push 和 pull_request,未额外声明分支/路径过滤 |
| Job | 只有一个名为 flake 的 Job |
| Runner | ubuntu-latest |
| Token 权限 | Workflow 级 contents: read |
| Actions | checkout 与 install-nix 都固定到完整 commit SHA |
| 检查内容 | nix flake check --all-systems --no-build、六种主题变体构建、输出契约检查 |
| Matrix/Cache/Artifact | 示例未配置 |
这里的“单一 flake Job”并不代表只执行一条命令:同一个 Job 内可以按顺序先求值、再构建变体、最后检查输出契约。没有 Matrix 是事实边界;六种变体是一个 nix build 命令的参数,不是六个矩阵 Job。没有 Cache 或 Artifact 也不表示 Actions 不支持它们,只表示示例没有把依赖加速或运行产物保存纳入这个 Workflow。
9. 官方资料与下一篇#
- Workflow syntax for GitHub Actions
- Events that trigger workflows
- Running variations of jobs with a matrix
- Dependency caching reference
- Store and share data with workflow artifacts
- Control the concurrency of workflows and jobs
- Reusing workflows
读完本篇后,下一步是把本地检查整理成稳定的质量门,决定哪些 Workflow Check 应成为 Required Check,并为 push/PR 的重复运行、失败重试和变更范围设计过滤策略:Actions CI 配置方法。
参考与延伸
- Workflow syntax for GitHub Actions ↗
- Events that trigger workflows ↗
- Running variations of jobs with a matrix ↗
- Dependency caching reference ↗
- Store and share data with workflow artifacts ↗
- Control the concurrency of workflows and jobs ↗
- Reusing workflows ↗
- GitHub Actions 安全使用参考 ↗
- GitHub Actions 事件参考 ↗
根据技术笔记重新整理,示例使用通用名称。