4. CI/CD平台选型与流水线骨架

固件项目的CI/CD与纯软件项目有一个根本差异:构建产物不是可执行文件或容器镜像,而是需要烧录到真实芯片上的二进制固件。这意味着流水线不仅要完成编译,还要处理交叉编译工具链、签名密钥、硬件在环测试等环节。平台选型的第一步,是判断哪个系统能最自然地承载这些环节。

三大平台的核心差异

Jenkins、GitLab CI、GitHub Actions三者的架构模型不同,直接决定了Runner的部署方式和流水线的可维护性。

Jenkins是自托管为主的老牌方案。它的核心是Jenkins Controller加Agent节点,所有构建逻辑通过Jenkinsfile或界面配置定义。优势在于对私有网络、自定义硬件(如连接J-Link的测试机)的接入非常灵活,插件生态覆盖嵌入式工具链。代价是Controller需要自行维护、升级和安全加固,插件版本冲突是常见故障源。

GitLab CI与代码仓库深度绑定,流水线由仓库根目录的.gitlab-ci.yml定义,Runner通过注册令牌接入。它的强项是环境隔离和缓存机制成熟,适合已经在用GitLab管理固件源码的团队。Runner可以部署在Docker、Shell或Kubernetes执行器中,其中Shell执行器最适合需要直接访问USB调试器的固件测试场景。

GitHub Actions采用托管Runner加自托管Runner混合模式,工作流由.github/workflows/下的YAML定义。托管Runner开箱即用,但无法访问本地硬件;固件构建通常需要自托管Runner,并配合runs-on标签路由到带工具链的机器。它的Marketplace Action复用度高,但第三方Action的供应链安全需要额外审查——这对固件安全课程而言是一个必须纳入考量的风险点。

固件CI/CD平台选型与Runner部署拓扑 Jenkins Controller(自维护) Agent(Shell/Docker) USB调试器 / J-Link GitLab CI .gitlab-ci.yml Runner(注册令牌接入) Shell执行器 / 硬件测试机 GitHub Actions .github/workflows/*.yml 托管Runner / 自托管Runner runs-on 标签路由 首个固件构建流水线骨架(以GitLab CI为例) checkout 拉取源码+子模块 toolchain arm-none-eabi-gcc build cmake --build sign 签名+哈希校验 artifact .bin/.hex 产物:firmware.bin + firmware.hex + firmware.map + sha256sum.txt 失败条件:编译警告视为错误 / 签名失败 / 哈希不匹配

Runner部署的关键决策

Runner是流水线真正执行构建的机器。固件项目对Runner有三个硬性要求:能访问交叉编译工具链、能访问签名密钥、能连接目标硬件。这决定了Runner不能随意使用公共托管资源。

以GitLab Runner为例,注册命令如下:

gitlab-runner register \
  --url https://gitlab.example.com/ \
  --registration-token $RUNNER_TOKEN \
  --executor shell \
  --tag-list "firmware,arm,jlink" \
  --description "firmware-build-runner-01"

这里选择shell执行器而非docker,原因是固件测试阶段需要直接访问宿主机的USB设备。如果使用Docker执行器,必须额外配置--device参数挂载J-Link,且容器内需要安装对应的udev规则,复杂度显著上升。标签firmware,arm,jlink用于在流水线中精确路由任务,避免普通软件构建占用固件构建机。

Jenkins的Agent接入方式类似,通过java -jar agent.jar -jnlpUrl ...启动,但需要在Controller端预先配置节点标签和工具路径。GitHub Actions的自托管Runner则通过仓库Settings中的Runner注册流程获取令牌,执行./config.sh --url ... --token ...完成接入,之后在workflow中用runs-on: [self-hosted, firmware]指定。

流水线即代码的落地方式

“流水线即代码”意味着构建逻辑必须与源码一起版本控制,而不是在Web界面里点击配置。三个平台都支持这一模式,但文件格式和表达能力不同。

Jenkins使用Jenkinsfile,支持声明式和脚本式两种语法。声明式更适合固件团队,因为它的stages结构清晰,便于审查。GitLab CI使用.gitlab-ci.yml,通过stages、script、artifacts等关键字定义。GitHub Actions使用workflow YAML,以jobs和steps为核心组织单元。

无论哪个平台,固件流水线的骨架都应包含以下阶段:源码检出(含子模块)、工具链准备、编译构建、签名与哈希、产物归档。下面是一个可直接参考的GitLab CI示例:

stages:
  - checkout
  - build
  - sign
  - package

variables:
  TOOLCHAIN: arm-none-eabi-
  BUILD_DIR: build/release

checkout:
  stage: checkout
  script:
    - git submodule update --init --recursive

build:
  stage: build
  tags: [firmware, arm]
  script:
    - cmake -B $BUILD_DIR -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake
    - cmake --build $BUILD_DIR -j$(nproc)
  artifacts:
    paths:
      - $BUILD_DIR/*.bin
      - $BUILD_DIR/*.hex
      - $BUILD_DIR/*.map
    expire_in: 30 days

sign:
  stage: sign
  tags: [firmware, arm]
  script:
    - python3 tools/sign_firmware.py $BUILD_DIR/firmware.bin
    - sha256sum $BUILD_DIR/firmware_signed.bin > $BUILD_DIR/sha256sum.txt
  artifacts:
    paths:
      - $BUILD_DIR/firmware_signed.bin
      - $BUILD_DIR/sha256sum.txt

这个骨架中有几个固件特有的设计点。第一,checkout阶段显式执行子模块更新,因为固件项目常把HAL库、RTOS、加密库作为子模块管理。第二,build阶段通过CMAKE_TOOLCHAIN_FILE指定交叉编译工具链,而不是依赖Runner的全局环境变量,这样流水线在不同Runner上行为一致。第三,sign阶段独立成阶段,签名密钥通过CI/CD变量注入,不写入代码仓库。第四,产物中保留.map文件,便于后续分析固件体积和符号布局。

创建首个固件构建流水线的实操要点

首次搭建时,建议先用一个最小可运行版本验证链路,再逐步增加签名和硬件测试。最小版本只需要checkout和build两个阶段,确认Runner能正确调用交叉编译工具链并产出.bin文件。

验证时重点观察三件事:编译日志中工具链路径是否正确、产物文件是否出现在artifacts中、流水线耗时是否在可接受范围。如果编译失败,优先检查Runner上的工具链版本与项目要求是否一致——这是固件CI中最常见的环境问题。

在Runner上固定工具链版本,例如将arm-none-eabi-gcc安装到/opt/toolchain/10.3-2021.10/并在流水线中显式引用该路径,可以避免因Runner系统升级导致工具链漂移,从而保证固件构建的可复现性。

当最小流水线跑通后,再引入签名阶段和硬件在环测试阶段。签名阶段需要提前在CI/CD变量中配置密钥路径和口令,硬件测试阶段则需要为Runner打上jlink标签并确保测试脚本能通过pyocd或JLinkExe烧录固件。每一步都应保持“先验证、再扩展”的节奏,避免一次性引入过多变量导致排障困难。