25、持续集成(CI)集成:GitHub Actions配置、GitLab CI配置、Jenkins配置

持续集成,说白了就是让机器替你跑测试。

我见过太多团队,代码写完了,手动编译、手动跑测试、手动打包。一次两次还行,项目一大人就麻了。我自己就吃过这个亏——有一次凌晨三点上线,忘了跑单元测试,结果一个边界条件没覆盖,线上直接崩了。从那以后,我对CI的态度就是:能自动化的,绝不手动。

这一章,咱们就聊聊怎么把CMake和CTest集成到主流的CI平台里。GitHub Actions、GitLab CI、Jenkins,这三个我都在项目里用过,各有各的脾气。

核心思路:CI里跑CMake+CTest,本质上就三步——安装依赖、配置构建、运行测试。不同平台只是语法不同,逻辑一模一样。

GitHub Actions .github/workflows/*.yml GitLab CI .gitlab-ci.yml Jenkins Jenkinsfile / Pipeline 核心步骤:安装依赖 → CMake配置 → 构建 → CTest运行 输出:测试报告 / 构建产物 / 通知

25.1 GitHub Actions 配置

GitHub Actions 是我用得最多的CI平台。原因很简单——代码和CI在一个地方,不用切来切去。

配置文件的路径是 .github/workflows/,文件名随便起,比如 ci.yml。我习惯一个项目只用一个workflow,但分多个job来跑不同平台。

我的习惯:Linux、macOS、Windows各开一个job,矩阵构建(matrix)是GitHub Actions的杀手锏。

下面是一个完整的例子。它会在push和pull request时触发,跑三个操作系统上的CMake构建和CTest测试。

name: CMake CTest CI

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        build_type: [Debug, Release]

    steps:
    - uses: actions/checkout@v4

    - name: 配置CMake
      run: cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }}

    - name: 构建
      run: cmake --build build --config ${{ matrix.build_type }}

    - name: 运行CTest
      run: ctest --test-dir build --output-on-failure -j4

这里有几个细节要注意:

  • --output-on-failure:测试失败时打印详细输出。我当年没加这个,查了半天不知道错在哪。
  • -j4:并行跑测试,省时间。但如果你有资源竞争的问题,记得去掉。
  • --test-dir build:指定构建目录。CMake 3.20+ 才支持这个参数,老版本用 -C build 或者 cd build && ctest

注意:Windows上CMake默认用MSBuild,构建类型通过 --config 指定。Linux/macOS上 --config 会被忽略,因为用的是单配置生成器(如Makefile、Ninja)。

25.2 GitLab CI 配置

GitLab CI 的配置放在项目根目录的 .gitlab-ci.yml 里。它的设计哲学和GitHub Actions不太一样——它用 stages 来定义流水线阶段,每个job属于一个stage。

我个人觉得GitLab CI的语法更接近传统CI,适合做复杂的流水线。比如先编译、再跑单元测试、再跑集成测试、最后打包。

stages:
  - build
  - test

variables:
  BUILD_DIR: build

before_script:
  - apt-get update -qq
  - apt-get install -y -qq cmake g++

build-job:
  stage: build
  script:
    - cmake -B $BUILD_DIR -DCMAKE_BUILD_TYPE=Debug
    - cmake --build $BUILD_DIR
  artifacts:
    paths:
      - $BUILD_DIR/
    expire_in: 1 hour

test-job:
  stage: test
  script:
    - ctest --test-dir $BUILD_DIR --output-on-failure -j4
  dependencies:
    - build-job

这里我用了 artifacts 把构建产物传给后面的job。为什么要这么做?因为GitLab CI的每个job默认是独立运行的,不共享文件系统。你想想看,如果test-job重新编译一遍,那多浪费时间。

避坑指南:我曾经在GitLab CI里忘了加 dependencies,结果test-job拿不到构建产物,直接报错。后来养成了习惯——只要分stage,就显式声明依赖关系。

还有一个常用的技巧:用 rules 控制触发条件。比如只有main分支才跑Release构建:

release-build:
  stage: build
  script:
    - cmake -B $BUILD_DIR -DCMAKE_BUILD_TYPE=Release
    - cmake --build $BUILD_DIR
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

25.3 Jenkins 配置

Jenkins 是老牌CI工具了。我最早接触CI就是用的Jenkins,那时候还是用Web界面点点点配置的。现在嘛,大家都用Pipeline as Code,也就是 Jenkinsfile

Jenkins的灵活性是最高的,但配置起来也最啰嗦。你需要自己管理agent、插件、凭证。不过一旦配好了,它能做的事情远超其他平台。

pipeline {
    agent any

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Configure') {
            steps {
                sh 'cmake -B build -DCMAKE_BUILD_TYPE=Debug'
            }
        }

        stage('Build') {
            steps {
                sh 'cmake --build build -j4'
            }
        }

        stage('Test') {
            steps {
                sh 'ctest --test-dir build --output-on-failure -j4'
            }
            post {
                always {
                    junit 'build/test-results/*.xml'
                }
            }
        }
    }

    post {
        failure {
            emailext(
                subject: "构建失败: ${env.JOB_NAME} - ${env.BUILD_NUMBER}",
                body: "请检查构建日志: ${env.BUILD_URL}",
                to: 'team@example.com'
            )
        }
    }
}

注意看 post 块里的 junit 步骤。它会把CTest生成的JUnit格式的测试报告解析出来,在Jenkins界面上展示趋势图。这个功能我特别喜欢——一眼就能看出测试是变好了还是变差了。

小技巧:要让CTest输出JUnit格式,需要在CMakeLists.txt里设置 CTEST_OUTPUT_ON_FAILURE 环境变量,或者用 ctest --output-junit test-results.xml。CMake 3.21+ 才支持 --output-junit,老版本得自己写脚本转换。

25.4 跨平台测试的注意事项

说完了三个平台的配置,我想聊聊跨平台测试时容易踩的坑。嗯,这里要特别注意。

问题 表现 解决方案
路径分隔符 Windows用反斜杠,Linux用正斜杠 CMake里用 /,或者用 file(TO_CMAKE_PATH)
换行符 Windows是CRLF,Linux是LF Git配置 core.autocrlf input,或者CI里显式转换
动态库后缀 .dll vs .so vs .dylib 用CMake的 CMAKE_SHARED_LIBRARY_SUFFIX 变量
编译器差异 MSVC vs GCC vs Clang 矩阵构建中指定不同的编译器

我曾经在Windows上写了一个测试,路径写死了 /tmp/test.txt。结果在Windows agent上跑,直接报文件找不到。后来学乖了——所有路径都用CMake的 ${CMAKE_CURRENT_BINARY_DIR} 或者 ${CMAKE_CURRENT_SOURCE_DIR} 来拼接。

25.5 测试报告与通知

CI跑完了,测试结果怎么通知到人?

  • GitHub Actions:默认会在PR上显示check状态。还可以用 dorny/test-reporter 这个action生成漂亮的测试报告。
  • GitLab CI:支持JUnit报告,在Merge Request界面直接展示。配置 reports:junit 即可。
  • Jenkins:上面说了,用 junit 步骤。还可以集成邮件、Slack、钉钉通知。

我的建议:不要只发失败通知。成功的通知也要发,但可以降低频率——比如每天一次汇总。否则大家会麻木的,真的。

好了,这一章的内容就到这里。CI配置看起来琐碎,但核心逻辑就那几步。你只要把CMake和CTest的命令写对,剩下的就是不同平台的语法糖而已。

最后提醒一句:CI里的测试一定要和本地开发环境保持一致。我见过有人在CI里用Ninja,本地用Makefile,结果测试顺序不一样,导致偶发失败。统一生成器,统一编译器版本,能省很多麻烦。


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