前面已经把 GitLab 和 SonarQube 都部署到了 Kubernetes 里:

GitLab     https://gitlab.jihw.top
SonarQube  https://sonarqube.jihw.top
Ingress    192.168.3.230

这篇记录两者配合使用的流程:代码放在 GitLab,流水线由 GitLab Runner 执行,扫描结果上传到 SonarQube。

本文用这个前端项目作为检测示例:

D:\workspace\jihw\qianduoduo\frontend

它是一个 Vite + Vue 项目,关键文件是:

package.json
package-lock.json
vite.config.js
src/

当前 package.json 里已有:

{
  "scripts": {
    "build": "vite build",
    "dev": "vite --host 0.0.0.0"
  }
}

所以 CI 里可以先执行 npm cinpm run build,确认前端至少能正常构建,然后再执行 SonarQube 扫描。

整体流程

1. GitLab 创建项目
2. 把 frontend 代码推到 GitLab
3. SonarQube 创建项目并生成 Token
4. GitLab 项目配置 CI/CD Variables
5. 提交 sonar-project.properties
6. 提交 .gitlab-ci.yml
7. GitLab Runner 执行流水线
8. SonarQube 查看质量报告

确认 GitLab Runner

GitLab 本身只负责管理流水线,真正执行 job 的是 Runner。

先在 GitLab 页面确认 Runner:

Admin Area -> CI/CD -> Runners

或者进入具体项目:

Project -> Settings -> CI/CD -> Runners

创建runner

如果没有 Runner,先安装一个 Kubernetes Runner。示例:

helm repo add gitlab https://charts.gitlab.io/
helm repo update

helm upgrade --install gitlab-runner gitlab/gitlab-runner \
  -n gitlab \
  --set gitlabUrl=https://gitlab.jihw.top \
  --set runnerToken='你的 Runner Token' \
  --set rbac.create=true

这里使用 Kubernetes executor,所以建议一开始就打开 rbac.create=true。否则 Runner 虽然能注册成功,也能从 GitLab 接到 job,但真正执行时可能会因为 ServiceAccount 权限不足而失败。

典型错误如下:

secrets is forbidden:
User "system:serviceaccount:gitlab:default" cannot create resource "secrets"

如果已经安装过 Runner,可以用下面的方式补上 RBAC:

helm upgrade gitlab-runner gitlab/gitlab-runner \
  -n gitlab \
  --reuse-values \
  --set rbac.create=true

还要注意 Runner 的标签。比如创建 Runner 时给它设置了 qianduoduo-frontend 标签,而 .gitlab-ci.yml 里的 job 没写 tags,默认情况下这个 Runner 不一定会接未打标签的 job。

有两种处理方式。

方式一是在 GitLab 页面允许 Runner 执行未打标签的 job:

Project -> Settings -> CI/CD -> Runners -> 编辑 Runner
勾选 Run untagged jobs

方式二是在 .gitlab-ci.yml 里给 job 明确加上标签:

build:
  stage: build
  tags:
    - qianduoduo-frontend

sonarqube:
  stage: scan
  tags:
    - qianduoduo-frontend

Runner 能访问 https://sonarqube.jihw.top 时,CI 里可以直接使用这个域名。
如果 DNS 没配好,也可以在 CI 变量里把 SonarQube 地址写成集群内 Service 地址。

常见两种写法:

SONAR_HOST_URL=https://sonarqube.jihw.top

或者:

SONAR_HOST_URL=http://sonarqube-sonarqube.sonarqube.svc.cluster.local:9000

创建 GitLab 项目

在 GitLab 里创建一个项目,例如:

qianduoduo-frontend

然后在本地项目目录执行:

cd D:\workspace\jihw\qianduoduo\frontend

git init
git remote add origin https://gitlab.jihw.top/root/qianduoduo-frontend.git
git add .
git commit -m "init frontend"
git branch -M main
git push -u origin main

如果这个目录已经是 Git 仓库,只需要确认 remote 指向 GitLab:

git remote -v

不要把 node_modules 推到 GitLab。建议 .gitignore 至少包含:

node_modules/
dist/
web_dist/
.sonar/
coverage/

配置免密推送

直接使用 HTTPS remote 时,第一次 git push 会要求输入 GitLab 账号和密码。GitLab 通常不建议直接用登录密码推代码,更推荐使用 Personal Access Token。

当前这套 GitLab 部署里,Web Ingress 已经可用:

https://gitlab.jihw.top

GitLab Shell 本身是集群内 ClusterIP

gitlab-gitlab-shell.gitlab.svc.cluster.local:22

如果要从 Windows 本机使用 SSH 免密推送,需要额外把 22 端口暴露到集群外。当前已经通过 ingress-nginx 的 TCP 转发把 192.168.3.230:22 转发到了 GitLab Shell。

暴露命令如下:

helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update ingress-nginx

cat <<'EOF' | helm upgrade ingress-nginx ingress-nginx/ingress-nginx \
  -n ingress-nginx \
  --version 4.15.1 \
  -f - \
  --timeout 5m
controller:
  ingressClassResource:
    default: true
  containerPort:
    "22-tcp": 22
  service:
    type: LoadBalancer
    ports:
      "22-tcp": 22
    targetPorts:
      "22-tcp": "22-tcp"

tcp:
  "22": gitlab/gitlab-gitlab-shell:22
EOF

确认:

kubectl -n ingress-nginx get svc ingress-nginx-controller -o wide
kubectl -n ingress-nginx get cm ingress-nginx-tcp -o yaml

期望看到:

ingress-nginx-controller   LoadBalancer   ...   192.168.3.230   80:.../TCP,443:.../TCP,22:.../TCP

Windows 上测试:

Test-NetConnection gitlab.jihw.top -Port 22
ssh -T git@gitlab.jihw.top

如果 SSH key 已经添加到 GitLab,应该看到类似:

Welcome to GitLab, @root!

方案一:HTTPS + Token

不想暴露 SSH 端口时,也可以继续用:

HTTPS remote + Personal Access Token + Git Credential Manager 记住凭据

在 GitLab 创建 Token:

头像 -> Preferences -> Access Tokens

创建一个 Project 或 Personal Access Token,勾选:

read_repository
write_repository

如果还要通过 API 操作项目,可以额外勾选:

api

然后确认当前 remote 是 HTTPS:

cd D:\workspace\jihw\qianduoduo\frontend
git remote -v

如果不是,改成:

git remote set-url origin https://gitlab.jihw.top/root/qianduoduo-frontend.git

第一次推送:

git push -u origin main

提示输入时:

Username: GitLab 用户名,例如 root
Password: 粘贴刚生成的 Token,不是 GitLab 登录密码

Windows 上 Git for Windows 默认带 Git Credential Manager。第一次输入后,它会把凭据保存到 Windows 凭据管理器,后面再 git push 就不需要重复输入。

可以确认 credential helper:

git config --global credential.helper

如果没有配置,可以启用:

git config --global credential.helper manager

如果输错过 Token,可以到这里删除旧凭据:

控制面板 -> 凭据管理器 -> Windows 凭据

找到 git:https://gitlab.jihw.top 相关条目删除,然后重新 git push

方案二:SSH Key

SSH key 是更传统的免密方式,前提是上面的 GitLab SSH 入口已经暴露给 Windows 本机访问。

先生成 SSH key:

ssh-keygen -t ed25519 -C "rx@gitlab.jihw.top"

查看公钥:

cat $env:USERPROFILE\.ssh\id_ed25519.pub

复制公钥,添加到 GitLab:

头像 -> Preferences -> SSH Keys

当前已经把 GitLab Shell 暴露为:

ssh://git@gitlab.jihw.top:22

就可以把 remote 改成 SSH:

git remote set-url origin git@gitlab.jihw.top:root/qianduoduo-frontend.git

测试:

ssh -T git@gitlab.jihw.top
git push

如果 SSH 端口不是 22,而是例如 2222,remote 要写成:

git remote set-url origin ssh://git@gitlab.jihw.top:2222/root/qianduoduo-frontend.git

如果没有额外暴露 GitLab Shell,直接配置 SSH key 仍然会连不上。这时先用 HTTPS + Token 更稳。

创建 SonarQube 项目

登录 SonarQube:

https://sonarqube.jihw.top

创建项目:

Projects -> Create Project -> Manually

示例:

Project display name: qianduoduo-frontend
Project key:          qianduoduo-frontend
Main branch:          main

创建完成后生成扫描 Token。注意不要去这个页面:

Administration -> Configuration -> Authentication -> GitLab

也就是类似这个地址:

https://sonarqube.jihw.top/admin/settings?category=authentication&tab=gitlab

那个页面是配置“使用 GitLab 账号登录 SonarQube”的,不是生成 CI 扫描 Token 的地方。

扫描 Token 通常在当前登录用户的账号设置里生成:

右上角头像 -> My Account -> Security -> Generate Tokens

Token 类型可以选择:

Project Analysis Token

Token 只显示一次,生成后马上复制。 Token

配置 GitLab 变量

进入 GitLab 项目:

Project -> Settings -> CI/CD -> Variables

CICD变量配置

添加:

SONAR_HOST_URL = https://sonarqube.jihw.top
SONAR_TOKEN    = SonarQube 里生成的 Token

建议:

SONAR_TOKEN 勾选 Masked
SONAR_TOKEN 勾选 Protected 取决于分支策略
SONAR_HOST_URL 不需要 Masked

如果 Runner 在集群内,并且还没给 DNS 配 sonarqube.jihw.top,可以先用 Service 地址:

SONAR_HOST_URL = http://sonarqube-sonarqube.sonarqube.svc.cluster.local:9000

添加 sonar-project.properties

D:\workspace\jihw\qianduoduo\frontend 下创建:

sonar-project.properties

内容:

sonar.projectKey=qianduoduo-frontend
sonar.projectName=qianduoduo-frontend
sonar.sourceEncoding=UTF-8

sonar.sources=src
sonar.tests=

sonar.exclusions=node_modules/**,dist/**,web_dist/**,coverage/**,.sonar/**
sonar.javascript.lcov.reportPaths=coverage/lcov.info

当前项目还没有单元测试和覆盖率文件,所以 coverage/lcov.info 不存在也没关系。后面接入 Vitest 或其他测试框架后,再让 CI 生成覆盖率即可。

如果暂时不想配置覆盖率,也可以先删掉这一行:

sonar.javascript.lcov.reportPaths=coverage/lcov.info

添加 .gitlab-ci.yml

在项目根目录创建:

.gitlab-ci.yml

基础版本:

stages:
  - build
  - scan

build:
  stage: build
  image: node:22-alpine
  cache:
    key:
      files:
        - package-lock.json
    paths:
      - .npm/
  script:
    - npm ci --cache .npm --prefer-offline
    - npm run build
    - mkdir -p web_dist
    - cp -r ../web_dist/. web_dist/
  artifacts:
    when: always
    expire_in: 1 week
    paths:
      - web_dist/

sonarqube:
  stage: scan
  image:
    name: sonarsource/sonar-scanner-cli:latest
    entrypoint: [""]
  variables:
    SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
    GIT_DEPTH: "0"
  cache:
    key: "${CI_JOB_NAME}"
    paths:
      - .sonar/cache
  script:
    - sonar-scanner
      -Dsonar.host.url="${SONAR_HOST_URL}"
      -Dsonar.token="${SONAR_TOKEN}"
  needs:
    - build
  allow_failure: false

这里分成两个 job:

build      先确认 Vite 项目能构建
sonarqube  再上传代码质量扫描结果

vite.config.js 里现在写的是:

build: {
  outDir: '../web_dist',
  emptyOutDir: true
}

所以 CI 产物路径写成:

script:
  - npm run build
  - mkdir -p web_dist
  - cp -r ../web_dist/. web_dist/

artifacts:
  paths:
    - web_dist/

如果以后把构建输出改回项目内的 dist,这里也要同步改成:

artifacts:
  paths:
    - dist/

推送并触发流水线

提交配置:

cd D:\workspace\jihw\qianduoduo\frontend

git add sonar-project.properties .gitlab-ci.yml .gitignore
git commit -m "ci: add sonarqube scan"
git push

在 GitLab 查看流水线:

Project -> Build -> Pipelines

正常情况会看到:

build      passed
sonarqube  passed

然后回到 SonarQube 项目页面,就能看到:

Bugs
Vulnerabilities
Security Hotspots
Code Smells
Duplications
Quality Gate

让质量门禁卡住流水线

如果希望 SonarQube Quality Gate 不通过时,GitLab pipeline 直接失败,可以加:

sonarqube:
  variables:
    SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
    GIT_DEPTH: "0"
    SONAR_SCANNER_OPTS: "-Dsonar.qualitygate.wait=true"

或者直接在命令里写:

script:
  - sonar-scanner
    -Dsonar.host.url="${SONAR_HOST_URL}"
    -Dsonar.token="${SONAR_TOKEN}"
    -Dsonar.qualitygate.wait=true

这样扫描完成后,scanner 会等待 SonarQube 计算质量门禁结果。
如果 Quality Gate 是 Failed,GitLab job 也会失败。

合并请求里的用法

对于日常开发,更推荐只在 merge request 和 main 分支扫描:

sonarqube:
  stage: scan
  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.qualitygate.wait=true
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == "main"'

这样可以避免每个临时分支都频繁扫描。

常见问题

流水线一直 running 或 pending

先看 Runner 是否在线:

Project -> Settings -> CI/CD -> Runners

如果 Runner 在线,但 job 仍然一直 pending,优先检查标签是否匹配。

这次遇到的问题是:Runner 标签是 qianduoduo-frontend,并且 run_untagged=false;但是 pipeline 里的 job 没有写 tags,所以 GitLab 不会把 job 分配给这个 Runner。

修复方式二选一:

1. 在 Runner 设置里打开 Run untagged jobs
2. 或者在 .gitlab-ci.yml 的每个 job 里添加 tags: [qianduoduo-frontend]

如果 job 已经被 Runner 接走,但很快失败,继续看 Runner 日志:

kubectl -n gitlab logs deploy/gitlab-runner --tail=200

如果看到下面的权限错误:

secrets is forbidden:
User "system:serviceaccount:gitlab:default" cannot create resource "secrets"

说明 Kubernetes Runner 缺少 RBAC 权限。用 Helm 打开 RBAC:

helm upgrade gitlab-runner gitlab/gitlab-runner \
  -n gitlab \
  --reuse-values \
  --set rbac.create=true

等待 Runner 重启:

kubectl -n gitlab rollout status deploy/gitlab-runner

然后回到 GitLab 页面重试失败的 job 或 pipeline。修复后,buildsonarqube job 应该都会被 Runner 接走并执行。

Runner 无法连接 SonarQube

先在 Runner job 里临时加:

script:
  - curl -I "${SONAR_HOST_URL}"
  - sonar-scanner ...

如果域名不通,优先确认:

sonarqube.jihw.top 是否解析到 192.168.3.230
Runner Pod 是否能访问 ingress-nginx
是否需要改用 Kubernetes Service 地址

集群内 Service 地址一般更稳:

http://sonarqube-sonarqube.sonarqube.svc.cluster.local:9000

Token 错误

如果日志里有:

Not authorized
You're not authorized to analyze this project

检查:

SONAR_TOKEN 是否复制完整
Token 是否属于有项目分析权限的用户
sonar.projectKey 是否和 SonarQube 项目一致

node_modules 被扫描

确认 sonar-project.properties 里排除了:

sonar.exclusions=node_modules/**,dist/**,web_dist/**,coverage/**,.sonar/**

构建成功但扫描失败

分开看两个 job:

build 失败      先修 npm ci / npm run build
sonarqube 失败  再看 SonarQube 地址、Token、projectKey

不要把构建和扫描混在一个 job 里排查。分开以后,问题会清楚很多。

最小检查清单

GitLab 项目已经有 frontend 代码
GitLab Runner 在线
Runner 标签和 .gitlab-ci.yml 的 tags 匹配,或 Runner 允许 untagged job
Kubernetes Runner 已开启 RBAC
SonarQube 项目 key 是 qianduoduo-frontend
GitLab Variables 已配置 SONAR_HOST_URL 和 SONAR_TOKEN
项目根目录有 sonar-project.properties
项目根目录有 .gitlab-ci.yml
流水线 build 通过
流水线 sonarqube 通过
SonarQube 页面能看到扫描结果

到这里,GitLab 和 SonarQube 就串起来了。后面可以继续补单元测试和覆盖率,让质量门禁从“能扫”升级到“能拦住低质量代码”。