Skip to content

CI 明明被触发,为什么同步任务却直接跳过

一条 CI job 被 rules:changes 触发,进入脚本后却发现 source fingerprint 没变化,于是直接退出。表面看只是浪费了一次 runner,实际更危险:某些文件变化可能触发 job,却永远不会进入同步结果;另一些无关文件又可能制造重复 MR。

问题不在 YAML 或 Python 单独哪一边,而在两套“什么会改变产物”的定义没有对齐。

现场:去重优化反而制造新问题

目标原本很合理:只有 SDK 真正变化时,才向下游仓库同步并创建 MR。方案使用两层门禁:

  1. CI 的 rules:changes 决定 job 是否启动;
  2. 同步脚本计算 source fingerprint,决定是否真正执行。

第一版实现的问题是,两层输入集合不同。

例如 CI 可能把整份 .gitlab-ci.yml、同步脚本和 examples 都列为 trigger;fingerprint 却只包含库源码。于是出现:

text
CI 判断“需要同步”
  → job 启动
  → fingerprint 判断“SDK 没变”
  → job 直接退出

反过来,如果某个真正影响库产物的路径只进入 fingerprint、没有进入 rules:changes,job 根本不会启动。

根因:没有定义 Artifact Contract

真正要同步的不是“仓库里看起来相关的文件”,而是能够改变目标 SDK artifact 的输入集合。

可以把它写成:

text
Artifact = Build(actual_sources, public_headers, build_options, required_scripts)

CI trigger 集合 T 和 fingerprint 集合 F 都应该由这份合同推导:

text
T ≈ F ≈ 会改变目标 artifact bytes 或公开合同的文件集合

不要求两者在语法上完全相同,但语义必须一致。

第二次踩坑:为了保险,把范围又放大了

修完第一轮 mismatch 后,一个常见反应是“那就多包含一些路径”。结果 examples 和整份 CI 文件又被放进 trigger/fingerprint。

这会重新制造无效同步。示例程序只生成独立 executable,并不进入共享库 source list;CI 里修改 unrelated job,也不会改变 SDK bytes。它们不应该因为“在同一目录附近”就被纳入 artifact contract。

修复方法:从构建图反推输入

最终保留的输入类型只有:

  • SDK 的公开头文件;
  • 共享库/静态库真正编译的源码;
  • 决定 source list 或编译选项的 CMake 文件;
  • 确实影响产物的依赖构建脚本;
  • 用于保证 artifact 可用性的必要 smoke 脚本。

同步脚本本身、examples、整份 CI 配置都从正常 source fingerprint 中移除。需要手动刷新时,保留显式 force-sync 或 test mode,而不是污染日常触发集合。

怎样验证不是“看起来对齐”

这类修复至少需要四种检查:

1. Trigger/Fingerprint 对照表

逐项回答每个路径是否会改变 artifact,以及它是否同时被 trigger 和 fingerprint 覆盖。

2. Fingerprint 确定性

相同文件内容、不同扫描顺序必须得到同一个 hash。路径排序、文件边界和空文件都应有稳定编码。

3. 正反样本

  • 修改 public header:job 应触发,fingerprint 应变化;
  • 修改库源码:两者都应变化;
  • 修改 example:默认不应同步;
  • 修改无关 CI job:默认不应同步;
  • 强制模式:即使 hash 相同也应按显式意图执行。

4. 轻量语法验证

YAML、脚本语法、diff whitespace 和 fingerprint determinism 都应在提交前完成,避免把逻辑 review 和基础语法错误混在一起。

这类问题为什么值得单独写

CI 去重不是“少跑几个 job”的小优化。它在定义跨仓库交付边界:什么变化必须传播,什么变化不应该制造下游噪声。

如果这份边界只存在于两段彼此独立的配置里,迟早会漂移。更稳的做法是先定义 artifact contract,再让 trigger、fingerprint、同步内容和测试共同引用同一个概念。

参考:GitLab rules:changes 与 CI/CD YAML 官方说明