🐻GitLab CI 接入 SonarQube 实现文档

📋 1. 概述
本文档描述如何在 GitLab CI/CD 中集成 SonarQube,实现 提交 Merge Request 时自动触发代码质量扫描,并将扫描结果回写到 MR 评论中,帮助团队在代码合并前发现潜在问题。
整体流程
开发者提交 MR → GitLab CI 触发 Pipeline → SonarScanner 执行扫描
→ 结果上报 SonarQube Server → Quality Gate 判定 → 结果回写 MR
graph LR
subgraph GitLab
MR[<b>Merge Request</b><br>触发 CI Pipeline] --> Runner[GitLab Runner]
end
subgraph SonarQube_Scanner
Runner --> Scanner[SonarScanner<br>分析代码]
end
subgraph SonarQube_Server
Scanner --> QG{Quality Gate<br>质量门禁}
QG -->|通过| Pass[✅ 通过]
QG -->|未通过| Fail[❌ 失败]
end
subgraph Feedback
Pass & Fail --> API[GitLab API]
API --> Comment[<b>MR 评论</b><br>自动回写报告]
end
style MR fill:#fc6d26,stroke:#333,color:#fff
style QG fill:#4c9bd6,stroke:#333,color:#fff
style Comment fill:#6f42c1,stroke:#333,color:#fff
✅ 2. 前置条件
| 项目 | 要求 |
|---|---|
| SonarQube Server | 已部署,版本 ≥ 8.9(Community Edition 及以上) |
| GitLab | 版本 ≥ 13.0,已配置 GitLab Runner |
| GitLab Runner | 推荐 Docker executor,已注册到项目或 Group |
| 网络 | Runner 可访问 SonarQube Server |
🔧 3. SonarQube 端配置
3.1 创建项目
- 登录 SonarQube → Projects → Create Project → Manually
- 填写 Project Key(如
my-project)和 Display Name - 记录 Project Key,后续配置需要使用
3.2 生成 Token
- 进入 My Account → Security → Generate Tokens
- 输入 Token 名称(如
gitlab-ci-token),类型选择 Project Analysis Token - 关联到对应项目,点击 Generate
- 复制并妥善保存 Token(仅显示一次)
3.3 配置 Quality Gate(可选)
- 进入 Quality Gates → 选择或新建一个 Quality Gate
- 推荐条件:
| 指标 | 条件 | 阈值 |
|---|---|---|
| New Code Coverage | is less than | 80% |
| New Duplicated Lines (%) | is greater than | 3% |
| New Bugs | is greater than | 0 |
| New Vulnerabilities | is greater than | 0 |
| New Code Smells | is greater than | 10 |
- 将 Quality Gate 关联到目标项目
3.4 配置 GitLab 集成(Developer Edition+)
如果使用 Developer Edition 及以上版本,可启用原生 MR 装饰功能。
- 进入 Administration → Configuration → ALM Integrations → GitLab
- 填写 GitLab URL 和 Personal Access Token(需要
api权限) - 在项目设置中绑定 GitLab 仓库
🦊 4. GitLab 端配置
4.1 配置 CI/CD Variables
进入 GitLab Project → Settings → CI/CD → Variables,添加以下变量:
| 变量名 | 值 | 类型 | Protected | Masked |
|---|---|---|---|---|
SONAR_HOST_URL |
https://sonarqube.example.com |
Variable | No | No |
SONAR_TOKEN |
sqa_xxxxxxxxxxxxxxx |
Variable | No | Yes |
注意:
SONAR_TOKEN务必开启 Masked 选项,防止在日志中泄露。
4.2 项目根目录添加 sonar-project.properties
# 项目唯一标识,必须与 SonarQube 中一致
sonar.projectKey=my-project
sonar.projectName=My Project
# 源码目录(根据项目实际结构调整)
sonar.sources=src/main/java
sonar.tests=src/test/java
# 编码
sonar.sourceEncoding=UTF-8
# Java 项目需要指定编译产物路径
sonar.java.binaries=target/classes
sonar.java.test.binaries=target/test-classes
# 排除不需要扫描的文件
sonar.exclusions=**/generated/**,**/node_modules/**,**/*.min.js
# 覆盖率报告路径(如使用 JaCoCo)
sonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml
🚀 5. GitLab CI 配置
5.1 基础配置(.gitlab-ci.yml)
stages:
- test
- sonar
variables:
MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
# Maven 构建和单元测试
unit-test:
stage: test
image: maven:3.8-openjdk-11
script:
- mvn clean verify -DskipITs
artifacts:
paths:
- target/
- target/site/jacoco/
expire_in: 1 hour
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
# SonarQube 扫描(仅 MR 触发)
sonar-scan:
stage: sonar
image:
name: sonarsource/sonar-scanner-cli:latest
entrypoint: [""]
dependencies:
- unit-test
variables:
SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
GIT_DEPTH: "0" # 必须全量 clone,否则 SCM 分析不完整
script:
- >
sonar-scanner
-Dsonar.host.url=${SONAR_HOST_URL}
-Dsonar.token=${SONAR_TOKEN}
-Dsonar.projectKey=${CI_PROJECT_NAME}
-Dsonar.qualitygate.wait=true
-Dsonar.pullrequest.key=${CI_MERGE_REQUEST_IID}
-Dsonar.pullrequest.branch=${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME}
-Dsonar.pullrequest.base=${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}
allow_failure: true
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
5.2 关键参数说明
| 参数 | 说明 |
|---|---|
sonar.pullrequest.key |
MR 的 IID,SonarQube 用于关联 MR |
sonar.pullrequest.branch |
MR 源分支名 |
sonar.pullrequest.base |
MR 目标分支名 |
sonar.qualitygate.wait=true |
等待 Quality Gate 结果返回,未通过则 Job 失败 |
GIT_DEPTH: "0" |
全量 clone 代码,确保 blame 信息完整 |
allow_failure: true |
Quality Gate 未通过时不阻塞 MR 合并(按需调整) |
5.3 增强配置:扫描结果回写 MR 评论
适用于 SonarQube Community Edition(不支持原生 MR 装饰)。
sonar-scan:
stage: sonar
image:
name: sonarsource/sonar-scanner-cli:latest
entrypoint: [""]
dependencies:
- unit-test
variables:
SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
GIT_DEPTH: "0"
script:
# 执行扫描
- >
sonar-scanner
-Dsonar.host.url=${SONAR_HOST_URL}
-Dsonar.token=${SONAR_TOKEN}
-Dsonar.qualitygate.wait=true
-Dsonar.pullrequest.key=${CI_MERGE_REQUEST_IID}
-Dsonar.pullrequest.branch=${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME}
-Dsonar.pullrequest.base=${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}
after_script:
# 获取 Quality Gate 结果并回写 MR 评论
- |
PROJECT_KEY="${CI_PROJECT_NAME}"
SONAR_RESULT=$(curl -s -u "${SONAR_TOKEN}:" \
"${SONAR_HOST_URL}/api/qualitygates/project_status?projectKey=${PROJECT_KEY}&pullRequest=${CI_MERGE_REQUEST_IID}")
QG_STATUS=$(echo "$SONAR_RESULT" | jq -r '.projectStatus.status')
# 获取具体指标
BUGS=$(echo "$SONAR_RESULT" | jq -r '.projectStatus.conditions[] | select(.metricKey=="new_bugs") | .actualValue // "0"')
VULNS=$(echo "$SONAR_RESULT" | jq -r '.projectStatus.conditions[] | select(.metricKey=="new_vulnerabilities") | .actualValue // "0"')
SMELLS=$(echo "$SONAR_RESULT" | jq -r '.projectStatus.conditions[] | select(.metricKey=="new_code_smells") | .actualValue // "0"')
COVERAGE=$(echo "$SONAR_RESULT" | jq -r '.projectStatus.conditions[] | select(.metricKey=="new_coverage") | .actualValue // "N/A"')
if [ "$QG_STATUS" = "OK" ]; then
STATUS_ICON="✅"
else
STATUS_ICON="❌"
fi
# 构造评论内容
COMMENT_BODY=$(cat <<EOF
## ${STATUS_ICON} SonarQube Quality Gate: **${QG_STATUS}**
| 指标 | 值 |
|------|-----|
| 🐛 New Bugs | ${BUGS} |
| 🔓 New Vulnerabilities | ${VULNS} |
| 💤 New Code Smells | ${SMELLS} |
| 📊 New Coverage | ${COVERAGE}% |
🔗 [查看详细报告](${SONAR_HOST_URL}/dashboard?id=${PROJECT_KEY}&pullRequest=${CI_MERGE_REQUEST_IID})
EOF
)
# 通过 GitLab API 回写 MR 评论
curl -s --request POST \
--header "PRIVATE-TOKEN: ${GITLAB_API_TOKEN}" \
--header "Content-Type: application/json" \
--data "{\"body\": $(echo "$COMMENT_BODY" | jq -Rs .)}" \
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}/notes"
allow_failure: true
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
使用评论回写功能需额外配置
GITLAB_API_TOKEN(GitLab Personal Access Token,需api权限)。
📦 6. Maven 项目完整示例
针对 Java/Maven 项目的完整 .gitlab-ci.yml:
stages:
- build
- test
- sonar
variables:
MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
MAVEN_CLI_OPTS: "-s .m2/settings.xml --batch-mode"
cache:
key: "${CI_JOB_NAME}"
paths:
- .m2/repository
- .sonar/cache
# 编译
compile:
stage: build
image: maven:3.8-openjdk-11
script:
- mvn $MAVEN_CLI_OPTS compile -DskipTests
artifacts:
paths:
- target/
expire_in: 1 hour
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
# 单元测试 + 覆盖率
unit-test:
stage: test
image: maven:3.8-openjdk-11
dependencies:
- compile
script:
- mvn $MAVEN_CLI_OPTS verify
artifacts:
paths:
- target/classes/
- target/test-classes/
- target/surefire-reports/
- target/site/jacoco/
reports:
junit: target/surefire-reports/TEST-*.xml
expire_in: 1 hour
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
# SonarQube 扫描
sonar-scan:
stage: sonar
image:
name: sonarsource/sonar-scanner-cli:latest
entrypoint: [""]
dependencies:
- unit-test
variables:
SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
GIT_DEPTH: "0"
script:
- >
sonar-scanner
-Dsonar.host.url=${SONAR_HOST_URL}
-Dsonar.token=${SONAR_TOKEN}
-Dsonar.projectKey=${CI_PROJECT_NAME}
-Dsonar.projectName=${CI_PROJECT_NAME}
-Dsonar.sources=src/main/java
-Dsonar.tests=src/test/java
-Dsonar.java.binaries=target/classes
-Dsonar.java.test.binaries=target/test-classes
-Dsonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml
-Dsonar.junit.reportPaths=target/surefire-reports
-Dsonar.qualitygate.wait=true
-Dsonar.pullrequest.key=${CI_MERGE_REQUEST_IID}
-Dsonar.pullrequest.branch=${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME}
-Dsonar.pullrequest.base=${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}
allow_failure: true
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
🧩 7. 多模块 Maven 项目配置
对于多模块项目,在根目录 sonar-project.properties 中配置:
sonar.projectKey=my-multi-module-project
sonar.projectName=My Multi Module Project
sonar.sourceEncoding=UTF-8
# 多模块声明
sonar.modules=module-api,module-service,module-dao
# 各模块配置
module-api.sonar.projectBaseDir=module-api
module-api.sonar.sources=src/main/java
module-api.sonar.tests=src/test/java
module-api.sonar.java.binaries=target/classes
module-service.sonar.projectBaseDir=module-service
module-service.sonar.sources=src/main/java
module-service.sonar.tests=src/test/java
module-service.sonar.java.binaries=target/classes
module-dao.sonar.projectBaseDir=module-dao
module-dao.sonar.sources=src/main/java
module-dao.sonar.tests=src/test/java
module-dao.sonar.java.binaries=target/classes
# 全局排除
sonar.exclusions=**/generated/**,**/model/**/*DTO.java
或者直接使用 Maven SonarQube 插件(无需 sonar-scanner-cli 镜像):
sonar-scan:
stage: sonar
image: maven:3.8-openjdk-11
variables:
GIT_DEPTH: "0"
script:
- >
mvn $MAVEN_CLI_OPTS sonar:sonar
-Dsonar.host.url=${SONAR_HOST_URL}
-Dsonar.token=${SONAR_TOKEN}
-Dsonar.qualitygate.wait=true
-Dsonar.pullrequest.key=${CI_MERGE_REQUEST_IID}
-Dsonar.pullrequest.branch=${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME}
-Dsonar.pullrequest.base=${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}
allow_failure: true
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
📐 8. 使用共享 CI 模板
推荐将 SonarQube 扫描抽取为共享模板,在多个项目中复用。
8.1 创建模板仓库
在 GitLab 中创建 ci-templates 仓库,添加 sonar-scan.yml:
# ci-templates/sonar-scan.yml
.sonar-scan-template:
stage: sonar
image:
name: sonarsource/sonar-scanner-cli:latest
entrypoint: [""]
variables:
SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
GIT_DEPTH: "0"
script:
- >
sonar-scanner
-Dsonar.host.url=${SONAR_HOST_URL}
-Dsonar.token=${SONAR_TOKEN}
-Dsonar.projectKey=${SONAR_PROJECT_KEY:-$CI_PROJECT_NAME}
-Dsonar.qualitygate.wait=${SONAR_QUALITY_GATE_WAIT:-true}
-Dsonar.pullrequest.key=${CI_MERGE_REQUEST_IID}
-Dsonar.pullrequest.branch=${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME}
-Dsonar.pullrequest.base=${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}
allow_failure: true
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
8.2 业务项目中引用
include:
- project: 'devops/ci-templates'
ref: main
file: '/sonar-scan.yml'
stages:
- test
- sonar
sonar-scan:
extends: .sonar-scan-template
dependencies:
- unit-test
🔍 9. 常见问题排查
9.1 扫描报错 Not authorized
- 检查
SONAR_TOKEN是否正确配置且未过期 - 确认 Token 有对应项目的 Execute Analysis 权限
- 检查 GitLab CI Variable 是否设置为 Masked(不要同时设为 Protected,除非分支也是 Protected)
9.2 SCM provider for this project is: git 相关警告
- 确保
GIT_DEPTH: "0",全量 clone 代码 - 如果使用
GIT_STRATEGY: fetch,确认.git目录完整
9.3 Quality Gate 一直显示 NONE
- 确认项目已关联 Quality Gate
- 检查 Quality Gate 条件是否针对 New Code(增量代码)
- 确认
sonar.qualitygate.wait=true已设置
9.4 覆盖率始终为 0%
- 确认 JaCoCo 报告路径正确:
target/site/jacoco/jacoco.xml - 确认
unit-testJob 的 artifacts 包含了 JaCoCo 报告 - 检查
sonar.coverage.jacoco.xmlReportPaths路径配置
9.5 MR Pipeline 未触发
- 确认
.gitlab-ci.yml中使用了rules而非only/except - 检查条件:
$CI_PIPELINE_SOURCE == "merge_request_event" - 确认项目 Settings → CI/CD → General Pipelines 中未禁用 MR Pipeline
9.6 扫描耗时过长
- 配置
sonar.exclusions排除无需扫描的文件 - 启用 SonarScanner 缓存(
.sonar/cache) - 考虑增加 Runner 资源或使用专用 Runner
💡 10. 最佳实践
- 仅扫描增量代码:使用
sonar.pullrequest.*参数,仅分析 MR 变更部分,减少扫描时间 - Quality Gate 设为 Warning 而非 Block:初期使用
allow_failure: true,待团队适应后再改为阻塞合并 - 缓存 Scanner 数据:通过 GitLab CI cache 缓存
.sonar/cache和.m2/repository - 统一模板管理:使用
include引用共享模板,避免各项目配置不一致 - 分支策略:
- MR 分支 →
sonar.pullrequest.*参数(增量扫描) - 主分支合并后 →
sonar.branch.name参数(全量扫描,更新基线)
- MR 分支 →
- 定期清理:配置 SonarQube 的 Housekeeping 策略,清理过期分支数据

浙公网安备 33010602011771号