news 2026/9/23 3:22:07

Handsontable 的 Changelog 条目管理机制:从 `.changelogs` 到 `CHANGELOG.md` 的自动化工作流

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Handsontable 的 Changelog 条目管理机制:从 `.changelogs` 到 `CHANGELOG.md` 的自动化工作流
  • 前端
  • UI组件

【免费下载链接】handsontable

JavaScript Data Grid / Data Table with a Spreadsheet Look & Feel. Works with React, Angular, and Vue. Supported by the Handsontable team ⚡

项目地址:https://gitcode.com/gh_mirrors/ha/handsontable
点击查看免费下载

Handsontable 是一个用 JavaScript 编写的电子表格风格数据网格组件,仓库中同时维护着handsontable核心包与 React、Vue、Angular 三大框架封装(wrappers/)。在这样一个多包、多 PR 并行开发的仓库里,数百个开发者同时修改同一个CHANGELOG.md极易产生合并冲突。为此,Handsontable 采用了一套「临时 JSON 条目 + 脚本编译 + CI 门禁」的 changelog 管理机制:每个 PR 先在 .changelogs/ 目录下生成一个独立的.json条目文件,发布前再由 bin/changelog 统一编译进根目录的CHANGELOG.md。读完本文,你将掌握这套机制的完整规则、条目 JSON 的六字段格式、bin/changelogentry/consume/sync三个命令的用法,以及 GitHub Actions 如何用两道检查强制「一个 PR 只能有一个条目」。

为什么需要.changelogs目录

CHANGELOG.md是仓库根目录下的一份单一文件,任何版本迭代都要往里追加内容。当大量 PR 并行推进时,多个 PR 同时编辑同一份文件,Git 几乎必然报出合并冲突,解决起来既费时又容易出错。

Handsontable 的解法是:把「编写」与「合并」两件事分离。PR 作者不再直接修改CHANGELOG.md,而是在 .changelogs/ 目录下新建一个简单的.json文件作为临时条目。每个条目文件对应一个独立的 GitHub 编号(issue 或 PR 号),文件之间互不重叠,从根源上避免了并发写同一文件的冲突。等到正式发布新版本时,再由脚本统一把目录里的所有条目编译进CHANGELOG.md并清空目录。

强制的 PR 检查(Changelog Gate)

何时必须提交 changelog 条目

条目并不是可选的。仓库通过一个 GitHub Actions 工作流(定义在 .github/workflows/checks.yml 的changelogjob 中)强制执行如下规则:

当 PR 修改了可发布源码(shippable source)——即handsontable/src/**wrappers/**下的任何内容(测试文件与 Markdown 除外)——时,必须新增一个.changelogs/*.json文件,否则检查失败。

以下类型的 PR自动通过,既不需要添加条目,也不需要任何豁免声明:

  • 仅修改文档(docs/
  • 仅修改测试
  • 仅修改 CI 与工具链配置

此外,如果推送的提交没有关联 PR(例如直接推送到分支),检查会被整体跳过。

[skip changelog]:源码变更的豁免标记

对于「改了源码但确实没有用户可见影响」的少数场景(例如纯内部重构),可以在PR 描述(PR description)中、任何 HTML 注释之外写入以下字符串来豁免:

[skip changelog]

写入后,需要从 PR 的 checks 标签页重新运行失败的 Changelog 检查(或者推送任意一个新提交,git commit --allow-empty也可以)。原因是检查脚本在运行时实时读取 PR 描述——仅编辑描述本身不会重新触发检查,这一点在 .github/scripts/check-changelog.js 的源码中体现得很明确:脚本通过 GitHub API 拉取实时的PR body,而不是使用事件负载里冻结的旧值,这样作者补充标记后重跑即可让检查通过。

豁免并非无声无息:[skip changelog]生效时,检查日志会把被「放行」的源码文件逐一列出,供评审者判断这个豁免是否合理。

一个 PR 只有一个条目

核心规则

一个 PR 添加一个条目。只有引用了不同的 GitHub 编号时,才允许第二个条目。

这条规则的含义是:

  • 即使一个 PR 修复了多个 issue、涉及多个包、包含多个不同的用户可见变更,仍然只写一个条目,用一个标题概括整体变更;
  • 如果一条标题实在无法承载 PR 的全部内容,正确的做法是拆分 PR,而不是拆分条目——否则同一个变更会以两行出现在同一版本的 release notes 里,读者无法分辨它们来自同一个变更;
  • 不同的type不能作为添加第二个文件的理由,不同的framework也不能。一个同时修改了 API 的修复仍然是一个变更,把两者写进同一条标题,让最关键的type决定它落在哪个 section;一个横跨 React、Vue、Angular 三个封装的修复也只是一个变更,用framework: none提交一次即可。

唯一常规的第二个条目场景是:PR 在完成自身变更的同时,顺带关闭了一个公开的 GitHub issue。此时写两个文件——一个引用 issue 编号,一个引用 PR 编号——每个文件引用各自的编号,因此每一行描述的仍是不同的事情。

由什么强制实施

两条检查都是阻塞性的(blocking),且都不依赖评审者的人工把关:

检查位置断言内容
条目文件名bin/changelogconsumesync命令)以及 pre-push 钩子每个.changelogs/*.json都以<issueOrPR>.json命名——纯数字,且与条目引用的编号一致
条目数量.github/scripts/check-changelog.js一个 PR 最多添加两个条目文件

其中文件名检查是承重墙(load-bearing):一个编号只拥有一个文件,因此磁盘上不可能同时存在引用同一编号的两个条目——这正是让13442-changed.json13442.json并存变得「不可能而非仅仅不被鼓励」的机制。它在每个 PR 上都会通过checks.ymlchangelogjob 中的consume --date 2050-01-01 --dry-run步骤运行,并且不需要 diff,所以通过重命名文件也绕不过去。

从源码看,这条不变量实现在 bin/lib/entry-filenames.js 中:ENTRY_BASENAME_PATTERN = /^\d+$/(锚定、纯数字,13442-changed13442 (copy)entry都会被拒绝)。值得注意的是,文件名比较是字符串比较而非数值比较——Number('013442')会等于13442,若用数值比较就会把013442.json当作 #13442 的第二个文件放行,恰好制造出本机制要阻止的冲突。

文件名断言在consumesync中都是**fail-closed(默认拒绝)**的:过期的或命名错误的文件没有任何绕过途径,发布编译必须停下来,直到文件被重命名、合并、删除或修正。这意味着develop分支上一个游离的错误文件可能连累无关 PR 失败、甚至阻止一次发版——但换来的保证是:无效的 release notes 永远不会被悄悄发布。命令会列出每一个违规文件,并打印安全的补救方案(可直接重命名者给出git mv命令;因编号冲突而不可重命名者给出合并标题 +git rm的建议)。

由于bin/changelog entry写出的文件名永远是<编号>.json、不可能写成别的,保持两条检查都通过的最简单方式,就是使用它而不是手写 JSON

[multiple changelogs]:超过两个条目的唯一途径

只有一种 PR 需要超过两个条目:维护性 PR 为其他 PR 回溯补填条目(back-filling)。此时在 PR 描述中(HTML 注释之外)写入:

[multiple changelogs]

然后重新运行失败的 Changelog 检查。这个标记与[skip changelog]刻意分开的两个标记:后者回答「这个变更到底需不需要条目」,它永远不解除条目数量上限。

提交前的自检命令

在提交条目之前,文档建议先执行以下命令核对:

git fetch origin develop git diff --name-only --diff-filter=A origin/develop...HEAD -- '.changelogs/*.json'

输出超过两个路径即为错误;两个路径引用同一编号的情况不可能出现。如果 PR 目标分支是某个 release 分支,把develop换成该分支名即可。

条目格式:六字段 JSON

.changelogs/目录下的每个.json文件都只含一条条目,包含六个必填字段:

{ "issuesOrigin": "private", "title": "Fixed the cell editor closing unexpectedly on scroll.", "type": "fixed", "issueOrPR": 12345, "breaking": false, "framework": "none" }
字段可接受值含义
issuesOriginprivatepublicissueOrPR是否为公开 GitHub issue 编号,详见下文
title非空字符串对变更的用户可见描述,以句号结尾
typeaddedchangeddeprecatedremovedfixedsecurity条目落在CHANGELOG.md的哪个 section
issueOrPR数字引用的 GitHub 编号,同时必须是文件名——严格为<issueOrPR>.json,不允许任何后缀变体(由前面的一 PR 一条目规则断言)
breaking布尔值破坏性变更会在所属 section 内排在最前
frameworknonereactvueangular给条目加上框架名前缀;none表示核心包

这些字段的取值在 bin/changelog 源码中与常量数组一一对应:changelogEntryTypes = ['added', 'changed', 'deprecated', 'removed', 'fixed', 'security']changelogFrameworkTypes = ['none', 'react', 'vue', 'angular']changelogIssuesOriginTypes = ['private', 'public'],且assertChangelogEntryFormat会对每个字段做类型与取值校验(例如title必须是非空字符串、issueOrPR必须是非 NaN 数字、breaking必须是布尔值),校验失败会抛出错误,无法生成条目。

issuesOrigin字段详解

这个字段只回答一个问题:**issueOrPR里的编号是不是一个公开的 GitHub issue?**它与 PR 无关,也与仓库是否公开无关。

它决定生成CHANGELOG.md时采用的链接路径,而bin/changelog又依据issueOrPR命名文件,因此两个字段是联动的:

issuesOriginissueOrPR文件名渲染出的链接
private(默认,几乎总是正确的)拉取请求编号<PR-number>.jsonhttps://github.com/handsontable/handsontable/pull/<n>
public(少见)公开 GitHubissue编号<issue-number>.jsonhttps://github.com/handsontable/handsontable/issues/<n>

在私有 ClickUp 任务中跟踪的工作一律属于private——这覆盖了所有带DEV-xxxID 的变更。只有当条目引用的是一个真实的公开 GitHub issue 编号时才用public

值得注意的细节:即使issuesOrigin填错,也不会破坏已发布输出——因为 GitHub 会把/issues/<n>重定向到/pull/<n>。但错误值会让 release notes 发布错误的链接路径,同时让这个字段失去信息量。因此,交互式创建条目时,bin/changelog entry会检测「选了public但编号实际对应一个 PR」的情况并发出警告(见 bin/changelog 中的warnWhenPublicNumberIsPullRequest)。该警告需要网络访问,离线时会被静默跳过,而且它无法检查手写的条目文件。

使用bin/changelog辅助脚本

仓库提供了统一的 changelog 辅助脚本,用于创建条目并把它们编译进最终的CHANGELOG.md。查看命令列表与选项:

bin/changelog bin/changelog <command> --help

所有命令除了交互式问答外,都接受命令行参数。具体参数见各命令的--help

仓库的 package.json 中注册了快捷方式"changelog": "bin/changelog",因此也可以写npm run changelog(CI 报错信息中提示的也是npm run changelog entry)。

脚本内部通过yargs定义了三个子命令(entryconsumesync),每个都支持--dry-run等选项。

entry:添加新条目

bin/changelog entry

该命令会通过交互式问答收集issuesOrigintitleissueOrPRbreakingtypeframework六个字段,校验后自动以<编号>.json为文件名在.changelogs/目录下创建文件。它始终issueOrPR命名文件,不可能产生其他命名。创建前会预览该条目编译成 Markdown 后的样子,例如:

### Fixed - Fixed the cell editor closing unexpectedly on scroll. [#12345](https://github.com/handsontable/handsontable/pull/12345)

type的交互默认值会尝试从标题中猜测:例如标题含 "Fixed" 默认选fixed,含 "Added" 默认选added,否则默认changed

命令还内置了「防覆盖保护」:如果<编号>.json已存在(说明该编号已有条目),会明确提示「一个 PR 只能有一个条目」,并拒绝在无终端(非 TTY)环境下覆盖既有条目——因为静默覆盖会丢失已有标题,这正是这套检查要防止的损失。所有字段也可以通过命令行参数直接传入(如--type fixed --issue 12345 --breaking --framework react),非交互环境(CI 或 Agent)下会直接采用参数值。

你不需要为了条目有效而修改CHANGELOG.md——编译时会统一处理。

consume:编译条目

发布新版本时,需要把.changelogs/*.json编译成人类可读的CHANGELOG.md

bin/changelog consume

该命令会「消费」所有 changelog 条目:断言它们全部有效(字段校验 + 文件名断言 + 重复发布断言)、按类型格式化分组,并把结果插入CHANGELOG.md<!-- UNVERSIONED -->标记之后(见 CHANGELOG.md 第 10 行),然后删除所有.changelogs/*.json文件。版本号与发布日期取自 hot.config.js 的HOT_VERSION(当前为18.1.0)与HOT_RELEASE_DATE,也可用--date覆盖。

编译输出按type分组为### Added### Changed### Deprecated### Removed### Fixed### Security六个 section(顺序即上文changelogEntryTypes数组顺序),每组内破坏性变更(breaking: true)排最前,再按 framework 排序;条目渲染为- [**Breaking change**: ] [React|Vue|Angular: ]标题 #编号的形式。

consume副作用安全的——它不会修改本地仓库副本之外的任何东西;想撤销只需用git checkout恢复.changelogsCHANGELOG.md的旧版本(命令完成后终端会打印这行撤销命令)。建议发布前先试跑:

bin/changelog consume --dry-run

--dry-run只校验与预览、不写入不删除。consume支持--date YYYY-MM-DD指定发布日期,非交互环境下直接使用该值或hot.config.js中的默认值。

sync:同步条目到既有版本段

sync [version]命令用于把.changelogs/*.json条目合并进CHANGELOG.md已存在的某个版本 section(默认取CHANGELOG.md中第一个## [版本号]标题对应的版本)。它会先解析目标 section 已有的类型分桶,跳过其中已发布的条目,再把新条目按type合并进对应桶并重新序列化整个 section,随后同样删除已消费的.json文件。sync同样支持--dry-run,并在写入前执行文件名断言与重复发布断言——因为 release 分支由维护者直接管理,cherry-pick 到分支上的条目可能从未经过checks.ymlsync必须在本地兜底。

发布到文档站的 changelog 页面

consumesync写入的是根目录的CHANGELOG.md,而 release 工作流会把当前版本的 section 复制到 docs/content/guides/upgrade-and-migration/changelog/changelog.md。注意:没有任何脚本写入按大版本划分的页面docs/content/guides/upgrade-and-migration/changelog-<N>/changelog-<N>.md(仓库中现存changelog-6changelog-18等目录)——这个页面由人工把对应版本 section 复制过去,并把###降级为####。文档站与版本对比 UI(version-comparison)读取的正是这个按大版本划分的页面。

这条人工步骤拥有一条编辑规则,也是本节存在的意义:每条Added条目中提及的新选项(option)、钩子(hook)、方法(method)与插件(plugin),都必须链接到其 API 参考页面。这一实践从 10.0.0 延续到 14.2.0,却从未被写成文档,并在 14.3.0 时因负责人离开而中断(DEV-2790)。

链接采用方括号形式,锚点为小写:

- Added an Enter key handler and a new `searchMode` option to the `Filters` plugin. [#11871](https://github.com/handsontable/handsontable/pull/11871)
命名的东西链接目标
配置选项@/api/options.md#<选项名全小写>
钩子@/api/hooks.md#<钩子名全小写>
核心方法@/api/core.md#<方法名全小写>
插件或插件方法@/api/<插件名>.md@/api/<插件名>.md#<方法名全小写>

锚点就是纯小写的成员名:参考页会把成员名原样渲染为标题,所以#minRowHeights永远解析失败,而#minrowheights可以。没有任何参考页的东西不要加链接:主题令牌(theme tokens)与 CSS 类名、TypeScript 类型名、Intl.NumberFormat这类外部 API、非 API 成员的对象键、封装包名——链接到一个不文档化该名称的页面比不加链接更糟。

npm run docs:validate-changelog-links --prefix docs命令会列出候选者并标记无法解析的@/api/文件或锚点链接。该检查只报告、不阻塞,在每个文档 PR 上都会运行,且其候选集合是启发式的:一个恰好与选项名同名的反引号单词并不证明该条目引入了那个选项,需要人工判断每条发现。它能识别选项、钩子、插件类与Core成员,但不能识别插件方法——例如裸的collapseAll()同时属于CollapsibleColumnsNestedRows两个插件,只有句子上下文才能说明是哪个,这类链接需要人工处理。

为什么链接不能放进条目title

bin/changelog会把title原样渲染到四个目的地:根CHANGELOG.md、GitHub release body、文档 changelog 页面、版本对比 UI。其中只有文档页面能解析@/api/链接;在 GitHub 上同样的文本会把@/api/...渲染成指向字面路径的链接并 404,版本对比 UI 则会丢弃链接只保留文本。因此,需要文档链接的条目标题,应使用绝对 URL(https://handsontable.com/docs/...),链接解析留到文档页面的渲染阶段。

防止条目重复发布:No entry may be published twice

consumesync都会把待处理的条目与CHANGELOG.md已经发布的内容做比对,匹配到确凿结论时拒绝编译,匹配不确凿时仅警告。没有这道检查,同一个变更可能会在连续两个版本里被宣布两次——这种情况在 18.1.0 之前的大多数版本中发生过。

问题根源:两种 merge 形状

待处理的.json文件可能在 release 分支消费完条目后、release 合并回develop时存活下来,产生两种不易察觉的 merge 形态:

  1. 变更在 release 分支和develop上各提交一次,两个提交之间没有祖先关系。相对于 merge base,release 一侧显示为「先增后删」,因此develop上的新增成为两侧唯一的变更,merge 会静默保留它,不产生任何冲突;
  2. .json文件在develop上被消费后又被人编辑过,形成 modify/delete 冲突,很容易以错误的方式解决。

与其试图识别这两种形态,bin/lib/published-entries.js 直接断言两者共同的产物:一个CHANGELOG.md已经携带的待处理条目。匹配基于两个键,且严重性不同:

匹配结果
issueOrPR已在CHANGELOG.md中被引用,且issuesOriginprivate失败(error)
issueOrPR已被引用,issuesOriginpublic,且标题也匹配失败(error)
issueOrPR已被引用,issuesOriginpublic,但标题不匹配警告(warning)
仅标题已发布(任何编号下)警告(warning)

推理如下:一个 PR 编号不可能发布两次,所以private编号命中即为确凿;一个公开 issue 编号可能被两个版本合法地引用(先部分修复、后完整修复),但部分修复会得到新标题,所以「public编号命中且标题也匹配」不属于这种情况,按private命中同样失败;而一个标题(例如被 backport 到多个 release 线)可能合法地出现在两个 section 中,因此仅标题命中始终只是警告。两个键缺一不可:编号键会漏掉以错误链接发布的情况(如 #12727 曾以[#0]发布),标题键会漏掉在某个分支上被改写标题的情况(如 #13243)。

sync只针对一个版本 section,且已跳过其中存在的条目,因此检查对象是所有其他section。由于consume --dry-run在每个 PR 上都会运行(见 .github/workflows/checks.yml),这条规则在 PR 阶段与发布阶段都被强制执行。失败时会逐一列出违规文件,并打印清除它们的git rm命令。

解决方式:删除命令点名的条目文件(其变更已经发布);若其中确有真正的新变更只是复用了已发布的 PR 编号,则把条目改编号以引用它自己的 PR。没有跳过标记——已经发布的条目没有剩余内容可宣布。

release 合并回 develop 之后、推送之前,建议先运行:

bin/changelog consume --date 2050-01-01 --dry-run

2050-01-01是 CI 中使用的固定未来日期,确保校验不依赖真实发布日;--dry-run让它只校验不写入。)

总结:一套自洽的 changelog 工作流

Handsontable 的 changelog 机制可以概括为四个环环相扣的层次:

  1. 编写层:PR 作者用bin/changelog entry生成六字段 JSON 条目(或npm run changelog entry),文件名恒等于引用的 GitHub 编号;
  2. 门禁层:GitHub Actions 在 .github/workflows/checks.yml 中通过check-changelog脚本强制「改源码必须有条目、一 PR 至多两条目」,通过consume --dry-run强制「文件名必须等于引用编号」这一仓库级不变量,并提供[skip changelog][multiple changelogs]两个互不替代的 PR 描述标记作为受控豁免;
  3. 编译层:发布时bin/changelog consume(或面向既有版本段的sync)断言全部有效后,把条目按类型分组、破坏性变更置顶、framework 加前缀,插入<!-- UNVERSIONED -->标记处并清空目录;bin/lib/entry-filenames.js 与 bin/lib/published-entries.js 提供底层的文件名与重复发布检测逻辑;
  4. 分发层consume/sync写根CHANGELOG.md,release 工作流把当前版本 section 复制到 docs/content/guides/upgrade-and-migration/changelog/changelog.md,人工降级标题后落到changelog-<N>/按大版本页面,由docs:validate-changelog-links校验其中的@/api/参考链接。

这套机制把「合并冲突」问题转化为「文件系统不变量」问题,把「评审者把关」转化为「CI 断言」,既保证了 release notes 的质量底线,也让每个 PR 作者有了清晰、可自动化的操作路径。

  • 前端
  • UI组件

【免费下载链接】handsontable

JavaScript Data Grid / Data Table with a Spreadsheet Look & Feel. Works with React, Angular, and Vue. Supported by the Handsontable team ⚡

项目地址:https://gitcode.com/gh_mirrors/ha/handsontable
点击查看免费下载

相关推荐

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 3:17:06

Flutter测试迁移鸿蒙:test_process进程适配与CLI集成测试实践

我手头这个 Flutter 项目本来跑得好好的&#xff0c;CI 上一套集成测试每天都稳定执行。第一次把整套验证迁移到鸿蒙开发板上时&#xff0c;测试零零散散挂了一大片。我看日志还以为是打包脚本的问题&#xff0c;点进去才发现错误清一色集中在dart:io的进程相关调用上&#xff…

作者头像 李华
网站建设 2026/9/23 3:13:20

毛发检测仪核心技术解析与临床应用

1. 项目背景与产品定位在皮肤科与毛发医学领域&#xff0c;精准诊断一直是临床工作的核心难点。传统毛发检测主要依赖肉眼观察和普通光学设备&#xff0c;存在放大倍数有限、图像清晰度不足、数据难以量化等问题。这次在毛发专病医联体年会上亮相的"发觉星毛发拍摄仪"…

作者头像 李华