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流程图,你看完就明白了:
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的关键设计原则
- 阶段独立:每个stage失败不影响其他stage(除非你设置了依赖)。
- 产物传递:编译产物(如.bin文件)通过artifacts传递给后续stage。
- 条件触发:只有特定分支(如main)才触发烧录部署,避免开发分支乱烧硬件。
- 超时控制:嵌入式编译可能很慢,设置timeout防止Pipeline卡死。
4.4 嵌入式CI的特殊考量
和纯软件CI不同,嵌入式CI有几个坑,我踩过不少:
- 工具链授权:IAR、Keil这类商业编译器需要license。我建议用Docker封装工具链,或者用浮动license服务器。
- 硬件资源竞争:如果只有一块开发板,多个Pipeline同时跑烧录会冲突。解决方案是加锁机制,或者用硬件池。
- 烧录速度:有些MCU烧录一次要几十秒,如果每次提交都烧录,团队会疯掉。我一般只在合并到main时触发烧录。
- 测试环境还原:烧录前要确保开发板处于已知状态。我习惯在烧录前先执行一次硬件复位脚本。
重要提醒:别把生产环境的烧录器和开发环境的混在一起。我曾经因为CI误烧了产线板子,导致返工了一整天。后来严格区分了开发烧录器和产线烧录器的标签。
4.5 小结
CI/CD不是锦上添花,而是嵌入式团队从「作坊式开发」走向「工程化开发」的必经之路。选型时别盲目追新,根据团队规模和硬件环境来定。Pipeline结构记住一句话:拆得越细,查错越容易。