更安全的代码交付:真正重要的 GitHub Actions 模式

发布: (2026年3月16日 GMT+8 06:11)
10 分钟阅读
原文: Dev.to

Source: Dev.to

《Ship Safer Code:真正重要的 GitHub Actions 模式》封面图片

prabhu ponnambalam

大多数 CI 指南的问题

你搜索 “GitHub Actions unit tests”,找到一篇指南,复制其中的 YAML,它可以工作——但随后会出现:

  • 仅文档的 PR 触发了 20 分钟的构建。
  • 测试在 CI 中通过,却在生产环境崩溃。
  • 某个不稳定的测试随机失败,导致整个团队被卡住。

这些都是真实的问题。下面教你如何解决它们。

1. 路径过滤 — 当没有代码变动时跳过构建

skip tests when no code changed

- name: Detect changes
  id: changes
  uses: dorny/paths-filter@v3
  with:
    filters: |
      code:
        - '**/*.cpp'
        - '**/*.h'
        - '**/*.hpp'
        - '**/CMakeLists.txt'

- name: Run Tests
  if: steps.changes.outputs.code == 'true'
  run: make run-tests

真实影响: 以前在 README.md 中的一个拼写错误会消耗 20 分钟的 runner 时间。现在它能在不到 5 秒内完成。

2. 在 Docker 中构建并测试

GitHub Actions runner box

Runner 环境漂移是可复现性的大隐患。Ubuntu 版本变了,系统库更新了,结果你的 CI 绿灯莫名其妙地变红了。

- name: Build Docker image with tests enabled
  run: |
    docker build . -f Dockerfile -t myapp-ci \
      --build-arg WITH_TESTS=true

- name: Run tests inside container
  run: |
    mkdir -p ./test-output
    docker run --rm \
      -v ./test-output:/app/build/Testing/Temporary \
      myapp-ci \
      ctest --test-dir /app/build --output-on-failure

为什么要挂载 ./test-output 当测试失败时,你需要日志。没有挂载,容器退出后日志就会丢失。

3. 运行 ASAN 与 TSAN — 你的代码并不像你想的那样安全

sanitizer

大多数进入生产环境的 C++ bug 本可以在以下阶段被拦截:

  • AddressSanitizer (ASAN) – 缓冲区溢出、使用后释放、内存泄漏
  • ThreadSanitizer (TSAN) – 数据竞争、死锁
jobs:
  release:
    uses: ./.github/workflows/test.yml
    with:
      preset: release

  asan:
    uses: ./.github/workflows/test.yml
    with:
      preset: asan

  tsan:
    needs: asan          # 交错执行——两者都很耗 CPU
    uses: ./.github/workflows/test.yml
    with:
      preset: tsan

⚠️ 不要同时运行 ASAN 和 TSAN。 它们会争夺同一 runner 的资源,导致因资源饥饿产生的假阳性错误,而不是实际的 bug。使用 needs: 将它们顺序执行。

4. 只重试失败的测试 —— 而不是整个套件

Retry failed tests

不稳定的测试是客观存在的。否认它们会让你的团队每周损失数小时。错误的做法是把测试标记为 DISABLED_ 或直接忽略它们。正确的做法:

- name: Run Tests
  run: ctest --test-dir build --output-on-failure --parallel $(nproc)

- name: Retry Failed Tests
  if: failure()
  run: |
    ctest --test-dir build \
      --rerun-failed \
      --output-on-failure \
      --repeat until-pass:3

--rerun-failed 是 CTest 的内置功能。它会读取上一次运行的结果,只执行失败的测试。一次 20 分钟的全套重跑可以压缩到 90 秒的针对性重试。

5. 调试时输出详细信息 —— 不污染正常运行的日志

![verbose toggle](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-## 大多数 CI 指南的问题

你搜索 “GitHub Actions unit tests”,找到一篇指南,复制其中的 YAML,它可以工作——但随后会出现:

  • 仅文档的 PR 触发了 20 分钟的构建。
  • 测试在 CI 中通过,却在生产环境崩溃。
  • 某个不稳定的测试随机失败,导致整个团队被卡住。

这些都是真实的问题。下面教你如何解决它们。

1. 路径过滤 — 当没有代码变动时跳过构建

skip tests when no code changed

- name: Detect changes
  id: changes
  uses: dorny/paths-filter@v3
  with:
    filters: |
      code:
        - '**/*.cpp'
        - '**/*.h'
        - '**/*.hpp'
        - '**/CMakeLists.txt'

- name: Run Tests
  if: steps.changes.outputs.code == 'true'
  run: make run-tests

真实影响: 以前在 README.md 中的一个拼写错误会消耗 20 分钟的 runner 时间。现在它能在不到 5 秒内完成。

2. 在 Docker 中构建并测试

GitHub Actions runner box

Runner 环境漂移是可复现性的大隐患。Ubuntu 版本变了,系统库更新了,结果你的 CI 绿灯莫名其妙地变红了。

- name: Build Docker image with tests enabled
  run: |
    docker build . -f Dockerfile -t myapp-ci \
      --build-arg WITH_TESTS=true

- name: Run tests inside container
  run: |
    mkdir -p ./test-output
    docker run --rm \
      -v ./test-output:/app/build/Testing/Temporary \
      myapp-ci \
      ctest --test-dir /app/build --output-on-failure

为什么要挂载 ./test-output 当测试失败时,你需要日志。没有挂载,容器退出后日志就会丢失。

3. 运行 ASAN 与 TSAN — 你的代码并不像你想的那样安全

sanitizer

大多数进入生产环境的 C++ bug 本可以在以下阶段被拦截:

  • AddressSanitizer (ASAN) – 缓冲区溢出、使用后释放、内存泄漏
  • ThreadSanitizer (TSAN) – 数据竞争、死锁
jobs:
  release:
    uses: ./.github/workflows/test.yml
    with:
      preset: release

  asan:
    uses: ./.github/workflows/test.yml
    with:
      preset: asan

  tsan:
    needs: asan          # 交错执行——两者都很耗 CPU
    uses: ./.github/workflows/test.yml
    with:
      preset: tsan

⚠️ 不要同时运行 ASAN 和 TSAN。 它们会争夺同一 runner 的资源,导致因资源饥饿产生的假阳性错误,而不是实际的 bug。使用 needs: 将它们顺序执行。

4. 只重试失败的测试 —— 而不是整个套件

Retry failed tests

不稳定的测试是客观存在的。否认它们会让你的团队每周损失数小时。错误的做法是把测试标记为 DISABLED_ 或直接忽略它们。正确的做法:

- name: Run Tests
  run: ctest --test-dir build --output-on-failure --parallel $(nproc)

- name: Retry Failed Tests
  if: failure()
  run: |
    ctest --test-dir build \
      --rerun-failed \
      --output-on-failure \
      --repeat until-pass:3

--rerun-failed 是 CTest 的内置功能。它会读取上一次运行的结果,只执行失败的测试。一次 20 分钟的全套重跑可以压缩到 90 秒的针对性重试。

5. 调试时输出详细信息 —— 不污染正常运行的日志

![verbose toggle](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-## 大多数 CI 指南的问题

你搜索 “GitHub Actions unit tests”,找到一篇指南,复制其中的 YAML,它可以工作——但随后会出现:

  • 仅文档的 PR 触发了 20 分钟的构建。
  • 测试在 CI 中通过,却在生产环境崩溃。
  • 某个不稳定的测试随机失败,导致整个团队被卡住。

这些都是真实的问题。下面教你如何解决它们。

1. 路径过滤 — 当没有代码变动时跳过构建

![skip tests when no code changed](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2C

-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftonlb35bmo190yxk0q4w.png)

- name: Run Tests (normal)
  run: ctest --test-dir build --output-on-failure

- name: Run Tests (verbose)
  if: github.event.inputs.verbose == 'true'
  run: ctest --test-dir build --output-on-failure --verbose

当需要深入诊断时,切换 verbose 输入;否则保持输出简洁,适用于日常运行。

详细测试输出

每次运行时,冗长的测试输出会产生成千上万行噪声。但当你在凌晨 2 点调试时,你需要每一个字节。

- name: Run Tests
  if: runner.debug != '1'
  run: ctest --test-dir build --output-on-failure

- name: Run Tests (verbose)
  if: runner.debug == '1'
  run: ctest --test-dir build --verbose

启用调试模式:GitHub Actions → 重新运行作业 → 启用调试日志。无需更改 YAML,也不需要新提交。只需切换一下开关。

6. 使用单个配置块取消冗余运行

concurrency cancel

你推送了一个修复,发现忘了点什么,又再次推送。现在同一个分支出现了两个 CI 运行。第一个是浪费。

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

每个工作流顶部的两行代码。新的推送会立即终止旧的运行。零成本,零思考,立竿见影。

Summary

CI 是代码。 用同样的纪律对待它:没有浪费、信息丰富的失败、易于维护。

0 浏览
Back to Blog

相关文章

阅读更多 »