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

大多数 CI 指南的问题
你搜索 “GitHub Actions unit tests”,找到一篇指南,复制其中的 YAML,它可以工作——但随后会出现:
- 仅文档的 PR 触发了 20 分钟的构建。
- 测试在 CI 中通过,却在生产环境崩溃。
- 某个不稳定的测试随机失败,导致整个团队被卡住。
这些都是真实的问题。下面教你如何解决它们。
1. 路径过滤 — 当没有代码变动时跳过构建

- 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 中构建并测试

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 — 你的代码并不像你想的那样安全

大多数进入生产环境的 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. 只重试失败的测试 —— 而不是整个套件

不稳定的测试是客观存在的。否认它们会让你的团队每周损失数小时。错误的做法是把测试标记为 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. 调试时输出详细信息 —— 不污染正常运行的日志

- 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 中构建并测试

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 — 你的代码并不像你想的那样安全

大多数进入生产环境的 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. 只重试失败的测试 —— 而不是整个套件

不稳定的测试是客观存在的。否认它们会让你的团队每周损失数小时。错误的做法是把测试标记为 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. 调试时输出详细信息 —— 不污染正常运行的日志

- 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. 使用单个配置块取消冗余运行

你推送了一个修复,发现忘了点什么,又再次推送。现在同一个分支出现了两个 CI 运行。第一个是浪费。
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true每个工作流顶部的两行代码。新的推送会立即终止旧的运行。零成本,零思考,立竿见影。
Summary
CI 是代码。 用同样的纪律对待它:没有浪费、信息丰富的失败、易于维护。
