> .gitignore 能吞掉 CI 要执行的那个测试脚本,而本地永远是绿的

Systems & Infrastructure Intermediate 2026-10-11 18:09 2026-10-11
#git #ci #gitignore #testing #failure-modes #tooling

.gitignore 里有一行 .cmake(为了丢掉 CMake 生成的文件),后面跟着唯一一条否定 !CMakeLists.txt。

结论先行

.gitignore 里有一行 *.cmake(为了丢掉 CMake 生成的文件),后面跟着唯一一条否定 !CMakeLists.txt。

这条规则同时匹配了 tests/smoke_render.cmake—— 而 CMakeLists.txt 注册的 CTest 测试执行的正是这个文件:

add_test(NAME fractal_api_smoke
    COMMAND ${CMAKE_COMMAND} -DAPI_BIN=$<TARGET_FILE:fractal_api>
        -P ${CMAKE_SOURCE_DIR}/tests/smoke_render.cmake)

后果:提交、推送,CI 检出一棵没有这个文件的树,ctest 在"脚本不存在"上失败, cpp job 失败,而 node 和 docker-build 都 needs: cpp,于是全部被跳过。 本地什么都照旧通过,因为你的工作区里文件在。

修法是在 *.cmake 之后补 !tests/*.cmake。

为什么

真正的坑在怎么检查。git check-ignore -v 有三种行为,而它们很容易被读错:

# 1) 文件被忽略
$ git check-ignore -v tests/smoke.cmake
.gitignore:1:*.cmake    tests/smoke.cmake
$ echo $?
0

# 2) 加了否定之后——注意退出码一模一样
$ git check-ignore -v tests/smoke.cmake
.gitignore:3:!tests/*.cmake     tests/smoke.cmake
$ echo $?
0

# 3) 文件已经被跟踪——它什么都不打印,退出 1
$ git check-ignore -v tests/smoke_render.cmake
$ echo $?
1

三件事:

  • 被忽略和被否定,退出码都是 0。唯一的区别是打印出来那行开头有没有 !。 拿退出码当判据,等于把修好的情况也判成坏的。
  • 已跟踪的文件会被直接跳过(除非加 --no-index),所以这个工具的回答 取决于索引状态——这恰恰是它不能当检查的原因:你要问的是"一次干净检出里有没有它"。
  • 权威的检查是 git add -A --dry-run:它直接告诉你什么会进去。
$ git add -A --dry-run
add '.gitignore'
add 'CMakeLists.txt'
          ← tests/smoke.cmake 不在这里。这才是答案。

(实测于 git 2.53.0。)

怎么用

  1. 否定规则写完要用 git add -A --dry-run 验收,不要用 git check-ignore 的退出码。
  2. 更强的验收:把那个提交克隆到临时目录,在里面跑测试。 这个项目就是这么确认修好的——ctest 在全新检出上 2/2 通过。 "本地绿、CI 红"这一整类问题,只有在一棵干净树上才测得出来。
  3. 广义通配后面跟白名单,比黑名单安全。 *.cmake 这种按扩展名的宽规则迟早会吞掉手写的同扩展名文件; 按目录放行(!tests/*.cmake)把漏的方向变成安全的那一侧。 见 版本控制的边界要跟着"什么在变"走,不是跟着目录大小走。
  4. 在 .gitignore 里就把原因写下来。 这个仓库修完直接在那几行下面留了注释, 说明为什么需要这条否定——否则下一个清理 .gitignore 的人会把它删掉。

出处

  • ~/Engineering/Mandelbrot-Renderer/.gitignore 第 14–20 行(*.cmake、两条否定, 以及解释为什么需要 !tests/*.cmake 的注释)
  • 同项目 PROJECT_ENGINEERING_GUIDE.md §0.1:原始诊断、CI 的 needs: 链、 以及"克隆到临时目录确认 2/2 通过"的验收方式;修复提交 27be813
  • 三种 check-ignore 行为:2026-10-11 在一个最小复现仓库上实测(git 2.53.0), 并在本仓库上验证了"文件已跟踪 → 退出 1 且无输出"