编程 CodeQL 报 No code found during the build:exit 32 多半是构建缓存命中,编译器一次都没被调用

2026-11-07 00:04:27

现象:构建步骤正常,CodeQL 分析报 exit 32

构建步骤正常,CodeQL 的 analyze 步骤报错:

No source code was seen during the build
The process '/opt/hostedtoolcache/CodeQL/.../codeql' failed with exit code 32

最容易误判的地方在于:看到这句话的第一反应往往是“仓库没有代码”或“语言配错了”。多数情况下代码就在仓库里,也写得好好的;真正原因在构建侧——编译器一次都没有被调用。

CodeQL 对编译型语言不做纯文本解析。它在构建期间运行一个 build tracer(编译器包装器),拦截每一次编译器调用,记录每个编译单元实际被喂进去的源文件和编译选项,再按这份记录建数据库。tracer 抽到 0 个文件,数据库里就没有源码,finalize 阶段以 32 退出。

所以“构建成功”和“扫描成功”是两件事。构建成功只说明产物出来了;如果产物来自缓存,编译器可以整场缺席。

GitHub 官方对这两种报错的定性是:CodeQL 无法监控到你的代码。下面按可复现的顺序排查。

排查顺序

1. init 打开 debug

- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
debug: true

debug 产物会以 debug-artifacts 名字上传,里面包含 CodeQL 日志、CodeQL 数据库和 SARIF。

gh run download  -n debug-artifacts

2. 看两处日志

先看 tracer 自己记了什么:

cat debug-artifacts/log/build-tracer.log

这里会列出拦截到的编译器调用。如果整份日志里几乎没有编译行为,问题就基本确定:构建缓存命中、编译发生在 init 之前或 analyze 之后,或者编译根本不在 CodeQL 所在的容器里。

再看数据库侧的日志:

ls debug-artifacts/db-*/log/

抽取错误会以 error/warning 形式出现在这里。少量抽取错误是正常现象;只有绝大多数被编译文件都报错时才需要深挖。

3. 用 src.zip 对比

debug 产物里有一份 src.zip,它是本次扫描实际收录的源码副本。

unzip -l debug-artifacts/db-*/src.zip

把它和仓库文件列表比一遍:

git ls-files > /tmp/tracked.txt
unzip -Z1 debug-artifacts/db-*/src.zip | sort > /tmp/scanned.txt
comm -23 /tmp/tracked.txt /tmp/scanned.txt | head -50

左边独有的就是没进数据库的文件。如果缺的是一整块模块(比如 example/src 这种非标准路径下的源码),说明 autobuild 的启发式构建根本没走到那里。

4. 本地 CLI 复现

不必反复推 workflow。本地直接建库,观察同样的现象:

codeql database create db -l csharp

给上真实构建命令再跑一次:

codeql database create db \
--language=java-kotlin \
--command="./gradlew assembleDebug --no-build-cache --rerun-tasks"

两次结果的差异本身就是结论。issue #15377 里讨论的正是这条路径:先确认 autobuild 能不能抽取;必须手动构建时要保证是干净重建;再不行就下载 debug artifact 翻 log/ 目录,或本地建库看 db/log。该 issue 的提问者最后确认,清掉缓存后就正常了——“before i was thinking it will not taking from cache but it was.”

真实触发条件

  • Gradle / Bazel / dotnet 的增量或 build cache 命中。这是最常见的一种。docs-only 之类的 commit 不会触发重新编译,assembleDebug 直接缓存命中,tracer 看到 0 个 Kotlin 文件。
  • 一个仓库里有多个 build.sh,autobuild 只执行其中一个,剩下那部分源文件从未参与构建。
  • 编译发生在 init 之前或 analyze 之后。CodeQL 只观测 init 与 analyze 两个 step 之间的编译活动。
  • 构建跑在别的容器里,或使用 GitHub Actions 之外的分布式构建系统/守护进程,tracer 看不到这些进程。
  • .NET 项目缺少 /p:UseSharedCompilation=false。共享编译会让真正的编译发生在常驻进程里:
- run: dotnet build /p:UseSharedCompilation=false
  • Lombok 之类的注解处理器。它用非公开编译器 API 改编译器行为,这些假设对 CodeQL 的 Java extractor 不成立,代码无法分析。
  • 语言自动检测只挑一个。workflow 没显式写 language 时 CodeQL 隐式检测,并且在 C/C++、C#、Java 这三个编译型语言里只分析源文件最多的那一个;检测到 .h、.gyp 但没有对应可执行代码也会落空。这类情况直接在 matrix 里写死语言,例如 ['go', 'javascript']。
  • 构建配置本身不编译任何代码。Java 多模块项目源码在 example/src 这类非标准路径下(issue #9534),或 pom.xml 里写了 pom,mvn package 一个 Java 文件都不编译(issue #11464)。CodeQL 靠跟随构建过程判断哪些代码与项目相关,packaging 为 pom 时它看不到任何可抽取的代码。

修法一:手动构建替换 autobuild

把默认的 autobuild 换成和生产一致的构建命令,并确保缓存在 CI 上不生效。

- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}

- name: Build
run: ./gradlew assembleDebug --no-build-cache --rerun-tasks

代价是构建时间变长、需要维护构建参数,而且仓库里每一处需要编译的模块都得覆盖到。好处是抽取范围可控——scan 到的源码就是你构建出来的源码。

这个方式的另一个副作用是扫描行数会低于预期:编译型语言只分析构建过程中真正被构建的文件。Lines of code scanned are lower than expected 和 exit 32 是同一根因的不同表现,只是程度不同。

修法二:切入 build-mode: none

不执行构建,只从源码抽取。真实案例是 u1-slicer-for-android 的 commit f43fa62:build-traced 分析在 docs-only commit 上间歇失败,Gradle 构建缓存让 assembleDebug 直接命中,tracer 看到 0 个 Kotlin。切到 build-mode: none 后不再跑 Android 构建,顺带删掉了 Android SDK、Gradle、debug keystore 和 build 步骤。

- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: none

取舍要写清楚:build-mode: none 不执行构建,所以构建期生成的代码不在库里——注解处理器产出的类、代码生成器写出来的源文件,都不会出现在分析结果中。如果这些生成代码是攻击面的一部分,这个模式会漏。

另一个约束是增量分析:GitHub 的增量分析文档明确说明,overlay analysis 只支持 build-mode: none,不支持 traced build。overlay 复用默认分支缓存的数据库,版本要求 CLI bundle 2.23.8+(各语言另有最低版本),diff-informed analysis 需要 2.21.0+。前置条件还包括:源码根在 Git 仓库内、Git 2.38.0+、关注的文件必须被 Git 跟踪、index 准确反映待分析源码树。在 overlay-base 上跑查询时不要传 --expect-discarded-cache,否则缓存被丢弃、每次都全量重算。

检查项通过也可能没扫到东西

两类「检查项通过」都需要单独查。

CodeQL 侧:看 LOC scanned。这个数明显低于仓库实际规模,就说明进库的文件不全,即使 workflow 是正常的。别只看结论图标。

Semgrep 侧:semgrep scan 默认扫完即 exit 0,即使有 finding;要加 --error 才会在有 finding 时 exit 1。不加这个参数,job 永远通过。semgrep ci 的行为相反:没接 Semgrep AppSec Platform 时,只要有 finding 就 exit 1,因为它无法区分 blocking 与非 blocking。另外 semgrep ci 在 PR/MR 触发时默认跑 diff-aware 扫描,只报告变更代码里的 finding,不会给出全量结果;--config auto 用的是 Registry 规则集,不含组织策略里的自定义规则,想接近策略结果可以用 --config p/default。这些差异在 Semgrep 官方 CI vs CLI 文档里有完整对照。

跨文件分析需要 semgrep install-semgrep-pro 并加 --pro,Semgrep CE 只做单函数内分析——这一点在评估覆盖率时容易漏掉。

排查时最省时间的做法是先把 debug-artifacts 下下来,build-tracer.log 里编译调用数量和 src.zip 里的文件清单,基本能一次性定位是缓存、是构建路径错了,还是编译压根没被 tracer 看到。具体到某个仓库属于哪种,还需自行验证。

复制全文 生成海报 代码扫描 SAST CodeQL 静态分析 GitHub Actions CI

推荐文章

程序员茄子在线接单