SonarQube 质量门禁为什么卡不住 PR¶
这周把手里那条安全流水线翻出来重看了一遍。工具加得挺全,SonarQube 也一直在跑,但有个问题之前一直绕着走:SonarQube 项目页上是红的,GitLab 的 MR 是绿的,合并按钮照点不误。
10 月那篇《DevSecOps 与安全测试左移》讲的是卡点怎么分级,这篇往下钻一层,只解决 SonarQube 这一个工具「扫了但没卡住」的问题。后面几天打算接着把 SAST 规则、CodeQL、SCA、DAST 逐块过一遍,算是把这几天的排查过程记下来。
一、scanner 成功,不等于门禁通过¶
最先排掉的是这个。SonarScanner 把分析结果提交给服务端就返回了,报告在服务端后台异步处理,质量门禁要等处理完才有状态。所以下面这条命令 exit code 是 0:
sonar-scanner \
-Dsonar.projectKey=order-service \
-Dsonar.host.url="$SONAR_HOST_URL" \
-Dsonar.login="$SONAR_TOKEN"
CI job 立刻变绿,此刻门禁结果可能还没算出来,更谈不上让它失败。很多人以为「装了门禁插件就自动阻断」,其实中间差一个显式等待。
官方参数是 sonar.qualitygate.wait=true,作用是让分析步骤轮询服务端直到拿到 Quality Gate 状态,门禁失败时让这一步失败:
sonar-scanner \
-Dsonar.projectKey=order-service \
-Dsonar.host.url="$SONAR_HOST_URL" \
-Dsonar.login="$SONAR_TOKEN" \
-Dsonar.qualitygate.wait=true \
-Dsonar.qualitygate.timeout=300
sonar.qualitygate.timeout 是 scanner 等报告处理完的秒数,参数表上默认 300(版本之间可能有出入,用之前对着自己装的 scanner 核一下)。服务慢的时候会撞到它,别急着往上加——先看服务端 Background Tasks 是不是积压了,任务堆着不动,等再久也是超时。
Jenkins 走插件的话有另一条路:withSonarQubeEnv 配合 waitForQualityGate abortPipeline: true。两条路选一条就行,同时上会轮询两次,日志里出现两个门禁结果,排障时容易看错哪个是最新的。
二、New Code 没设对,门禁迟早被人关掉¶
第二处更隐蔽。SonarQube 默认的 Sonar way 门禁,条件全部作用在 new code 上,不是整个代码库。官方管这个叫 Clean as You Code:老代码的债不要求一次还清,只要求每次改动别再带来新问题。
所以「哪些代码算新」这个定义,直接决定门禁报不报、报多少。官方给了四种:
| 定义 | 起点怎么算 | 适合 |
|---|---|---|
| Previous version | 当前 sonar.projectVersion 首次分析那天 |
按版本发版 |
| Number of days | 当前日期往前推 X 天,默认 30,最大 90 | 没有明确版本号的仓 |
| Specific analysis | 手动指定某次分析 | 持续交付里划周期 |
| Reference branch | 与选定参考分支的 diff | 短生命周期分支、trunk-based |
PR 分析是个例外:这时 new code 直接等于「本分支相对目标分支改了什么」,只报这部分上的问题,跟项目级设的 new code 定义不是一回事。
我这边之前踩的坑是把门禁条件加到了 overall code 上。老项目第一次接入,覆盖率 20%、几百个历史 issue,门禁立刻全红,两周后有人直接在 job 上加了 when: never。官方文档在这点上说得很直白:聚焦 new code 的门禁不建议再加 overall code 的条件,因为会把注意力从自己改的代码挪到别人三年前写的代码上,开发者也就不认领了。
历史债不是不管,单独立项、单独排期,别塞进 PR 门禁。
三、一份能直接改的配置¶
sonar-project.properties:
sonar.projectKey=order-service
sonar.projectName=order-service
sonar.projectVersion=1.0.0
sonar.sources=src/main/java
sonar.tests=src/test/java
sonar.java.binaries=target/classes
sonar.sourceEncoding=UTF-8
# 覆盖率报告要在 scanner 之前由构建产出,scanner 只负责读
sonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml
# 生成代码不进扫描,否则门禁会被 DTO / mapper 淹没
sonar.exclusions=**/generated/**,**/model/dto/**,**/*Mapper.xml
sonar.qualitygate.wait=true
GitLab CI 里对应的 job:
sonarqube-check:
stage: test
image: maven:3.9-eclipse-temurin-21
variables:
SONAR_USER_HOME: "$CI_PROJECT_DIR/.sonar"
GIT_DEPTH: "0" # 关键,见下
cache:
key: "$CI_JOB_NAME"
paths: [.sonar/cache]
script:
- mvn -B verify
- >
mvn -B sonar:sonar
-Dsonar.host.url="$SONAR_HOST_URL"
-Dsonar.login="$SONAR_TOKEN"
-Dsonar.qualitygate.wait=true
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
三个点单独拎出来:
GIT_DEPTH: "0"/fetch-depth: 0:new code 判定和 blame 都依赖完整 git 历史,浅克隆会让基线算不准,表现为「明明只改了一行,门禁却报了一堆无关问题」。mvn verify必须排在sonar:sonar前面。没有编译产物和 jacoco 报告,Java 分析会退化,覆盖率那一栏直接是 0。rules里要放merge_request_event。只跑普通分支流水线的话,MR 分析不会触发,PR 上什么都没有——这个我排查了半天才意识到不是 token 的问题。
四、门禁状态回写 MR 是另外一套配置¶
扫描跑通、门禁也变红了,MR 页面仍可能一片空白。回写(PR decoration)依赖 DevOps 平台集成:GitHub / GitLab / Bitbucket / Azure DevOps 各自要在 SonarQube 里配好应用和 token,项目还要绑定。
绑定项目一般会自动带上门禁状态回写;没绑定的项目得在项目设置里手工补配置名和 Project ID。还有个容易预期错的点:GitLab 这边支持 MR 装饰,但不支持逐行 inline annotations,别按 GitHub 的效果去验收。
最后一步在平台侧:把 SonarQube 上报的 status check 设为 required(GitHub 的 branch protection / GitLab 的 Merge Checks)。少了这步,门禁红了也只是「有个红勾」,合并照样能点。
五、验证别只看 job 变绿¶
改完我用四个动作验的,比看日志靠谱:
1. 提交一个空变更,确认 job 绿且耗时在预算内
2. 故意在新加的一行里引入一个阻断级问题,确认 job 红、MR 上出现失败状态
3. 删掉 jacoco 报告再跑一次,确认覆盖率不会假装是 0 然后默默通过
4. 换一个浅克隆的分支跑,对比 new code 问题数是否异常膨胀
第 2 步是唯一能证明「门禁真的接上了」的。前面所有配置都做完但没做这一步,等于没验。
坑位表¶
| 现象 | 根因 | 处理 |
|---|---|---|
| job 全绿,SonarQube 页面红 | scanner 异步返回,没等门禁 | sonar.qualitygate.wait=true |
| 等太久最后超时 | 服务端后台任务积压 | 先看 Background Tasks,别只调大 timeout |
| 首次接入几百条告警 | 门禁条件加在 overall code 上 | 只留 new code 条件,历史债单独立项 |
| 覆盖率恒为 0 | verify 与 jacoco 报告没在 scanner 前产出 | 调整 stage 顺序,核对 xml 路径 |
| new code 问题数莫名其妙 | 浅克隆丢了 git 历史 | GIT_DEPTH: "0" / fetch-depth: 0 |
| MR 页面没有门禁状态 | 平台集成或项目绑定没做 | 配 DevOps 集成,检查项目绑定 |
| 状态显示但拦不住合并 | status check 未设为 required | 平台侧改分支保护 / Merge Checks |
还留着没解决的¶
一是多语言仓库的 quality profile 还没分开配,现在共用一个 profile,前端那部分规则明显偏松;二是 sonar.qualitygate.timeout 该定多少没有依据,先按默认 300 用着,等攒够几次后台任务耗时再调。这两条先记着,不写进配置里装作已经解决了。