OpenMetadata UI 代码质量门禁:从“新增代码必须干净”到 SonarCloud Clean-as-You-Code 的完整落地实践
2026/9/14 5:20:32 网站建设 项目流程

OpenMetadata UI 代码质量门禁:从“新增代码必须干净”到 SonarCloud Clean-as-You-Code 的完整落地实践

【免费下载链接】OpenMetadataThe Open Context Layer for Data and AI , OpenMetadata is the open platform for building trusted data context and business semantics for humans, AI assistants, and agents.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMetadata

本文讲解 OpenMetadata 如何在前端(openmetadata-ui)的每一个 PR 上强制执行代码质量,以及你还需要在 SonarCloud 和分支保护中完成哪些一次性配置。读完本文,你将掌握一套“生成时 + 评审时”双层质量门禁的完整方案:本地一条make ui-checkstyle-changed命令如何与 CI 完全对齐、每条门禁的作用范围与失败条件如何划定、以及如何用 SonarCloud 的 Clean-as-You-Code 模型只对 PR 新增行做卡口,使历史债务永远不会阻塞合并。

一、治理原则:新代码必须干净,存量债务渐进偿还

整套门禁的第一性原理是:新增代码必须干净;存量债务逐步偿还。下面列出的每一条门禁都只作用于变更新增的部分,而不是整个文件、更不是整个仓库——这样待办清单(backlog)永远不可能阻塞一个 PR。

这一原则直接决定了三个工程决策:

  • 检查只报告“diff 里出现了什么”,而不是“文件里有什么”;
  • 严重等级(error/warn)由实测存量决定,而非个人口味;
  • 门禁的落点是ui-checkstyleCI 作业,而不是 pre-commit 钩子。

原始设计文档见 docs/ui-code-quality-gate.md,本文在此基础上结合仓库源码逐层展开。

二、两侧结构:生成时与评审时各跑一遍

由于几乎全新的 UI 代码是 AI 生成的,只在 CI 中运行的检查来得太晚——模型在写代码时就已经选错了模式。因此每条规则都存在两份

生成时(Agent 正在写代码)评审时(CI)
知识.claude/rules/*.md,由paths:glob 自动加载
强制执行.claude/settings.json的 hooksui-checkstyle作业
自助运行make ui-checkstyle-changedrequired status check

仓库中这些规则文件确实存在,例如 .claude/rules/component-library.md、.claude/rules/frontend-a11y.md、.claude/rules/frontend-performance.md 等,由 IDE Agent 按文件 glob 自动加载。

一套工具链,三个调用点

同一份 ESLint 配置在三个地方运行:Agent hook、make ui-checkstyle-changedui-checkstyleCI 作业。设计上的偏好是能用现成的 ESLint 插件就不要写定制脚本:插件匹配的是 AST 而不是 diff 文本、能在编辑器里实时反馈、还能与--fixeslint-disable组合。只有当一条规则确实无法用 ESLint 表达时才写脚本——tw-guard(Tailwind/antd 迁移守卫)就是这种情况,因为它对应的 antd/.less存量(864 个和 449 个文件)使得“只查新增行”的作用域划定不可避免。其实现位于 scripts/tw-deprecation-guard.js 与 scripts/tw-audit.js。

规则背后的深度知识存放在skills/vendor/下的react-best-practicesweb-design-guidelinescomposition-patterns(自 vercel-labs 的 agent-skills 仓库以 MIT 许可原样引入,无需安装步骤)。Skill 只在被调用时才加载,因此把“承重”子集蒸馏进了.claude/rules/frontend-performance.mdfrontend-a11y.md,在匹配文件上自动加载。

Checkstyle 是强制执行点,pre-commit 不是

新门禁刻意不放进.pre-commit-config.yaml:那里的每一个钩子都会在每次 commit 时付出代价,而 commit 必须保持快速。ui-checkstyle是门禁唯一必须守住的地方,而make ui-checkstyle-changed让你在 push 之前就在本地拿到同样的答案。

三、本地运行:make ui-checkstyle-changed到底做了什么

make ui-checkstyle-changed # 与 CI 完全一致,只跑你改动的文件

这是唯一需要信任的命令——它既运行修复步骤(organize-imports、eslint、prettier、license 头、i18n 同步、app-docs 生成),又运行审计门禁tw-audittw-guard)。门禁是收集式的而非短路式的:一个失败不会掩盖其他失败。

底层脚本的实现细节

Makefile 目标定义见 Makefile:ui-checkstyle-changed依次在openmetadata-ui/src/main/resources/uiopenmetadata-ui-core-components两个前端工程中执行yarn ui-checkstyle:changed。而 package.json 中ui-checkstyle:changed指向 scripts/ui-checkstyle-changed.sh,该脚本的关键行为值得逐条说明:

  1. 基准解析(BASE):无条件先解析origin/main与 HEAD 的merge-base作为 diff 基准;若本地没有origin/main会尝试fetch --depth=1,再失败则回退到HEAD~1。之所以无条件解析,是因为后面的“禁止新增债务”守卫要对 BASE 做 diff,不能放进分支逻辑里。
  2. 变更文件集合git diff --name-only --diff-filter=ACM $BASE HEAD,过滤出openmetadata-ui/src/main/resources/ui/src/下新增/拷贝/修改(ACM)的.ts/.tsx/.js/.jsx/.json文件,并排除src/generated/src/jsons/两个生成物目录。也可以显式传文件覆盖自动探测。
  3. 修复步骤按类型分发organize-imports-cli只处理 TS/JS(不处理 JSON),随后对所有变更文件跑lint:base --fixpretty:base --writelicense-header-fix;最后无条件跑yarn i18n(locale 同步)与yarn generate:app-docs
  4. 审计门禁收集失败:脚本特意set +e,把tw-audit(只查变更的 TSX 文件)与tw-deprecation-guard.js(对 BASE diff)的失败收集进数组,最后统一报✖ ui-checkstyle failed: <列表>exit 1——这正对应文档所说“与 CI 的 continue-on-error 行为保持镜像”,避免set -e在第一个失败处就停住、掩盖其余问题。

CI 侧的输出位置

在 CI 中,warning 会出现在 PR 上标题为UI Checkstyle passed — lint findings in changed files的 sticky GitHub Actions 评论里,按规则分组列出变更文件的行号、列号与消息。同样的输出也可在Actions → UI Checkstyle → checkstyle → ESLint + Prettier + Organise Imports (src)步骤中找到。warning 保持非阻塞;ESLint error 与格式化差异仍会让ui-checkstyle失败。CI 作业定义见 .github/workflows/ui-checkstyle.yml。

四、每条门禁的作用范围与失败条件

门禁作用范围失败条件
ESLint + Prettier + organize-imports变更文件输出与提交形态不一致
Licence 头变更文件Apache-2.0 头缺失/过期
i18n key 同步全部 localelocale 文件与en-us.json不同步
tw-audit变更文件硬编码了可映射到设计 token 的 Tailwind 值
tw-guard新增行新的antdimport 或新的.less文件
jsx-a11y(ESLint)变更文件19 条零存量无障碍规则中任一触发
SonarJS(ESLint)变更文件16 条零存量正确性规则中任一触发
OpenMetadata 性能(ESLint)变更文件静态引入路由页、未加守卫的 lazy 组件、无界模块缓存
OpenMetadata import 架构(ESLint)变更文件架构、循环、barrel、请求扇出类 finding(仅 warning)
SonarCloud 质量门禁新代码本 PR 新增行上的复杂度、重复度或新问题

五、组件复用是指导,不是门禁

.claude/rules/component-library.md承载“应该 import 什么而不是手写”的对照表(例如用Select而不是<div role="listbox">)。没有任何 linter 了解这套设计系统,所以这张表是指导与人工评审,而不是自动检查。

历史上曾为这个目的专门写过reuse-audit脚本,后来被移除。它糟糕地重新实现了 ESLint 本就做得好的事:对原始 diff 行做正则匹配会在data-role=[role="menu"]选择器与注释上产生误报;其手写 git 处理逻辑还可能在 diff 加载失败时误报“干净”。它唯一真实的优势——只检查新增行——是为了容忍一个只有16处实例的存量,这个规模不足以justify约 430 行定制代码及其独立测试套件。

CI 转而强制的是:手搓组件至少必须是无障碍的jsx-a11y会拒绝无效的role、缺少必备aria-*属性的 role、以及不可用的 Tab 顺序。使用组件库组件是满足这些要求的最简单方式。

六、两级严重等级:由实测数据选定,而非口味

所有规则都是开启的。严重等级由规则的实测存量决定——因为 ESLint 是按文件而非按新增行报告的,一条带着既有违规的error规则会让任何只是触碰了这些文件的 PR 失败。

等级含义当前构成
error零实测存量——阻塞16 条 SonarJS + 19 条 jsx-a11y + 3 条 OpenMetadata 性能
warn有存量——在编辑器与 CI 输出中可见,不阻塞21 条 SonarJS、15 条 jsx-a11y、4 条 React、10 条 OpenMetadata import 规则、react-hooks/exhaustive-depsi18next/no-literal-string@typescript-eslint/no-non-null-assertion

以当前仓库为例:全仓0 error、10120 warning,分布在 2228 个文件。这些 warning就是被摆上台面的存量——目标是零,逐规则达成。

从 eslint.config.mjs 可以直接核对:16 条零存量 SonarJS 规则以error显式列出(如 no-identical-conditions),19 条零存量 jsx-a11y 规则同样显式以error配置(见 aria-props 至 scope);而高存量规则则显式降为 warn,例如sonarjs/cognitive-complexity: ['warn', 15](存量 85 处)与react-hooks/exhaustive-deps: 'warn'(596 文件 1693 处)——注释里保留了成本数字,方便下一个人判断提升代价。

一个值得注意的细节:i18next/no-literal-string曾带着TODO: re-enable when the plugin supports ESLint 9被禁用。该不兼容已无法复现——它现在运行正常并报告出大量存量,因此以warn恢复。仓库约定是不写用户可见的字符串字面量,所以它最终应该升到error

七、仓库专属性能规则:三条“只在归零后升 error”的规则

eslint-rules/openmetadata-performance.mjs 包含三条仅报告(reporting-only)的规则,测试套件由yarn test:eslint-rules运行。三条规则都是在全量src/扫描达到零命中之后才以error启用的:

  1. no-eager-page-imports:作用于src/components/AppRouter/**,拒绝路径包含pages/的运行时静态 import;type-only import 仍然合法。从源码实现看,规则通过ImportDeclaration节点判断/pages\//路径模式,并检查importKind === 'type'或全部 specifier 均为 type-only(见 openmetadata-performance.mjs),这正是“只拒绝运行时依赖”的实现保证。
  2. require-suspense-fallback:只识别从 React 导入的lazy/React.lazy。可接受的写法包括:组件直接传递给(或随后传递给)从components/AppRouter/withSuspenseFallback导入的批准 helper;或者一条从 lazy 绑定到 JSX 渲染/传递路径的局部变量依赖链,且该 JSX 位于带显式fallback属性的真实 ReactSuspense边界之下。模块里无关的其他边界不算数。
  3. no-unbounded-module-cache:检查模块级、带缓存风格名称的MapSet绑定。一个缓存需要显式的数值或大写命名的大小比较,且被守卫的if分支或while循环体必须用deleteclear同一个绑定做逐出。

这三条规则刻意不自动修复:引入 loading 边界、选择逐出策略、以及决定哪个路由依赖应保持静态引入,都需要运行时上下文才能判断。

八、Import 架构与请求扇出:十条 warn 级架构规则

eslint-rules/openmetadata-imports.mjs 包含十条仅报告的规则,均以warn启用(见 eslint.config.mjs 的 openmetadata-imports 配置块)、不自动修复,因此在其实测存量被消化之前不会导致 CI 失败:

规则报告内容基线 finding / 文件数
no-impure-pure-utils*PureUtils中出现 React/JSX 或向上的 UI、状态、页面、hook 或 REST 依赖62 / 23
no-lower-layer-page-importspages 与 AppRouter owner 之外的页面 import291 / 271
no-cross-page-imports一个页面特性静态 import 另一个页面特性43 / 32
no-rest-ui-importsREST client 依赖 components、pages、hooks、context 或 stores55 / 37
no-hook-ui-importshooks 依赖 components 或 pages10 / 6
no-circular-imports参与循环的运行时 import/re-export;type-only 忽略295 / 164
no-internal-barrel-imports解析到应用内部indexbarrel 的运行时 import;type-only 允许143 / 134
no-lodash-default-import从 Lodash 包根做 default 或 namespace import1 / 1
no-api-calls-in-iteration循环或map等动态迭代回调中的 REST 调用28 / 21
review-sequential-api-calls同一函数中第二个及以后的直接 await REST 调用,供依赖评审207 / 105

最后一条刻意表述为“review”(评审)而非错误:静态分析无法证明第二个请求是否依赖第一个。合法的顺序保持原样;独立请求并行化。晋升路径:把某条规则的存量清零、重新测量、再移到error;基线数字就写在 eslint.config.mjs 每条规则旁边,下一个人可以直接看到代价。请求评审规则在强制化之前需要单独重新评估。

刻意仍关闭的规则,及原因

  • react/jsx-no-useless-fragment——它会 autofix,所以任何严重等级下eslint --fix都会改写文件并使 git-diff 检查硬失败。正确做法是先落地一个一次性的全仓 autofix commit,再以error加入。
  • sonarjs/file-headerarrow-function-conventionshorthand-property-groupingelseif-without-else——纯风格,且与 Prettier 和仓库既有约定冲突。开启它们会制造成千上万没人打算修的 warning,贬低每一条其他 warning 的价值。
  • sonarjs/no-reference-errorno-implicit-dependencies——需要本配置未提供的 resolver/global 配置,缺了它几乎全是误报。

warn层加规则之前,先确认它是否会 autofix。ui-checkstyle会跑eslint --fix并对产生的 git diff 失败,所以warn级的 autofix 规则会静默改写文件并硬失败门禁。当前所有warn规则都是fixable: none或仅 suggestions。react-hooks/exhaustive-deps声明了fixable: 'code',但经实测验证在--fix下不会改写依赖数组——这一点双重重要,因为自动添加 effect 依赖会改变运行时行为。

九、ESLint 里的 SonarJS:Sonar 的“快速一半”

eslint-plugin-sonarjs与每个 UI PR 上已运行的 SonarCloud 分析是同一个引擎、同一套Sxxxx规则 id。编辑器里的 finding 就是 Sonar 会报告的 finding。

高存量 SonarJS 规则同时由 SonarCloud阻塞式强制执行,其 Clean-as-You-Code 模型把它们限定到新增行——这是 ESLint 从根本上无法表达的作用域。所以cognitive-complexityno-duplicate-string在本地是 warn,在 PR 门禁上则对新代码阻塞。

版本纪律同样严格:eslint-plugin-sonarjs精确固定4.2.0(见 package.json)。SonarCloud 在服务端按自己的节奏升级分析器,且这种漂移是无声的。

十、编辑器里的第三个位置:SonarQube for IDE

这是同一套规则出现的第三个位置,也是唯一逐字显示服务端profile 的位置,而不是本地近似:

  1. 安装SonarQube for IDE(前 SonarLint)——支持 VS Code、IntelliJ 等。
  2. Connected Mode下把 workspace 绑定到 SonarCloud,组织open-metadata,项目open-metadata-ui
  3. Connected Mode 会拉取项目的质量 profile,于是编辑器标出的正好是 PR 门禁会标出的内容——包括 ESLint 本地暂缓的高存量规则,且针对新代码标记。

不用 Connected Mode 时插件使用自己的默认值,会与 CI 不一致。要么绑定,要么依赖make ui-checkstyle-changed

十一、SonarCloud 配置(管理员一次性完成)

项目open-metadata-ui,组织open-metadata,由 .github/workflows/yarn-coverage.yml 扫描。

质量 Profile

创建一个自定义 profile,激活规则镜像 eslint.config.mjs 中启用的集合;并在两侧显式设置规则参数——不要指望两个默认值恰好一致(例如cognitive-complexity阈值两边都设为 15)。

质量门禁:条件全部只针对 New Code

门禁名:OpenMetadata UI — Clean as You Code,设为项目默认门禁。

条件(New Code)操作符
覆盖率小于90.0%
问题数大于0
已评审安全热点小于100%
重复行占比大于3.0%
任何针对 Overall Code 的条件禁止

**绝不要给 Overall Code 挂任何条件。**那会从第一天起被遗留债务击穿,破坏整个“只看新代码”契约。上表每条条件都只针对 PR 新增或修改的行评估。

新代码 90% 覆盖是这里最严的条件——高于 Sonar 默认的 80%,而 UI 目前完全没有覆盖率地板(jest.config.js 设置了collectCoverageFrom但没有coverageThreshold)。初期它会是失败 PR 最多的条件:任何新组件、hook 或 util 都需要测试落在同一个 PR 里。这就是本意——新代码被要求达到存量债务不必达到的标准——但它实质改变了 UI PR 的“完成”定义,团队应当在门禁开启前被告知,而不是从红勾里发现。

两个值得知道的机械后果:

  • 一个只移动或重排格式的 PR 仍可能把这些行登记为新的且未覆盖;
  • 覆盖率来自sonar.typescript.lcov.reportPathssrc/test/unit/coverage/lcov.info),所以若 Jest 运行失败或 lcov 缺失,新代码覆盖率会读成 0%,门禁失败。修测试运行,而不是修门禁。

New Code 定义

分支场景:参考分支 =mainmain自身:Previous version(或 30 天)。在项目设置里配置,不在门禁上。

分支保护

main上把以下三项标为 required:

Required check强制执行
ui-checkstylelint(含 SonarJS + jsx-a11y)、prettier、licence、i18n、tw-audittw-guard
ui-coverageJest 运行已完成
ui-sonar-gateClean-as-You-Code 质量门禁,含新代码 90% 覆盖

要标ui-sonar-gate,而不是 SonarCloud 自己的 check。从 workflow 源码可以确认这个机制:扫描被dorny/paths-filtersafe to test标签门控(见 ui-checkstyle.yml 与 yarn-coverage.yml),所以一个不含 UI 变更的 PR 永远不会产生那个 check,若直接依赖它会在分支保护下永远卡住;而ui-sonar-gate在扫描被合法跳过时会始终运行并通过。

门禁结果直接来自扫描器本身:PR 扫描传入-Dsonar.qualitygate.wait=true(超时 600s),SonarCloud 决策、扫描器失败时退出非零(见 yarn-coverage.yml 的 sonar 参数),ui-sonar-gate作业把这个结果翻译成贡献者看到的 check(其needs: [ui-coverage-tests]并读取sonar_gate_status输出)。这是受支持的机制——不要重新引入对/api/qualitygates/project_status的轮询,那会与异步报告处理竞态,并在超时时静默放行。

十二、两个需要预期的行为

**被修改的行算新代码。**编辑一个混乱遗留文件里的某一行,会把该行的问题拉进门禁作用域。这是渐进偿还债务的机制——你清理你碰到的东西——但读起来像是“门禁在我没写的代码上失败了”。事实并非如此:那行就在你的 diff 里。

**新代码归属需要首次确认。**PR 扫描传入-Dsonar.scm.disabled=true(push 扫描不传),新代码归属来自sonar.pullrequest.*参数,大概率没问题——但在第一个受门禁的 PR 上,检查 Sonar 的New Code标签页是否只显示 diff 而不是整文件。若显示整文件,就从 PR 扫描步骤里去掉那个 flag。

十三、存量追踪:门禁是故意对旧代码盲的

门禁按设计看不到旧代码,因此它永远不会告诉你债务是否在减少。每月在overall代码上跟踪sqale_indexcode_smellsduplicated_lines_density(SonarCloud 的 measures/search_history 度量查询 API,组件open-metadata-ui,指标含cognitive_complexity,duplicated_lines_density,code_smells,sqale_index,ncloc):

GET measures/search_history ?component=open-metadata-ui &metrics=cognitive_complexity,duplicated_lines_density,code_smells,sqale_index,ncloc

预期形态:在增长中的ncloc上保持平坦或下降。连续两个月上行是安排专项清理的信号——Clean as You Code 只在人们恰好编辑的地方偿还债务。

十四、小结:这套门禁可复用的关键决策

结合 docs/ui-code-quality-gate.md 与仓库实现,OpenMetadata 的 UI 质量门禁有几个可直接迁移的工程决策:

  1. 增量作用域优先于绝对清洁:所有门禁限定在“变更新增”,使 backlog 永不阻塞 PR;
  2. 严重等级是测量结果:error/warn 的划分由全仓扫描的实测存量决定,且成本数字写在配置注释里,晋升路径明确(清零 → 重测 → 升 error);
  3. 同一工具链多处调用:Agent hook、本地 make 目标、CI 作业共用一份 ESLint 配置,保证“本地通过 = CI 通过”;
  4. ESLint 与 Sonar 分工明确:ESLint 管能按文件表达的正确性与架构规则,SonarCloud Clean-as-You-Code 管只有“新增行”作用域才能成立的条件(复杂度、重复、90% 新代码覆盖);
  5. check 命名与门控解耦ui-sonar-gate这类“shim”check 把条件门控(paths-filter、标签)与分支保护的安全要求解耦,同时用qualitygate.wait取代 API 轮询,消除竞态与静默放行。

参考文件索引:docs/ui-code-quality-gate.md、Makefile、scripts/ui-checkstyle-changed.sh、package.json、eslint.config.mjs、eslint-rules/openmetadata-performance.mjs、eslint-rules/openmetadata-imports.mjs、.github/workflows/ui-checkstyle.yml、.github/workflows/yarn-coverage.yml。

【免费下载链接】OpenMetadataThe Open Context Layer for Data and AI , OpenMetadata is the open platform for building trusted data context and business semantics for humans, AI assistants, and agents.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMetadata

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询