13-质量门禁
CI 流水线不只是"把代码构建成镜像",还要在构建过程中拦住质量问题。质量门禁(Quality Gate)是流水线里的检查点——不通过就不让进下一阶段。本章讲四类门禁:覆盖率、代码质量、Lint、安全扫描,以及它们和流水线的联动方式。
代码覆盖率门槛#
覆盖率本身不是目的#
先泼盆冷水:追覆盖率数字是反模式。80% 覆盖率不代表代码质量高——可能是大量无意义测试凑数。覆盖率真正的作用是发现"没被测到的代码",而不是"达到某个百分比"。
门槛设置#
合理的覆盖率门禁:
# 不要求绝对值,要求"不下降"
coverage:
script:
- go test -coverprofile=coverage.out ./...
- go tool cover -func=coverage.out | tail -1
# GitLab CI 自动提取覆盖率
coverage: '/total:\s+\(statements\)\s+(\d+\.\d+)%/'两类门槛:
- 绝对门槛:覆盖率低于 X% 失败。如新代码必须 ≥ 70%
- 相对门槛:覆盖率比上次下降超过 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 集成#
# 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# 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 是一组条件——满足才算"通过":
新代码覆盖率 ≥ 80%
新代码重复率 ≤ 3%
新代码 Bug = 0
新代码 Vulnerabilities = 0
新代码 Code Smells ≤ 5CI 流水线调用 SonarQube API 检查 Quality Gate 状态:
# 触发分析后轮询 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
fiLint 和格式化卡点#
Lint#
Lint 检查代码风格和潜在错误。主流语言:
| 语言 | 工具 |
|---|---|
| Go | golangci-lint |
| JavaScript/TypeScript | ESLint |
| Python | ruff / pylint / flake8 |
| Java | SpotBugs / Checkstyle |
| Rust | clippy |
CI 集成:
lint:
stage: test
image: golangci/golangci-lint:v1.57
script:
- golangci-lint run --timeout=5m ./...
allow_failure: false # 失败必须修格式化#
格式不一致会导致无意义的 diff 和合并冲突。主流格式化工具:
| 语言 | 工具 |
|---|---|
| Go | gofmt / gofumpt |
| JavaScript/TypeScript | Prettier |
| Python | black |
| Rust | rustfmt |
强制策略:CI 里跑格式化检查,格式不对直接 fail:
# 检查是否已格式化(不修改文件)
if [ -n "$(gofmt -l .)" ]; then
echo "以下文件未格式化:"
gofmt -l .
exit 1
fi或者更友好——CI 自动格式化并提交:
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 | 多语言依赖 |
| Dependabot | GitHub 依赖(内置) |
| Renovate | 多平台依赖升级 |
| grype | 容器镜像、SBOM |
# GitLab CI
dependency-scan:
stage: scan
image:
name: aquasec/trivy:latest
entrypoint: [""]
script:
- trivy fs --exit-code 1 --severity HIGH,CRITICAL .
allow_failure: true # 高危漏洞警告但不阻塞(按需调整)镜像扫描#
构建出镜像后扫描:
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 设计#
PR 阶段(开发可见) 合并后(PRE → PROD)
│ │
▼ ▼
pre stage (warning) post stage (fail)
不阻塞,只提醒 阻塞 PROD 部署| Stage | 时机 | 模式 | 行为 |
|---|---|---|---|
| pre | PR 创建后 / PRE 部署前 | warning + exit 0 | 钉钉/PR 评论提醒,不阻塞 |
| post | 合并到 PRE 后 / PROD 部署前 | fail + exit 1 | 命中破坏性 DDL → 阻塞下一 stage |
pre stage 是"给开发看的镜子"——还没到 PROD,看到问题立刻修;post stage 是"给基础设施留的最后一道闸"——pre 被忽略时兜底,必须真阻塞。
风险等级配置#
# 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——避免"开发自己加豁免绕过门禁"。
门禁与流水线联动#
分层门禁#
① 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 警告#
明确每个门禁是"阻塞"还是"警告":
# 阻塞类(必须修才能合并)
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 构建出的镜像存好、管好、安全地分发到生产。