25、持续集成(CI)集成:GitHub Actions配置、GitLab CI配置、Jenkins配置
持续集成,说白了就是让机器替你跑测试。
我见过太多团队,代码写完了,手动编译、手动跑测试、手动打包。一次两次还行,项目一大人就麻了。我自己就吃过这个亏——有一次凌晨三点上线,忘了跑单元测试,结果一个边界条件没覆盖,线上直接崩了。从那以后,我对CI的态度就是:能自动化的,绝不手动。
这一章,咱们就聊聊怎么把CMake和CTest集成到主流的CI平台里。GitHub Actions、GitLab CI、Jenkins,这三个我都在项目里用过,各有各的脾气。
核心思路:CI里跑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,结果测试顺序不一样,导致偶发失败。统一生成器,统一编译器版本,能省很多麻烦。