4、持续集成(CI)基础:CI/CD概念、Jenkins/GitLab CI/GitHub Actions选型、Pipeline基本结构

4.1 先聊聊CI/CD到底是个啥

很多做嵌入式开发的朋友,一听到CI/CD就觉得是互联网后端的事。其实不然。我最早接触CI是在一个车载项目里,团队十几个人,每次合并代码都要手动编译、手动烧录、手动测试。结果呢?一周能出两次编译错误,烧录进去才发现某人的驱动改了接口。

说白了,持续集成(CI)就是让代码每次提交后,自动完成编译、静态检查、单元测试等一系列动作。而持续交付/部署(CD)则是把通过CI的产物,自动部署到测试环境甚至生产环境。

对于嵌入式场景,我们的CD通常就是自动烧录到目标板,或者生成固件包。嗯,这里要注意,嵌入式CD和纯软件CD不太一样——我们得考虑硬件环境、烧录工具链、甚至多板并行烧录的问题。

核心观点:CI/CD不是银弹,但它能帮你把「人肉重复劳动」变成「机器自动检查」。我见过太多团队把时间浪费在「编译-等-发现错误-再编译」的循环里,CI就是打破这个循环的钥匙。

4.2 三大主流CI工具选型

目前嵌入式领域用得最多的CI工具,无非是Jenkins、GitLab CI和GitHub Actions。我三个都用过,各有各的脾气。下面这张表是我个人的选型参考:

维度 Jenkins GitLab CI GitHub Actions
部署方式 自建服务器,需维护 自建或SaaS 纯云端(GitHub提供)
配置复杂度 较高,需装插件 中等,YAML配置 较低,YAML配置
嵌入式适配 强,插件生态丰富 中,需自定义Runner 中,依赖自托管Runner
硬件烧录支持 可通过脚本+Agent 需配置物理Runner 需自托管Runner连接烧录器
适合团队规模 中大型团队 中小型团队 小型团队/个人项目
学习成本 高 中 低

4.2.1 Jenkins——老牌劲旅

Jenkins是我用得最久的CI工具。它的优势在于插件生态极其丰富,几乎你能想到的嵌入式工具链都有对应插件。比如ARM GCC、IAR、Keil、甚至J-Link烧录插件。

但缺点也很明显——配置起来像搭积木,但积木太多容易乱。我曾经维护过一个Jenkins实例,光插件就装了60多个,升级一次提心吊胆。

我的建议:如果你的团队已经有运维人员,或者项目对硬件烧录有强依赖(比如多板并行烧录),Jenkins仍然是首选。但要做好Pipeline即代码(Jenkinsfile)的规范化。

4.2.2 GitLab CI——一体化体验

GitLab CI最大的好处是代码仓库和CI一体化。你不需要单独搭建CI服务器,直接在项目里放一个.gitlab-ci.yml文件就行。

对于嵌入式项目,GitLab CI的Runner机制很灵活。你可以把一台连着烧录器的Windows机器注册为Runner,专门负责固件烧录。我在一个IoT项目中就这么干过——用树莓派当Runner,连上J-Link,自动烧录到开发板。

注意:GitLab CI的免费版有流水线执行时长限制。如果你的编译任务动辄半小时以上,建议自建Runner,别用共享Runner。

4.2.3 GitHub Actions——云端新贵

GitHub Actions这几年发展很快。它的Marketplace里有大量现成的Action,比如setup-arm-gcc、upload-firmware等,拿来就能用。

但嵌入式场景有个硬伤——硬件烧录。GitHub Actions默认运行在云端,没法直接连烧录器。解决方案是自托管Runner,把一台本地机器注册为Runner,然后通过self-hosted标签调度任务。

我个人习惯:开源项目用GitHub Actions,企业内部项目用GitLab CI或Jenkins。原因很简单——开源项目不需要考虑硬件烧录,而企业项目往往需要和硬件深度绑定。

4.3 Pipeline基本结构

不管用哪个工具,Pipeline的结构都大同小异。我画了一张通用的CI Pipeline流程图,你看完就明白了:

嵌入式CI Pipeline通用结构 代码检出 git checkout 静态检查 lint / cppcheck 编译构建 make / cmake 单元测试 Ceedling / Unity 固件打包 bin / hex 生成 烧录部署 J-Link / OpenOCD 冒烟测试 LED / 串口检测 ❌ 失败告警 ✅ 虚线:失败路径 实线:成功路径

4.3.1 一个最小化的Pipeline示例

以GitLab CI为例,一个嵌入式项目的.gitlab-ci.yml大概长这样:

stages:
  - build
  - test
  - deploy

build-job:
  stage: build
  script:
    - mkdir -p build
    - cd build
    - cmake ..
    - make -j4
  artifacts:
    paths:
      - build/firmware.bin

test-job:
  stage: test
  script:
    - ./build/unit_tests
  dependencies:
    - build-job

deploy-job:
  stage: deploy
  script:
    - JLinkExe -device STM32F407VG -if SWD -speed 4000 -CommanderScript flash.jlink
  dependencies:
    - build-job
  only:
    - main

你看,结构其实很简单:三个阶段——编译、测试、部署。每个阶段里可以跑任意脚本。这就是Pipeline的核心思想:把流程拆成阶段,每个阶段只做一件事,做好就传给下一阶段。

避坑指南:我曾经在Pipeline里把所有事情写在一个stage里,结果编译失败时连日志都找不到在哪。后来强制自己每个stage只做一件事,出问题一眼就能定位。

4.3.2 Pipeline的关键设计原则

  1. 阶段独立:每个stage失败不影响其他stage(除非你设置了依赖)。
  2. 产物传递:编译产物(如.bin文件)通过artifacts传递给后续stage。
  3. 条件触发:只有特定分支(如main)才触发烧录部署,避免开发分支乱烧硬件。
  4. 超时控制:嵌入式编译可能很慢,设置timeout防止Pipeline卡死。

4.4 嵌入式CI的特殊考量

和纯软件CI不同,嵌入式CI有几个坑,我踩过不少:

  • 工具链授权:IAR、Keil这类商业编译器需要license。我建议用Docker封装工具链,或者用浮动license服务器。
  • 硬件资源竞争:如果只有一块开发板,多个Pipeline同时跑烧录会冲突。解决方案是加锁机制,或者用硬件池。
  • 烧录速度:有些MCU烧录一次要几十秒,如果每次提交都烧录,团队会疯掉。我一般只在合并到main时触发烧录。
  • 测试环境还原:烧录前要确保开发板处于已知状态。我习惯在烧录前先执行一次硬件复位脚本。

重要提醒:别把生产环境的烧录器和开发环境的混在一起。我曾经因为CI误烧了产线板子,导致返工了一整天。后来严格区分了开发烧录器和产线烧录器的标签。

4.5 小结

CI/CD不是锦上添花,而是嵌入式团队从「作坊式开发」走向「工程化开发」的必经之路。选型时别盲目追新,根据团队规模和硬件环境来定。Pipeline结构记住一句话:拆得越细,查错越容易。