6、持续集成:GitHub Actions / GitLab CI 配置,自动化构建与测试流水线,构建矩阵与并行任务
持续集成,说白了就是让机器替你跑腿。你提交代码,它自动编译、自动测试、自动报告结果。我见过太多团队,每天花一两个小时手动编译、手动跑测试,还美其名曰「质量把控」。其实,这些重复劳动完全可以用流水线替代。
今天我们就聊聊,在 C++ 大型项目中,怎么搭一套靠谱的 CI 流水线。我会以 GitHub Actions 和 GitLab CI 为例,把构建矩阵、并行任务这些核心概念讲透。
6.1 为什么 C++ 项目更需要 CI?
C++ 的构建和测试,跟 Python、JavaScript 完全不是一个量级。你想想看:
- 编译一次可能十几分钟甚至半小时
- 不同编译器(GCC、Clang、MSVC)行为不一样
- 不同平台(Linux、macOS、Windows)库依赖不同
- Debug 和 Release 配置可能暴露不同的问题
我在项目中遇到过最惨的一次:一个同事在 Windows 上用 MSVC 编译通过,提交后 Linux 上的 GCC 直接报模板实例化错误。因为没有 CI,这个问题直到集成测试才发现,整整耽误了两天。
所以,C++ 项目的 CI 不是「要不要」的问题,而是「怎么搭才高效」的问题。
6.2 GitHub Actions 配置实战
先看一个典型的 C++ 项目流水线。我个人习惯用 GitHub Actions,因为它跟 GitHub 仓库集成得最好,社区 Action 也丰富。
6.2.1 基础流水线结构
一个 .github/workflows/ci.yml 文件,大概长这样:
name: C++ CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: sudo apt-get update && sudo apt-get install -y cmake g++ libboost-all-dev
- name: Configure
run: cmake -B build -DCMAKE_BUILD_TYPE=Release
- name: Build
run: cmake --build build --parallel
- name: Test
run: cd build && ctest --output-on-failure
嗯,这里要注意:--parallel 参数会自动利用机器的多核能力。GitHub 的 runner 默认有 2 核,如果你用自托管 runner,可以充分利用硬件资源。
6.2.2 构建矩阵:一次跑多个配置
构建矩阵是 CI 里最实用的功能之一。它让你用一份配置,同时跑多个编译器、多个构建类型、多个平台。
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
compiler: [gcc, clang]
build_type: [Debug, Release]
exclude:
- os: windows-latest
compiler: clang
- os: macos-latest
compiler: gcc
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Setup compiler
run: |
if [ "${{ matrix.compiler }}" = "gcc" ]; then
echo "CC=gcc" >> $GITHUB_ENV
echo "CXX=g++" >> $GITHUB_ENV
else
echo "CC=clang" >> $GITHUB_ENV
echo "CXX=clang++" >> $GITHUB_ENV
fi
shell: bash
- name: Configure & Build
run: |
cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }}
cmake --build build --parallel
为什么要有 exclude?因为有些组合不常见,或者根本跑不通。比如 Windows 上默认用 MSVC,macOS 上默认用 Apple Clang。强行用 GCC 反而要额外安装,没必要。
6.3 GitLab CI 配置实战
GitLab CI 的配置思路跟 GitHub Actions 类似,但语法不同。它用 .gitlab-ci.yml 文件,核心概念是 stages 和 jobs。
6.3.1 多阶段流水线
stages:
- build
- test
- package
build-job:
stage: build
image: gcc:13
script:
- cmake -B build -DCMAKE_BUILD_TYPE=Release
- cmake --build build --parallel
artifacts:
paths:
- build/
expire_in: 1 hour
test-job:
stage: test
image: gcc:13
dependencies:
- build-job
script:
- cd build && ctest --output-on-failure
package-job:
stage: package
image: gcc:13
dependencies:
- build-job
script:
- cmake --build build --target package
artifacts:
paths:
- build/*.deb
- build/*.rpm
这里有个关键点:artifacts。它把构建产物传递给后续阶段,避免重复编译。我在项目中遇到过一个问题:测试阶段重新编译了一次,白白浪费 15 分钟。后来加上 artifacts,流水线时间直接砍半。
6.3.2 并行任务与依赖管理
GitLab CI 支持 needs 关键字,可以定义任务间的依赖关系,实现更灵活的并行:
stages:
- build
- test
- lint
linux-build:
stage: build
script:
- cmake -B build-linux
- cmake --build build-linux --parallel
windows-build:
stage: build
tags:
- windows
script:
- cmake -B build-win
- cmake --build build-win --parallel
unit-test:
stage: test
needs: ["linux-build"]
script:
- cd build-linux && ctest -R unit
integration-test:
stage: test
needs: ["linux-build"]
script:
- cd build-linux && ctest -R integration
lint-check:
stage: lint
needs: []
script:
- clang-tidy src/*.cpp -- -std=c++20
你看,unit-test 和 integration-test 都依赖 linux-build,但它们之间没有依赖关系,可以并行执行。而 lint-check 不依赖任何构建任务,可以跟构建同时跑。
6.4 构建矩阵与并行任务的核心逻辑
为了让你更直观地理解,我画了一张图:
这张图展示了整个流程:代码提交后,构建矩阵自动展开成多个并行任务,每个任务跑不同的配置组合。所有任务完成后,汇总结果。如果有任何一个任务失败,流水线就标记为失败,并通知相关人员。
6.5 避坑指南与最佳实践
做了这么多年 CI,我踩过不少坑。分享几个最实用的:
1. 缓存依赖,别每次都重新下载
C++ 项目的依赖往往很大。Boost、OpenCV、Qt 这些库,下载编译一次可能半小时。GitHub Actions 和 GitLab CI 都支持缓存:
# GitHub Actions 缓存示例
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ~/.cache/vcpkg
key: ${{ runner.os }}-vcpkg-${{ hashFiles('vcpkg.json') }}
restore-keys: |
${{ runner.os }}-vcpkg-
2. 控制流水线时间,别让队友等太久
我曾经把一个项目的 CI 时间从 45 分钟优化到 12 分钟,做了三件事:
- 启用 ccache 缓存编译产物
- 把单元测试和集成测试拆成并行任务
- 只对 changed files 运行 clang-tidy
记住:CI 是给团队用的,不是给自己看的。超过 20 分钟的流水线,大家就会开始想办法绕过它。
3. 小心「假通过」
我最怕的一种情况:流水线绿了,但代码有问题。为什么会这样?因为测试覆盖率不够,或者测试用例太弱。我建议在 CI 里加上覆盖率报告,低于某个阈值直接失败:
# 在 CMakeLists.txt 中启用覆盖率
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage")
endif()
# CI 中检查覆盖率
- name: Check coverage
run: |
gcovr --fail-under-line=80 --fail-under-branch=60
6.6 总结
持续集成不是银弹,但它能帮你省下大量重复劳动。核心就三点:
- 构建矩阵:用一份配置覆盖多平台、多编译器、多构建类型
- 并行任务:把编译、测试、静态分析拆开跑,充分利用 runner 资源
- 缓存与优化:别让队友等太久,也别让流水线变成形式主义
我个人习惯是:先搭一个最小可用的流水线,然后根据团队反馈逐步优化。别一开始就想搞个大而全的,那样往往撑不过两周就没人维护了。
好了,这一章就到这里。记住:CI 是给团队用的工具,不是展示技术能力的舞台。实用、稳定、快速,才是王道。