跳转至

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 用着,等攒够几次后台任务耗时再调。这两条先记着,不写进配置里装作已经解决了。

相关阅读