21、构建自动化:CI/CD概念、Jenkins/GitHub Actions配置、自动化测试集成

说实话,很多C语言开发者对CI/CD有误解。

他们觉得这是Java、Python那些“高级语言”才玩的东西。C语言嘛,make一下、gcc编译一把,能跑就行。我以前也这么想,直到有一次,一个同事改了头文件,没通知任何人。结果整个团队编译出来的固件,一半跑飞了。那天晚上,我们排查到凌晨三点。

嗯,从那以后,我下定决心:C语言项目,必须上CI/CD。

CI/CD到底是什么?

说白了,CI/CD就是给你的代码请了个“自动管家”。

  • CI(持续集成):你每次提交代码,系统自动拉下来、编译、跑测试。有问题立刻告诉你。
  • CD(持续交付/部署):通过测试的代码,自动打包、发布、甚至部署到目标设备。

你想想看,嵌入式C项目最怕什么?最怕“在我机器上能跑”。CI/CD就是专门治这个毛病的。

核心价值:把“人肉检查”变成“机器检查”。每次提交都跑一遍完整的构建+测试流程,确保代码库始终处于健康状态。

我建议的C语言CI/CD流水线结构

我在多个嵌入式项目中总结了一套标准流程。直接上图:

C语言CI/CD流水线标准流程 代码提交 git push / PR 静态检查 cppcheck / clang-tidy 编译构建 make / cmake 单元测试 CUnit / Unity 集成测试 硬件在环 / 模拟器 打包发布 固件镜像 / 文档 部署 OTA / 烧录 ❌ 失败 → 通知开发者 → 阻塞合并 ✅ 成功 → 自动合并 → 触发下一阶段

这个流程看起来复杂,但核心逻辑很简单:每一道关卡都在帮你拦截问题。静态检查抓代码风格和潜在bug,编译构建确保能通过,单元测试验证逻辑正确性,集成测试检查模块间配合。

GitHub Actions配置实战

我个人更偏爱GitHub Actions。为什么?因为对C语言项目来说,它足够轻量,配置简单,而且免费额度对大多数团队都够用。

下面是我在一个STM32项目里用的配置。直接看代码:

# .github/workflows/ci.yml
name: C Project CI

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

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    
    steps:
    - name: 检出代码
      uses: actions/checkout@v3
      
    - name: 安装工具链
      run: |
        sudo apt-get update
        sudo apt-get install -y gcc-arm-none-eabi cmake cppcheck
        
    - name: 静态代码分析
      run: |
        cppcheck --enable=all --suppress=missingIncludeSystem \
          --error-exitcode=1 src/ include/
          
    - name: 编译固件
      run: |
        mkdir build && cd build
        cmake .. -DCMAKE_TOOLCHAIN_FILE=../arm-gcc.cmake
        make -j$(nproc)
        
    - name: 运行单元测试
      run: |
        cd build
        ctest --output-on-failure

小技巧:在C语言项目中,cppcheck--error-exitcode=1参数很关键。它让静态检查发现问题时返回非零值,从而让CI流程失败。我曾经见过有人忘了加这个,结果代码里一堆警告,CI还一直绿着……

Jenkins配置要点

如果你的项目在本地服务器上,或者需要更复杂的流水线控制,Jenkins是更好的选择。

我建议用Pipeline as Code的方式,把流水线定义写在Jenkinsfile里。这样版本控制也方便。

// Jenkinsfile
pipeline {
    agent any
    
    stages {
        stage('检出代码') {
            steps {
                checkout scm
            }
        }
        
        stage('静态分析') {
            steps {
                sh 'cppcheck --enable=all --suppress=missingIncludeSystem src/'
            }
        }
        
        stage('编译') {
            steps {
                sh 'mkdir -p build && cd build && cmake .. && make'
            }
            post {
                failure {
                    // 编译失败时,保存编译日志
                    archiveArtifacts artifacts: 'build/*.log'
                }
            }
        }
        
        stage('单元测试') {
            steps {
                sh 'cd build && ctest --output-on-failure'
            }
            post {
                always {
                    // 无论成败,都保存测试报告
                    junit 'build/test-results/*.xml'
                }
            }
        }
        
        stage('固件打包') {
            steps {
                sh './scripts/package_firmware.sh'
            }
        }
    }
    
    post {
        failure {
            // 发送通知
            emailext(
                subject: "构建失败: ${env.JOB_NAME} - ${env.BUILD_NUMBER}",
                body: "请检查构建结果: ${env.BUILD_URL}",
                to: 'team@example.com'
            )
        }
    }
}

注意:Jenkins的post块非常有用。我曾经踩过一个坑:测试失败了,但没人知道,因为邮件通知没配。后来我养成了习惯,任何CI流程都必须配置失败通知。不然CI就失去了意义。

自动化测试集成

测试是CI/CD的灵魂。没有测试的CI,说白了就是个编译检查器。

对于C语言项目,我推荐以下测试框架:

框架 适用场景 特点
CUnit 桌面端C项目 成熟稳定,文档丰富
Unity 嵌入式C项目 轻量级,适合资源受限环境
CMock 需要mock的场景 与Unity配合,自动生成桩函数
Check Linux环境 支持fork模式,隔离性好

我个人习惯用Unity + CMock的组合。为什么?因为嵌入式项目里,硬件依赖太多。你想想看,测试一个传感器驱动,总不能每次都接真硬件吧?用CMock生成桩函数,模拟硬件行为,测试就能在CI环境里跑起来。

下面是一个Unity测试的示例:

// test/test_temperature_sensor.c
#include "unity.h"
#include "temperature_sensor.h"

// 模拟I2C读取函数
int mock_i2c_read(uint8_t addr, uint8_t reg, uint8_t* data) {
    *data = 0x1A;  // 模拟返回25°C
    return 0;
}

void setUp(void) {
    // 注册mock函数
    i2c_read_ExpectAndReturn(0x48, 0x00, NULL, 0);
    i2c_read_AddCallback(mock_i2c_read);
}

void tearDown(void) {
    // 清理
}

void test_sensor_reads_temperature(void) {
    float temp = temperature_sensor_read();
    TEST_ASSERT_FLOAT_WITHIN(0.5, 25.0, temp);
}

int main(void) {
    UNITY_BEGIN();
    RUN_TEST(test_sensor_reads_temperature);
    return UNITY_END();
}

关键点:测试代码要和产品代码分开管理。我建议在项目根目录下建test/文件夹,每个模块对应一个测试文件。CMake里用add_test()注册,这样ctest就能一键运行所有测试。

避坑指南

做CI/CD这几年,我踩过的坑不少。挑几个典型的说说:

  • 我曾经把整个工具链都放在CI脚本里安装,结果每次构建都要等10分钟装依赖。后来改用Docker镜像,构建时间从10分钟降到了2分钟。
  • 我曾经忽略了测试的隔离性,测试用例之间共享全局变量,结果一个测试失败导致后续全挂。后来每个测试用例都独立初始化,问题解决。
  • 我曾经把CI跑在Windows上,结果路径分隔符、换行符各种问题。现在统一用Linux环境,省心多了。

我的建议:刚开始做CI/CD时,不要追求一步到位。先配一个最简单的流程:代码提交→编译→跑测试。跑通了,再慢慢加静态检查、打包、部署。小步快跑,比憋大招靠谱。

好了,关于CI/CD的内容就聊到这里。记住一句话:自动化不是目的,质量才是。CI/CD只是帮你把质量检查变成自动化的手段。工具选GitHub Actions还是Jenkins不重要,重要的是你真正用起来了,并且坚持下去了。


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