> .gitignore 能吞掉 CI 要执行的那个测试脚本,而本地永远是绿的
.gitignore 里有一行 .cmake(为了丢掉 CMake 生成的文件),后面跟着唯一一条否定 !CMakeLists.txt。
// TABLE_OF_CONTENTS
结论先行¶
.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。)
怎么用¶
- 否定规则写完要用
git add -A --dry-run验收,不要用git check-ignore的退出码。 - 更强的验收:把那个提交克隆到临时目录,在里面跑测试。
这个项目就是这么确认修好的——
ctest在全新检出上 2/2 通过。 "本地绿、CI 红"这一整类问题,只有在一棵干净树上才测得出来。 - 广义通配后面跟白名单,比黑名单安全。
*.cmake这种按扩展名的宽规则迟早会吞掉手写的同扩展名文件; 按目录放行(!tests/*.cmake)把漏的方向变成安全的那一侧。 见 版本控制的边界要跟着"什么在变"走,不是跟着目录大小走。 - 在
.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 且无输出"