@be-link/prepush-gate
v2.1.3
Published
客户端 pre-push 质量门禁 CLI:非受保护分支本地跑 lint/typecheck + 完整单测门禁(小改动判定/单测差异/增量覆盖率),失败即拒绝推送
Readme
jenkins-node-quality-gate
用于统一 Node.js 项目质量门禁的 Jenkins Global Trusted Pipeline Library。
它将 Git diff 基线、小改动豁免、tests/unit 单测差异、本次变更覆盖率和服务整体覆盖率收敛在此仓库;业务 Jenkinsfile 只保留可见的 Stage 外壳和项目事实配置。
当前版本已在 Jenkins 通过 Shared Library 加载烟雾验证;业务项目接入前仍应执行其自身 canary。
固定规范
以下规则由公共库固定,业务项目不能降低或覆盖:
| 项目 | 规则 |
| --- | --- |
| 启用分支 | 默认仅标准化后的 test 分支;受控 canary 例外见下文 |
| 差异基线 | 以 GIT_PREVIOUS_SUCCESSFUL_COMMIT 为基线;因分支回退/改写不再是 HEAD 祖先时退回到二者共同祖先(merge-base)做超集 diff(只多检、不漏检);首次构建(指针确实缺失)进入 bootstrap 模式跳过本次增量门禁并建立基线;无效、无共同祖先或 checkout 过浅时仍阻断 |
| 生产源码 | sourcePaths 内的 .js / .jsx / .ts / .tsx,默认排除 .d.ts |
| 单测目录 | 固定为 tests/unit/**/* |
| 小改动豁免 | 非空白生产变更不超过 5 行、2 个文件,且没有新增、删除、重命名、复制、二进制或未知状态 |
| 本次变更覆盖率 | lines ≥ 90%、statements ≥ 90%、functions ≥ 90%、branches ≥ 80%,不达标阻断 |
| 服务整体覆盖率 | 全部可单测生产源码纳入统计;未加载源码应由 runner 的完整采集配置按 0% 纳入;仅展示趋势,不阻断 |
公共库在 initialize() 阶段一次解析并冻结基线完整 SHA;小改动、单测差异和增量覆盖率均使用同一 SHA。当上一次成功构建因分支回退/改写不再是 HEAD 祖先时,冻结的是它与 HEAD 的共同祖先(merge-base),对 共同祖先..HEAD 做 diff——这是真实增量的超集,只会多检不会漏检,从而在历史回退时不至于死锁。但仍不会缩小范围或回退到 HEAD^。
首次构建(bootstrap):当 GIT_PREVIOUS_SUCCESSFUL_COMMIT 确实缺失(全新 Job、构建历史被清空,或该 Job 首次跑门禁)时,公共库不再 fail-closed,而是进入 bootstrap 模式——本次跳过增量门禁(小改动/单测差异/单元测试与覆盖率 stage 均跳过),让构建正常走完 Build/Push/Deploy;成功后 git 插件会记录本次 commit,之后的构建从 checkout 返回值取到 GIT_PREVIOUS_SUCCESSFUL_COMMIT 即恢复正常强制。注意:pipeline 的 checkout 步骤不会把该变量自动导出到全局 env,业务 Jenkinsfile 必须在 Checkout 阶段接住返回值 env.GIT_PREVIOUS_SUCCESSFUL_COMMIT = scmVars.GIT_PREVIOUS_SUCCESSFUL_COMMIT ?: ''(见 examples/),否则每次构建都会被当作首次构建。这是一个受控的一次性口子:仅每个 Job 的第一次成功构建(及任何清空历史后的第一次)不受增量门禁保护。基线与 HEAD 无共同祖先,或 Jenkins shallow checkout 无法取得相关 commit 时,仍然失败而不是猜测基线。
受控 QA canary
公共库只对已登记的 canaryId 开放临时 QA 验证;业务仓库不能指定任意分支、阈值或测试目录。当前唯一登记项是:
canaryId: 'positive-order-service-qa'它仅在有效目标分支为 qa 时启用。有效目标分支统一按 CHANGE_TARGET → BRANCH_NAME → GIT_BRANCH 解析:因此目标为 QA 的 PR 与 QA 分支直接构建均可验证,普通项目的 QA 构建仍不会启用。公共库会将目标分支传给内部 Shell/Node 脚本,QA 与 test 的 V2 覆盖率基线严格隔离。
Jenkins 全局配置
在 Manage Jenkins → System → Global Trusted Pipeline Libraries 新增:
| 字段 | 建议值 |
| --- | --- |
| Name | node-quality-gate |
| Retrieval method | Modern SCM |
| SCM | Git:https://git.8848top.com/QATeam/jenkins-node-quality-gate.git |
| Credentials | Jenkins 中配置的 Gitea 只读凭据 |
| Default version | 发布后的不可变 tag,例如 v1.0.0 |
| Load implicitly | 关闭 |
| Allow default version to be overridden | 正式环境关闭 |
| Include library changes | 开启 |
该库需要设为 Trusted:它会读取 Jenkins 历史构建变量以计算覆盖率趋势,并通过 libraryResource 物化公共脚本。不要在 Jenkins 后台复制粘贴业务质量脚本。
业务项目接入
业务 Jenkinsfile 顶部显式加载库:
@Library('node-quality-gate') _在 Jenkinsfile 中声明项目事实配置。不要提供分支、阈值、小改动阈值或测试目录:
def qualityPolicy = [
sourcePaths: ['src/pos'],
coverageExcludePatterns: [
'src/pos/vendors/**', // 不采集 coverage,也不计入服务整体覆盖率基线
],
diffCoverageExcludePatterns: [
'src/pos/api/app.ts', // 仍采集并计入服务基线;变更时豁免单测差异与增量覆盖率门禁
],
testRunner: 'jest', // 或 'vitest'
testCommand: 'pnpm exec jest',
nodeToolName: 'v20', // 可省略,默认 v20;必须已在 Jenkins 配置
installCommand: 'pnpm install --prefer-offline --ignore-scripts',
buildCommand: 'pnpm run build',
]保留下面四个 Stage,便于在 Jenkins Stage View 定位失败点:
stage('V2 小改动判定') {
when { expression { nodeQualityGate.isEnabled(qualityPolicy) } }
steps { script { nodeQualityGate.evaluateSmallChange(qualityPolicy) } }
}
stage('V2 依赖安装与构建') {
when { expression { nodeQualityGate.shouldRun(qualityPolicy) } }
steps { script { nodeQualityGate.installAndBuild(qualityPolicy) } }
}
stage('单测差异检查') {
when { expression { nodeQualityGate.shouldRun(qualityPolicy) } }
steps { script { nodeQualityGate.requireTestDiff(qualityPolicy) } }
}
stage('单元测试与覆盖率') {
when { expression { nodeQualityGate.shouldRun(qualityPolicy) } }
steps { script { nodeQualityGate.runTestsAndCoverage(qualityPolicy) } }
}Docker Build/Push/Deploy、项目专有通知(例如飞书)与其他发布逻辑继续留在业务仓库。
配置字段
| 字段 | 必填 | 说明 |
| --- | --- | --- |
| sourcePaths | 是 | 非空、仓库相对源码目录列表;不可重复或互为父子目录 |
| coverageExcludePatterns | 否 | sourcePaths 内的正向仓库相对 glob 列表;从 runner coverage 采集和服务整体覆盖率基线排除,仅用于确实不应承担覆盖率责任的代码 |
| diffCoverageExcludePatterns | 否 | sourcePaths 内的正向仓库相对 glob 列表;仅豁免命中文件变更的单测差异与增量覆盖率门禁,仍采集且计入服务整体覆盖率基线;仅用于经评审确认的生成代码或不可测边界适配层 |
| testRunner | 是 | jest 或 vitest |
| testCommand | 是 | 测试 runner 基础命令;不得自行包含 coverage、reporter、output 参数 |
| nodeToolName | 否 | Jenkins NodeJS Tool 名称;默认 v20 |
| installCommand | 是 | 业务项目安装依赖命令 |
| buildCommand | 是 | 业务项目构建或类型检查命令 |
| canaryId | 否 | 仅可使用公共库登记的受控 canary 标识;当前仅 positive-order-service-qa |
Jest 与 Vitest 产物约定
公共库负责追加 CI、JSON 测试报告、coverage、完整源码采集、临时结果目录和排除项。coverageExcludePatterns 会传给 Jest/Vitest 并改变 coverage-final.json 与服务整体覆盖率口径;diffCoverageExcludePatterns 不会传给 runner,只影响变更文件的单测差异与增量覆盖率门禁范围。
- Jest:必须能产出
<temporary coverage directory>/coverage-final.json。 - Vitest:项目必须安装与当前 Vitest 版本匹配的 coverage provider(
@vitest/coverage-v8或@vitest/coverage-istanbul);公共库请求 JSON coverage reporter,并要求最终产物为 Istanbul 结构的coverage-final.json。 - 任一 runner 未生成测试 JSON 或
coverage-final.json都会失败;公共库不会在 CI 临时安装 runner/provider。 - 项目测试配置必须能扫描
tests/unit。若现有项目使用别的目录,应先迁移测试目录后接入。
Vitest provider 与版本可能影响 JSON 文件名和 coverage 映射;接入每类前端项目时,先在 canary Job 验证所用版本能生成公共库要求的 coverage-final.json。
覆盖率趋势(V2)
每个成功构建保存:
NODE_SERVICE_COVERAGE_BASELINE_V2=schema|scope|test|average|commitscope 基于排序后的 sourcePaths、coverageExcludePatterns 和固定后缀生成;diffCoverageExcludePatterns 不影响服务整体覆盖率口径,因此不参与 scope。仅当 schema、scope 和分支一致时才比较趋势。旧项目的 V1 基线不参与比较;首次 V2 成功构建显示 📊—。
公共库回填以下变量,供业务项目既有通知逻辑消费:
UNIT_TEST_SUMMARYDIFF_COVERAGE_SUMMARYDIFF_COVERAGE_FAILURE_REASONS:覆盖率门禁失败原因的单行 JSON 数组DIFF_COVERAGE_FILES:实际参与本次增量覆盖率统计的文件单行 JSON 数组DIFF_COVERAGE_UNCOLLECTED_CHANGED_FILES:本次变更但未进入 coverage 采集的文件单行 JSON 数组COVERAGE_BASELINE_SUMMARYGIT_DIFF_BASELINE_SHA:本次实际使用的完整上一次成功构建 commit SHAGIT_DIFF_BASELINE_SOURCE:固定为GIT_PREVIOUS_SUCCESSFUL_COMMITGIT_DIFF_BASELINE_SUMMARY:供通知展示的摘要,例如0fee923b(上一次成功构建)NODE_SERVICE_COVERAGE_BASELINE_V2
当 DIFF_COVERAGE_UNCOLLECTED_CHANGED_FILES 非空时,表示相应变更源码文件在本次生成的 coverage-final.json 中没有匹配记录。这可能来自覆盖率采集范围、忽略规则、路径匹配、runner/provider 配置或测试未加载该模块;该清单不能单独证明缺少单元测试文件。命中 diffCoverageExcludePatterns 的变更文件不会出现在该清单或 DIFF_COVERAGE_FILES 中,因此这两个字段不代表所有源码变更。
本地验证
该仓库不依赖额外 npm 包,使用 Node 内建断言和临时 Git fixture:
node test/quality-gate.test.js
bash -n resources/quality-gate/check-unit-test-diff.sh
node --check resources/quality-gate/check-diff-coverage.js
node --check resources/quality-gate/quality-result-utils.js覆盖的场景包括:覆盖率阈值、参与增量覆盖率统计的文件与未采集变更文件诊断、差异排除文件的增量门禁与单测差异豁免、差异排除不影响服务基线和小改动判定、Istanbul 行覆盖与 statement 覆盖差异、无可执行变更、Jest/Vitest(含 todo)摘要、覆盖率失败结果的字段解析、上次成功构建基线、小改动、全量新增文件取消豁免、测试重命名携带内容修改、缺失测试变更、边界超限和仅空白变更。
发布与回滚建议
- 先发布不可变候选 tag(例如
v1.0.0-rc.1)。 - 选择一个 Jest 后端项目和一个 Vitest 前端项目,在
testcanary/影子 Job 与旧门禁并行比对。 - 确认差异范围、小改动判定、测试结果和增量覆盖率一致;服务整体覆盖率因口径升级应只建立 V2 新基线,不与旧值直接比较。
- 发布不可变正式 tag(例如
v1.0.0),再将 Jenkins 默认版本切换到该 tag。 - 异常时在 Jenkins 后台回退到上一个稳定 tag;不要覆盖或移动已发布 tag。
