OFFSET偏移
← 文章目录OFFSET / FIELD NOTESNO. 106

GitHub Actions 执行模型

理解事件、Workflow、Job、Runner 与 Step,以及矩阵、缓存和产物的不同作用。

所在专题GitHub Actions 与 CI ↗
约 27 分钟阅读开始阅读

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.sh

needs 表达的是拓扑约束,不会自动传文件或环境。需要传小型字符串时用 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 Check 应成为 Required Check,并为 push/PR 的重复运行、失败重试和变更范围设计过滤策略:Actions CI 配置方法。

参考与延伸

  1. Workflow syntax for GitHub Actions ↗
  2. Events that trigger workflows ↗
  3. Running variations of jobs with a matrix ↗
  4. Dependency caching reference ↗
  5. Store and share data with workflow artifacts ↗
  6. Control the concurrency of workflows and jobs ↗
  7. Reusing workflows ↗
  8. GitHub Actions 安全使用参考 ↗
  9. GitHub Actions 事件参考 ↗

根据技术笔记重新整理,示例使用通用名称。

沿专题继续阅读

GitHub Actions 与 CI

  1. 01GitHub Actions 执行模型
  2. 02GitHub Actions CI 配置
  3. 03GitHub Actions 安全与排障
  4. 04Nix CI:求值、构建与输出契约
CONTINUE READING / 下一篇GitHub Actions CI 配置

从本地质量门设计 CI,并用稳定的检查状态接入仓库合并规则。