CI 明明被触发,为什么同步任务却直接跳过
一条 CI job 被 rules:changes 触发,进入脚本后却发现 source fingerprint 没变化,于是直接退出。表面看只是浪费了一次 runner,实际更危险:某些文件变化可能触发 job,却永远不会进入同步结果;另一些无关文件又可能制造重复 MR。
问题不在 YAML 或 Python 单独哪一边,而在两套“什么会改变产物”的定义没有对齐。
现场:去重优化反而制造新问题
目标原本很合理:只有 SDK 真正变化时,才向下游仓库同步并创建 MR。方案使用两层门禁:
- CI 的
rules:changes决定 job 是否启动; - 同步脚本计算 source fingerprint,决定是否真正执行。
第一版实现的问题是,两层输入集合不同。
例如 CI 可能把整份 .gitlab-ci.yml、同步脚本和 examples 都列为 trigger;fingerprint 却只包含库源码。于是出现:
CI 判断“需要同步”
→ job 启动
→ fingerprint 判断“SDK 没变”
→ job 直接退出反过来,如果某个真正影响库产物的路径只进入 fingerprint、没有进入 rules:changes,job 根本不会启动。
根因:没有定义 Artifact Contract
真正要同步的不是“仓库里看起来相关的文件”,而是能够改变目标 SDK artifact 的输入集合。
可以把它写成:
Artifact = Build(actual_sources, public_headers, build_options, required_scripts)CI trigger 集合 T 和 fingerprint 集合 F 都应该由这份合同推导:
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、同步内容和测试共同引用同一个概念。