Skip to content

GitHub Actions:把质量门自动化挂在每次提交上 ​

引言:靠人记得跑质量门,是不现实的 ​

到这一章,咱们已经攒下好几道“门”:-Wall -Wextra -Werror 让警告挡住编译、-fsanitize 让 UB 和内存错误现形、build_examples.py 保证每个示例都能编、validate_frontmatter.py 保证文档 frontmatter 合法。问题是,**您真的能保证每次提交前都手动把它们全跑一遍吗?**一定会忘。忘了就有漏网的 bug 混进主线。

CI(Continuous Integration,持续集成)就是解决这个的:它在每次 git push 或发 Pull Request 时,自动在一台干净的机器上把咱们的质量门全跑一遍,任何一个失败就让那次提交“变红”、在 GitHub 上明晃晃地标出来、阻止它被合并进主线。本课程用 GitHub Actions 做 CI。这一章咱们逐行拆解本项目真实的 .github/workflows/ci.yml,看 CI 到底跑了什么、怎么跑的,并在本地把这些步骤亲手跑一遍。

workflow 文件长什么样 ​

咱们先看 GitHub Actions 的配置放在哪:.github/workflows/ 目录下,一个 .yml 文件就是一个 workflow。本项目只有一个 ci.yml,它的骨架是:

yaml
name: CI

on:
  push:
    branches: [main, feat/integrate-old-c-code]
  pull_request:
    branches: [main]

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

最顶上 name: CI 是这个 workflow 的名字。on: 是触发条件:push 到 main(以及那个集成分支)时触发、对 main 发 pull_request 时也触发。所以咱们一推代码、或一开/更新 PR,CI 就自动开跑,不用手动按。concurrency 那段是个优化:cancel-in-progress: true 意思是“同一个分支,如果又推了新代码,就把上一轮还在跑的 CI 取消掉”,避免连推三次、后端排着跑三轮同样的活,浪费资源。

咱们再看结构:一个 workflow 由若干 job 组成(下面有四个),每个 job 在一台虚拟机上跑(runs-on: ubuntu-latest,就是 GitHub 提供的 Ubuntu 云主机),每个 job 又由若干 step 组成。job 之间默认并行,step 之间串行。

四个 job:本项目 CI 实际跑什么 ​

ci.yml 里定义了四个 job,正好把前面几章的门串成一条防线。咱们逐个看。

**第一个 build-examples:编译所有示例,当硬门。**这是咱们最核心的一道:

yaml
  build-examples:
    name: 编译 examples (${{ matrix.cc }})
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        cc: [gcc, clang]
    steps:
      - uses: actions/checkout@v4
      - name: 安装构建工具
        run: sudo apt-get update && sudo apt-get install -y cmake ninja-build
      - name: 选择编译器
        run: echo "CC=${{ matrix.cc }}" >> $GITHUB_ENV
      - name: 编译所有 examples(硬门)
        run: python3 scripts/build_examples.py

这里出现了一个新概念:矩阵(strategy.matrix)。cc: [gcc, clang] 让这个 job 复制成两份,一份用 gcc、一份用 clang,并行各跑一次。也就是说“同一个代码,分别用 gcc 和 clang 各编译一遍”(${{ matrix.cc }} 会被替换成 gcc 或 clang),确保咱们的代码不会被“只在 gcc 下能编、换 clang 就挂”坑到。fail-fast: false 让其中一个失败时不立刻取消另一个(两个都跑完,咱们能一次看全两边的问题)。

咱们看 step 这边:uses: actions/checkout@v4 是调用 GitHub 官方提供的 action(可复用的步骤,@v4 是版本号)把仓库代码拉到虚拟机上。接着 apt-get install 装构建工具。echo "CC=..." >> $GITHUB_ENV 把选定的编译器写进环境变量。最后 python3 scripts/build_examples.py 跑第 13 章那个脚本,它退出码非 0(有示例编不过),整个 job 就失败、CI 就红。这就是“硬门”的意思:直接挡住提交,不停留在警告。

**第二个 sanitize:sanitizer 门(呼应第 11 章)。**咱们让它换 clang,带 -fsanitize 再把示例全编一遍:

yaml
  sanitize:
    name: Sanitizer(ASan + UBSan)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: 安装构建工具
        run: ... && sudo apt-get install -y cmake ninja-build clang
      - name: 用 -fsanitize 编译 examples
        env:
          CC: clang
          CFLAGS: -fsanitize=address,undefined -fno-omit-frame-pointer -g
          LDFLAGS: -fsanitize=address,undefined
        run: python3 scripts/build_examples.py

咱们看 env: 那几行,是给这一步设环境变量:CC=clang、CFLAGS 带上第 11 章的 -fsanitize=address,undefined(外加 -fno-omit-frame-pointer 保留帧指针、让 sanitizer 报错时的栈跟踪更准),再配 LDFLAGS 让链接也带上 sanitizer 运行时。这一步跑 build_examples.py 时,CMake 会读到这些环境变量、用 clang 带 sanitizer 去编译。含越界、UAF、UB 的代码,到这一步会编译失败或被 sanitizer 拦下,CI 就靠它把第 11 章那套运行期检查自动化了。(ci.yml 里那条注释也诚实说明了:目前 sanitizer 门覆盖的是 CMake 子项目,根 CMakeLists 统一后会把全部示例纳入。)

第三、四个 docs 和 format-check:文档和格式门。docs job 装好 Python,跑 validate_frontmatter.py(第 1 章起一直在用的 frontmatter 校验)和 markdownlint(检查 Markdown 写法)。format-check job 装 clang-format,对 examples 下的所有 .c/.h 跑 clang-format --dry-run --Werror,--dry-run 只检查不改动,--Werror 把“格式不合规”当错误,任何文件格式不对,退出码非 0、CI 红(这正好是下一章第 18 章的主题)。这俩 job 保证咱们提交进来的代码和文档是干净、合规的。

本地把这些 job 跑一遍 ​

GitHub Actions 跑在 GitHub 的云端机器上,咱们本地没法完整触发它,但 CI 跑的命令就是上面 yml 里写的那些,完全可以手动复现,确认它们“就是 CI 在做的事”。咱们模拟 docs、format-check、sanitize 三个 job 的核心步骤(前面章节其实已经跑过,这里串起来看):

终端
$ python3 scripts/validate_frontmatter.py          # = docs job
✅ frontmatter 全部通过。

$ git ls-files 'examples/*.c' 'examples/*.h' | xargs clang-format --dry-run --Werror   # = format-check job
$ echo $?
0                                  ← 退出码 0,examples 格式全部合规

$ CC=clang CFLAGS="-fsanitize=address,undefined -g" python3 scripts/build_examples.py  # = sanitize job
✅ examples 全部构建通过。

咱们这边三个步骤退出码都是 0,这就是您在 GitHub 上看到 CI 绿勾时,背后实际发生的事。反过来,如果哪次提交让某个示例编不过、或格式不对、或触发 sanitizer,对应的 job 就非 0 退出、CI 变红。

失败挡合并:把前面所有门串起来 ​

理解了这四个 job,咱们就明白 CI 的意义了:它把第 9 章的 -Werror(藏在每个示例的编译旗标里)、第 11 章的 sanitizer、第 13 章的“每个示例都能编”、格式与文档合规,全部自动化、每次提交都强制跑一遍。在 GitHub 的分支保护里,咱们可以把“CI 必须绿”设成合并 PR 的前提条件,于是任何一道门没过,代码就合不进 main。人还是会忘、会偷懒,但 CI 不会。这一章是开发环境这一程的收尾:从工具链、编译流程、警告、标准与优化、sanitizer、make/CMake、GDB、Git,到这章的 CI,一条“让代码可靠地构建和验证”的链子就完整了。

小结 ​

CI 用 GitHub Actions 把质量门挂到每次 push/pull_request 上自动跑,失败就红、挡住合并,省得靠人记得手动跑。配置在 .github/workflows/*.yml,骨架是 name/on(触发条件)/jobs(并行)/steps(串行)/runs-on(虚拟机),还能用 matrix(如 gcc+clang 两份并行)、uses: actions/...@vN(复用现成 action)、concurrency(同分支新推送取消旧轮)。本项目 ci.yml 有四个 job:build-examples(gcc/clang 矩阵跑 build_examples.py,退出码非 0 即红,是硬门)、sanitize(clang 带 -fsanitize=address,undefined -fno-omit-frame-pointer 再编一遍,呼应第 11 章)、docs(validate_frontmatter.py + markdownlint)、format-check(clang-format --dry-run --Werror,呼应第 18 章)。咱们在本地把这四个 job 的核心命令都复现了一遍、确认退出码 0 就是 GitHub 上那个绿勾。CI 的本质是把前面所有章节立起来的门自动化强制执行,任何一道没过就合不进主线,人可以忘、CI 不会。下一章咱们把最后一道 format-check 展开讲清楚,看 clang-format 怎么统一代码风格、以及怎么在 CI 里和本地用同一份 .clang-format 守住它。

参考资源 ​

  • GitHub Actions 文档:workflow 语法(on/jobs/steps/runs-on)、strategy.matrix 矩阵、actions/checkout、actions/setup-python、concurrency
  • 本项目 .github/workflows/ci.yml(四个 job 的真实配置)
  • 第 11 章:Sanitizer 门禁(CI 的 sanitize job)、第 13 章:CMake 入门(build_examples.py 编的 CMake 子项目)、第 9 章:警告旗标(-Werror 在编译旗标里)
  • 第 18 章:格式化与质量门(format-check job 用的 clang-format 详解)

87bb5f2 · 87bb5f2 · 2026-09-21