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 反而要额外安装,没必要。

我的经验:构建矩阵不是越大越好。我见过有人配了 12 个组合,每次提交跑一个多小时。实际上,你只需要覆盖主流平台 + 主流编译器即可。Linux + GCC、Windows + MSVC、macOS + Clang,这三个组合基本能覆盖 90% 的问题。

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-testintegration-test 都依赖 linux-build,但它们之间没有依赖关系,可以并行执行。而 lint-check 不依赖任何构建任务,可以跟构建同时跑。

注意:并行任务不是越多越好。GitLab 的免费 runner 有并发限制,同时跑太多任务反而会排队。我曾经在一个项目里配了 8 个并行任务,结果每个任务都在等 runner 资源,总耗时反而比串行还长。建议根据实际 runner 数量,控制在 3-5 个并行任务。

6.4 构建矩阵与并行任务的核心逻辑

为了让你更直观地理解,我画了一张图:

CI 构建矩阵与并行任务核心逻辑 代码提交 构建矩阵展开(strategy.matrix) os: [ubuntu, windows, macos] × compiler: [gcc, clang] × build_type: [Debug, Release] 任务 1 ubuntu + gcc + Debug 编译 + 单元测试 任务 2 ubuntu + clang + Release 编译 + 集成测试 任务 3 windows + msvc + Debug 编译 + 静态分析 结果汇总:全部通过 / 部分失败 通知 / 阻断合并

这张图展示了整个流程:代码提交后,构建矩阵自动展开成多个并行任务,每个任务跑不同的配置组合。所有任务完成后,汇总结果。如果有任何一个任务失败,流水线就标记为失败,并通知相关人员。

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 是给团队用的工具,不是展示技术能力的舞台。实用、稳定、快速,才是王道。


公众号:蓝海资料掘金营,微信deep3321