🐻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 创建项目

  1. 登录 SonarQube → Projects → Create Project → Manually
  2. 填写 Project Key(如 my-project)和 Display Name
  3. 记录 Project Key,后续配置需要使用

3.2 生成 Token

  1. 进入 My Account → Security → Generate Tokens
  2. 输入 Token 名称(如 gitlab-ci-token),类型选择 Project Analysis Token
  3. 关联到对应项目,点击 Generate
  4. 复制并妥善保存 Token(仅显示一次)

3.3 配置 Quality Gate(可选)

  1. 进入 Quality Gates → 选择或新建一个 Quality Gate
  2. 推荐条件:
指标 条件 阈值
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
  1. 将 Quality Gate 关联到目标项目

3.4 配置 GitLab 集成(Developer Edition+)

如果使用 Developer Edition 及以上版本,可启用原生 MR 装饰功能。

  1. 进入 Administration → Configuration → ALM Integrations → GitLab
  2. 填写 GitLab URL 和 Personal Access Token(需要 api 权限)
  3. 在项目设置中绑定 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-test Job 的 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. 最佳实践

  1. 仅扫描增量代码:使用 sonar.pullrequest.* 参数,仅分析 MR 变更部分,减少扫描时间
  2. Quality Gate 设为 Warning 而非 Block:初期使用 allow_failure: true,待团队适应后再改为阻塞合并
  3. 缓存 Scanner 数据:通过 GitLab CI cache 缓存 .sonar/cache 和 .m2/repository
  4. 统一模板管理:使用 include 引用共享模板,避免各项目配置不一致
  5. 分支策略:
    • MR 分支 → sonar.pullrequest.* 参数(增量扫描)
    • 主分支合并后 → sonar.branch.name 参数(全量扫描,更新基线)
  6. 定期清理:配置 SonarQube 的 Housekeeping 策略,清理过期分支数据

posted @ 2026-04-16 09:56  丿似锦  阅读(112)  评论(0)    收藏  举报