路线图

13-质量门禁

星辉 2026-07-02 阅读 4 min 645 字 路线图
13-质量门禁 封面

CI 流水线不只是"把代码构建成镜像",还要在构建过程中拦住质量问题。质量门禁(Quality Gate)是流水线里的检查点——不通过就不让进下一阶段。本章讲四类门禁:覆盖率、代码质量、Lint、安全扫描,以及它们和流水线的联动方式。

代码覆盖率门槛#

覆盖率本身不是目的#

先泼盆冷水:追覆盖率数字是反模式。80% 覆盖率不代表代码质量高——可能是大量无意义测试凑数。覆盖率真正的作用是发现"没被测到的代码",而不是"达到某个百分比"。

门槛设置#

合理的覆盖率门禁:

yaml
# 不要求绝对值,要求"不下降"
coverage:
  script:
    - go test -coverprofile=coverage.out ./...
    - go tool cover -func=coverage.out | tail -1
  # GitLab CI 自动提取覆盖率
  coverage: '/total:\s+\(statements\)\s+(\d+\.\d+)%/'

两类门槛:

  1. 绝对门槛:覆盖率低于 X% 失败。如新代码必须 ≥ 70%
  2. 相对门槛:覆盖率比上次下降超过 X% 失败。如下降超过 2% 失败

相对门槛更合理——它防止"新代码没测试就合",又不强迫老代码补到某个值。SonarQube 默认用相对门槛。

工具#

  • Go:go test -coverprofile,配合 go tool cover
  • Java:JaCoCo
  • JavaScript/TypeScript:Istanbul/nyc
  • Python:coverage.py / pytest-cov

报告格式统一用 Cobertura XML 或 lcov,主流 CI 平台和 SonarQube 都能解析。

SonarQube 集成#

SonarQube 是代码质量平台,覆盖:

  • Bug 检测:静态分析找潜在 bug
  • 漏洞检测:安全相关代码模式
  • 代码异味:坏味道(不一定是 bug,但影响可维护性)
  • 重复代码:复制粘贴检测
  • 复杂度:圈复杂度高的函数告警
  • 覆盖率:集成测试报告

CI 集成#

yaml
# GitLab CI
sonarqube:
  stage: scan
  image: maven:3.9
  script:
    - mvn sonar:sonar
        -Dsonar.host.url=$SONAR_HOST
        -Dsonar.token=$SONAR_TOKEN
        -Dsonar.projectKey=$CI_PROJECT_NAME
  only:
    - main
    - merge_requests
yaml
# GitHub Actions
- uses: SonarSource/sonarqube-scan-action@v5
  env:
    SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
    SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}

Quality Gate#

SonarQube 的 Quality Gate 是一组条件——满足才算"通过":

text
新代码覆盖率 ≥ 80%
新代码重复率 ≤ 3%
新代码 Bug = 0
新代码 Vulnerabilities = 0
新代码 Code Smells ≤ 5

CI 流水线调用 SonarQube API 检查 Quality Gate 状态:

bash
# 触发分析后轮询 Quality Gate
ANALYSIS_ID=$(curl -s -u $SONAR_TOKEN: \
  "$SONAR_HOST/api/project_analyses/search?project=myproject" | jq -r '.analyses[0].id')

STATUS=$(curl -s -u $SONAR_TOKEN: \
  "$SONAR_HOST/api/quality_gates/project_status?projectKey=myproject" \
  | jq -r '.projectStatus.status')

if [ "$STATUS" != "OK" ]; then
  echo "Quality Gate failed: $STATUS"
  exit 1
fi

Lint 和格式化卡点#

Lint#

Lint 检查代码风格和潜在错误。主流语言:

语言工具
Gogolangci-lint
JavaScript/TypeScriptESLint
Pythonruff / pylint / flake8
JavaSpotBugs / Checkstyle
Rustclippy

CI 集成:

yaml
lint:
  stage: test
  image: golangci/golangci-lint:v1.57
  script:
    - golangci-lint run --timeout=5m ./...
  allow_failure: false    # 失败必须修

格式化#

格式不一致会导致无意义的 diff 和合并冲突。主流格式化工具:

语言工具
Gogofmt / gofumpt
JavaScript/TypeScriptPrettier
Pythonblack
Rustrustfmt

强制策略:CI 里跑格式化检查,格式不对直接 fail:

bash
# 检查是否已格式化(不修改文件)
if [ -n "$(gofmt -l .)" ]; then
  echo "以下文件未格式化:"
  gofmt -l .
  exit 1
fi

或者更友好——CI 自动格式化并提交:

yaml
format:
  script:
    - gofmt -w .
    - |
      if [ -n "$(git diff)" ]; then
        git config user.name "CI Bot"
        git commit -am "style: auto-format"
        git push
      fi

依赖与镜像安全扫描#

SCA(软件成分分析)#

扫描第三方依赖中的已知漏洞(CVE):

工具扫描对象
Trivy容器镜像、文件系统、SBOM
Snyk多语言依赖
DependabotGitHub 依赖(内置)
Renovate多平台依赖升级
grype容器镜像、SBOM
yaml
# GitLab CI
dependency-scan:
  stage: scan
  image:
    name: aquasec/trivy:latest
    entrypoint: [""]
  script:
    - trivy fs --exit-code 1 --severity HIGH,CRITICAL .
  allow_failure: true    # 高危漏洞警告但不阻塞(按需调整)

镜像扫描#

构建出镜像后扫描:

yaml
image-scan:
  script:
    - docker build -t myapp:${TAG} .
    - trivy image --exit-code 1 --severity CRITICAL myapp:${TAG}
    - docker push myapp:${TAG}

或集成到镜像仓库(Harbor)——push 后自动扫描,详见 15 章。

从 Schema Check 看质量门禁实践#

质量门禁的一个典型实战场景是 DB Schema 检查——防止破坏性 DDL(如 DROP COLUMN)带着服务一起炸。这个场景揭示了门禁设计的几个关键原则。

双 Stage 设计#

text
PR 阶段(开发可见)        合并后(PRE → PROD)
   │                          │
   ▼                          ▼
pre stage (warning)       post stage (fail)
不阻塞,只提醒              阻塞 PROD 部署
Stage时机模式行为
prePR 创建后 / PRE 部署前warning + exit 0钉钉/PR 评论提醒,不阻塞
post合并到 PRE 后 / PROD 部署前fail + exit 1命中破坏性 DDL → 阻塞下一 stage

pre stage 是"给开发看的镜子"——还没到 PROD,看到问题立刻修;post stage 是"给基础设施留的最后一道闸"——pre 被忽略时兜底,必须真阻塞。

风险等级配置#

yaml
# ignore-rules.yaml
risk_levels:
  block:                       # 命中即 post stage fail
    - DROP_TABLE
    - DROP_COLUMN
    - ALTER_COLUMN_TYPE
    - RENAME_TABLE
  warn:                        # pre/post 都通知,不阻塞
    - ADD_COLUMN_NOT_NULL_NO_DEFAULT
    - ADD_UNIQUE_INDEX
  silent:                      # 静默
    - ADD_COLUMN_NULLABLE
    - ADD_INDEX_NORMAL

ignore_tables:                 # 已知豁免
  biz_db_a:
    - tmp_migration_*          # 临时表
    - audit_log_2024_*

豁免规则集中维护、走 PR review——避免"开发自己加豁免绕过门禁"。

门禁与流水线联动#

分层门禁#

text
① pre-commit hook(本地)    ─►  最快、最便宜(lint、格式)
② PR check(CI)              ─►  完整测试、覆盖率、SCA
③ merge queue(合并前)       ─►  post stage 阻塞类检查
④ pre-deploy(部署前)        ─►  schema check、镜像签名验证
⑤ runtime(运行时)           ─►  准入控制、Falco 监控

每层职责不同:

  • 本地 hook:拦截低级错误(格式、lint),10 秒内反馈
  • PR check:完整 CI 检查,分钟级
  • merge queue:合并前最后一道闸,阻塞类
  • pre-deploy:部署前的 schema、签名检查
  • runtime:运行时的准入控制和监控

阻塞 vs 警告#

明确每个门禁是"阻塞"还是"警告":

yaml
# 阻塞类(必须修才能合并)
lint:
  allow_failure: false
test:
  allow_failure: false
critical-vuln-scan:
  allow_failure: false

# 警告类(提醒但不阻塞)
coverage-drop:
  allow_failure: true
high-vuln-scan:
  allow_failure: true

原则:

  • 阻塞类用于"不修一定出事"的问题(语法错误、破坏性 DDL、Critical CVE)
  • 警告类用于"应该修但不至于阻塞"的问题(覆盖率下降、High CVE)

纸老虎陷阱#

门禁最大的风险不是"没装",而是"装了但没真拦"——技术上线了、状态绿色,但实际什么都没守护。比如:

  • post stage 复用了 pre stage 的 continueOnError: true,永远 exit 0
  • 镜像扫描配了但 allow_failure: true,扫描出 Critical 也照常部署
  • 门禁规则太宽,所有改动都"silent",等于没装

反制:上线后做一次"反向 review"——故意制造一次失败,看 stage 是不是真红了、下游是不是真停了。"看起来在拦但实际没拦"比"完全没装"还危险。

实战要点#

  • 覆盖率用相对门槛,不用绝对门槛。防止"为追数字凑测试"和"老代码补不动"。
  • SonarQube Quality Gate 是最完整的质量门禁。新代码 Bug = 0 + 覆盖率 ≥ 80% + 重复率 ≤ 3%,是业界面板。
  • 格式化自动提交优于 fail。CI 自动 format + commit 比让开发手动修更友好。
  • 门禁要分层:本地 hook → PR check → merge queue → pre-deploy → runtime。单点门禁容易绕过。
  • 阻塞和警告要分清。所有检查都阻塞会让开发绕过门禁;所有检查都警告等于没门禁。
  • 定期"反向 review"门禁有效性。故意制造失败 case,看门禁是不是真的拦住了。
  • 集中维护豁免规则。ignore-rules 走 PR review,避免开发自己加豁免绕过。

小结#

质量门禁是 CI 流水线的"免疫系统"——覆盖率、代码质量、Lint、安全扫描、Schema 检查各管一摊,合起来把质量问题拦在合并前。

门禁设计的核心原则:分层、分等级、定期验证。分层让不同问题在不同时机被发现;分等级(阻塞 vs 警告)让门禁不至于太严被绕过、也不至于太松形同虚设;定期验证避免"纸老虎陷阱"。

下一阶段进入制品与镜像管理,讲怎么把 CI 构建出的镜像存好、管好、安全地分发到生产。