给 SonarQube 质量门禁接 GitHub Actions 时,我按改动频率和修复成本把指标分了三档,豁免规则只留给有记录的例外

把 SonarQube 质量门禁挂到 GitHub Actions 上,难点从来不是“怎么接”——一个 sonarqube/sonarqube-scan-action 加上几个环境变量就能跑通。真正的取舍在门禁策略本身:哪些指标必须硬卡,哪些可以放水,豁免该走什么流程才不至于三个月后变成一张废纸。

我现在的做法是把质量门禁指标按改动频率修复成本分成三档,豁免规则只给有记录、可追溯的例外,不接受“先跳过再说”。这套规则在三个前端项目上跑了快两年,下面拆开讲。

三档指标的设计逻辑

第一档是硬性阻断项,直接对应“代码进了主干就难回头”的那类问题。我用的是:

  • 重复率 > 3%(前端项目里重复代码几乎都是复制粘贴的组件草稿,改一处漏一处是常态)
  • 安全热点 A 级及以上 > 0
  • 可靠性评级低于 A

这三个指标有一个共同点:修复成本随代码生命周期指数上升。重复代码今天抽个函数半小时,三个月后两份拷贝各自长出不同的业务分支,再合并就是一场小型重构。安全热点同理,现在不堵,等它进入依赖链上游就晚了。所以这一档不做任何豁免,连 PR 级别的 sonar.qualitygate.wait=true 都直接卡死。

第二档是高成本维护项,允许在 PR 里整改但必须清零才能合入:

  • 新增代码覆盖率 < 80%
  • 新增代码重复率 > 3%
  • 新增代码安全热点 > 0

这里的关键词是“新增”。全量代码的覆盖率对老项目是个历史包袱,但新增代码没有理由低于 80%——你新写的函数自己都不测,指望 QA 帮你兜底,这在 2024 年之后的前端项目里说不过去。SonarQube 的 new_code 周期默认按版本走,我在 Actions 里显式指定了 sonar.projectKeysonar.qualitygate.wait=true,配合 sonar.newCode.referenceBranch=main,让门禁只对 PR 相对主干的增量生效。

第三档是可延后项,不卡合入但会以注释形式留在 PR 里:

  • 代码异味总数
  • 认知复杂度超阈值的函数
  • 可维护性评级为 B 的项目

这类问题放行的判断依据是:修复成本低但分布广,强行在一次 PR 里全清会导致 diff 爆炸,反而增加 review 负担。我的做法是在 SonarQube 里把这类规则设为“信息”级别,门禁不阻断,但 Actions 的 PR 评论会列出 Top 5 问题文件,让 reviewer 心里有数。

豁免规则:只认记录,不认口头承诺

豁免是质量门禁最容易腐化的地方。我见过太多团队从“这个规则太严格了先跳过”开始,半年后门禁里躺着一百多条豁免,CI 绿灯形同虚设。

我现在的规则是:任何豁免必须先落一条带编号的记录,否则门禁不接受。具体做法是在 SonarQube 里用 sonar.issue.ignore.multicriteria 配合一个自定义的“豁免原因”字段,但更实际的是在 GitHub Actions 里加一个显式的 check:

# .github/workflows/sonarqube.yml 核心片段
- name: SonarQube Scan
  uses: sonarqube/sonarqube-scan-action@v4
  env:
    SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
    SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
  with:
    args: >
      -Dsonar.projectKey=${{ github.event.repository.name }}
      -Dsonar.qualitygate.wait=true
      -Dsonar.newCode.referenceBranch=main
      -Dsonar.issue.ignore.multicriteria=e1
      -Dsonar.issue.ignore.multicriteria.e1.ruleKey=typescript:S107
      -Dsonar.issue.ignore.multicriteria.e1.resourceKey=**/legacy/**/*
      -Dsonar.issue.ignore.multicriteria.e1.message=LEGACY-2024-Q3

注意那个 message=LEGACY-2024-Q3——这不是 SonarQube 的原生功能,是我在团队约定里强加的:每条豁免规则必须在 message 里带一个 Jira/Linear 单号,单号对应的 ticket 里写清楚三件事:为什么豁免、计划什么时候解除、谁负责跟进。没有单号的豁免配置会在 code review 时被直接打回。

这条规则执行起来不复杂,但效果很明显。过去一年里,我们团队只有 4 条豁免记录,其中 3 条已经按计划解除,剩下 1 条是 typescript:S107(函数参数过多)针对 legacy 目录的豁免,单号里有明确的迁移时间表。

Actions 接入的实操要点

SonarQube 官方 Action 的默认行为是扫描完就结束,门禁结果不会自动反馈到 PR 的 check status。要让 PR 真正被卡住,需要两步:

  1. 在 SonarQube 服务端配置 GitHub PR 集成(Administration → DevOps Platform Integration),填入 GitHub App 的 credentials;
  2. Actions 里用 sonarqube/sonarqube-scan-action 跑扫描后,不要手动写任何 status check 逻辑,让 SonarQube 通过 PR decoration 直接往 GitHub 写 check。

这里有个容易踩的坑:如果 Actions 里同时配了 pull_requestpush 两个 trigger,SonarQube 对同一 commit 会收到两次扫描请求,导致 PR 上的 check 状态闪烁。我的方案是只保留 pull_request trigger,主干合并后由 SonarQube 的 branch 分析自动补齐,不需要在 Actions 里重复跑。

另一个细节:前端项目务必把 sonar.sources 限定在 src/ 目录,把 node_modules/dist/coverage/ 排除。SonarQube 默认会扫所有文件,前端项目里 node_modules 的扫描时间能轻松拖到 5 分钟以上,而且会把第三方库的漏洞误报成你自己的问题。

args: >
  -Dsonar.sources=src
  -Dsonar.exclusions=**/*.test.ts,**/*.spec.ts,**/*.d.ts
  -Dsonar.coverage.exclusions=**/*.config.ts,**/index.ts
  -Dsonar.javascript.lcov.reportPaths=coverage/lcov.info

最后是覆盖率数据。前端项目用 Jest 或 Vitest 跑单测时,产物路径要跟 SonarQube 的 lcov.reportPaths 对齐。我用 Vitest 的配置是 coverage.reporter: ['lcov', 'text'],跑完测试后 coverage/lcov.info 直接传给 SonarQube,不用额外转换。

这套规则的实际效果

接入这套三档门禁后,主干分支的新增代码覆盖率稳定在 87% 以上,重复率从最初的全量 6.2% 降到了 2.8%。最明显的变化是 PR 的 review 时长——以前大量时间花在“这段代码能不能用”的基础判断上,现在门禁先过一遍,reviewer 可以集中精力看业务逻辑和设计选择。

当然也有代价。最直接的是新人在头两周会频繁被门禁打回,PR 里一片红色 check。但这件事我不认为是坏事——门禁的职责就是让规则显性化,新人早撞墙比晚撞墙成本低得多。真正需要关注的是门禁规则本身是否合理,而不是频繁调整规则来适应个体习惯。

常见问题

为什么不直接把重复率阈值设成 0%?

前端项目里 0% 重复率不现实,类型定义、工具函数、测试 mock 数据都会产生合理的重复。3% 是一个经过验证的平衡点:低于这个值,复制粘贴的问题基本是零散的偶发行为;高于这个值,说明有人在成块地拷贝组件草稿,值得在 review 时追问。

门禁卡死 PR 后,紧急修复怎么办?

我的做法是紧急修复不豁免门禁,而是缩小改动范围。真正紧急的 hotfix 通常改动很小,覆盖率 80% 的门槛对 20 行以内的 diff 几乎不构成障碍。如果一个小改动都覆盖不到 80%,说明代码结构本身有问题,这时候绕过门禁只会把问题往后推。

SonarQube 的 PR 分析需要额外付费吗?

Community Edition 不支持 PR decoration,需要 Developer Edition 及以上。如果预算有限,可以在 Actions 里用 sonarqube/sonarqube-quality-gate-action 轮询 CE 接口判断门禁状态,但体验不如原生 PR 集成。预算允许的话,Developer Edition 的 PR 分析是值得的——它把门禁结果直接挂在 GitHub check 上,reviewer 不需要额外打开 SonarQube 页面。

豁免单号写在 message 里,SonarQube 会自动校验吗?

不会。这是团队约定,靠 code review 强制执行。如果团队想要更硬的约束,可以在 Actions 里加一个脚本,扫描 SonarQube 的 qualitygates/project_status 响应,检查是否有豁免规则缺少单号,有就主动 fail 掉 workflow。这个脚本我写过一版,核心逻辑不复杂,但需要维护豁免规则的解析逻辑,团队规模小于 20 人时用 review 约束就够了。