JointJS 仓库 Changesets 变更集实践指南:格式规范、常用命令与自动发布链路
JointJS 仓库 Changesets 变更集实践指南:格式规范、常用命令与自动发布链路
导读
本文以 JointJS 仓库中 .changeset/README.md 为骨架,系统讲解该仓库基于 Changesets 工具的版本管理与变更集(changeset)工作流:包括变更集文件的完整格式(frontmatter、正文、作用域、描述规则)、yarn changeset 与 yarn changeset add --empty 等常用命令、CI 对缺失变更集的强制校验,以及从「Version Packages」PR 到 npm 发布的全自动链路。读完本文,你将能够在 JointJS 的 Yarn workspace 多包仓库中,为任何涉及可发布代码的 PR 正确编写变更集,让 CHANGELOG.md 与版本号自动演进。
一、什么是 Changesets,为什么 JointJS 用它
Changesets 是一套管理 monorepo 版本号与 changelog 的工具链。在 JointJS 中,它的作用链路是:
.changeset/目录中挂起的变更集(pending changesets)→ 驱动版本号提升、各包CHANGELOG.md生成与 npm 发布。
这套流程由 @changesets/cli,格式规则见 CONTRIBUTING.md 中的 Changeset format 小节。仓库根目录的 package.json 将 @changesets/cli(版本 3.0.0)声明为根 devDependency,而 .changeset/ 目录下目前包含三个文件:
README.md—— 本文讲解的格式速览;config.json—— Changesets 配置(linked 分组、变更文件匹配规则等);changelog.cjs—— 自定义 changelog 行生成逻辑(为每条记录追加短 commit SHA)。
值得注意的架构事实是:JointJS 采用 Yarn workspaces monorepo(见 package.json 的 workspaces 字段,包含 ./packages/* 与 ./examples/*),每个包维护独立的 CHANGELOG.md;绝大多数包独立发版,而 @joint/core、@joint/layout-directed-graph、@joint/layout-msagl 三者是 linked(联动) 的——若同一轮发布涉及其中两个或以上,它们会共享同一个版本号。这一点在 .changeset/config.json 的 linked 字段中亦有印证,实际链接组还包括 @joint/router-avoid。
二、创建变更集:命令与手写两种方式
为 PR 添加变更集有两种等价途径:
方式一:交互式命令
yarn changeset
执行后按提示选择受影响的包与 bump 类型(patch / minor / major),并编写变更摘要。生成的 .changeset/*.md 文件需要随 PR 一起提交(见 CONTRIBUTING.md)。
方式二:手写文件
直接在 .changeset/ 目录下新建一个 Markdown 文件,例如:
---
"@joint/core": minor
---
dia.Paper - add `originX` and `originY` options to `getFitToContentArea()`
这个示例并非虚构:dia.Paper - add originXandoriginYoptions togetFitToContentArea()` 这条记录实际出现在 packages/joint-core/CHANGELOG.md 的 4.3.0 Minor Changes 中,可对照参考其最终渲染形态。
空变更集(empty changeset)
当 PR 触及了可发布文件但不应触发发布时(例如纯测试改动被匹配规则之外的路径触碰、或需要占位说明的 PR),使用:
yarn changeset add --empty
空变更集标记该 PR 不产生发布动作。若无需任何变更集,则可用 yarn changeset add --empty 的判定边界依赖于 changedFilePatterns 配置(见下文第三节)。
三、变更集格式规范:Frontmatter + 正文
一个变更集本质上是一个带 YAML frontmatter 的 Markdown 文件,位于 .changeset/ 目录。frontmatter 声明哪些包随本次变更发布;正文则是将写入这些包 CHANGELOG.md 的条目。
3.1 Frontmatter:包名与 bump 类型
每个键是包名,每个值是 bump 类型(patch、minor、major),例如:
---
"@joint/core": minor
---
关键规则:单个变更集中列出多个包应当很少见。 因为正文会被原样复制到每个列出包的 changelog 中,只有当同一句话对所有这些包的读者都准确时才是正确的。若一个 commit 涉及多个包,应当为每个包单独写一个变更集,正文针对该包的用户分别措辞(见 CONTRIBUTING.md#changeset-format)。
3.2 正文:一行 = 一条 changelog bullet
正文必须保持单行——一个变更集恰好产出一条 changelog bullet。 只有正文的第一行会获得 - 标记;后续行会被缩进并渲染为同一条 bullet 的续行文本,而不是独立条目。因此:
- 不要在正文中自己写
-/*列表符号或 Markdown 标题; - 每需要一条额外的 changelog 行,就新建一个变更集文件。
这一规则在底层由自定义 changelog 生成器 .changeset/changelog.cjs 强制执行:getReleaseLine 将 summary 按 \n 拆分,首行输出为 - <首行> (<short SHA>),续行以两个空格缩进拼接;同时若变更集尚无 commit(如本地创建),则省略 SHA 后缀。该文件还实现了 getDependencyReleaseLine,用于生成 - Updated dependencies 块,按依赖列出 <dependency>@<newVersion>,并对 SHA 去重。
3.3 作用域(Scope):从类级到包级
正文遵循已有 CHANGELOG.md 的风格:作用域 - 描述(scope,空格-连字符-空格,再描述)。作用域的分级规则如下:
最常见:namespace.Class 类级作用域
dia.Paper - add `getCellView()` method for strict view lookup
mvc.View - add `classNamePrefix` instance property to override the `joint-` CSS class prefix
elementTools.Control - respect the `padding` option when computing the handle position
layout.DirectedGraph - add `rankSep` option
@joint/react 包:组件或 hook 作为作用域——组件写成 JSX 标签形式,hooks 直接用名字:
<Paper /> - fix the visual grid to redraw reactively when `drawGrid` changes
useCells - fix ghost cells reported after `resetCells()`
较少见:裸 namespace——当改动覆盖整个命名空间的所有内容、逐类列出显得冗余时使用:
anchors - add `rotate` option to all built-in anchors
connectionPoints - fix stroke-width handling on transformed elements
最罕见:完全无作用域——用于影响整个包的全局性或架构性改动,此时正文只有描述,没有作用域也没有前导的 -:
drop support for Internet Explorer 11
publish native ESM alongside the UMD bundle
唯一的固定例外:全新包的变更集使用字面量作用域 new package:
new package - idiomatic React components and hooks for JointJS, built directly on the core engine
3.4 描述(Description):约 100 字符内的动词开头
描述规范(见 CONTRIBUTING.md#changeset-format):
- 长度:约 100 字符以内;
- 大小写:小写字母开头;
- 结尾:不加句号;
- 开头动词:以动词起头(
add、fix、deprecate、remove、support),缺陷修复自然读作fix <what>或fix to <do what>; - 代码工件:描述中提到的代码标识符一律放进反引号,如
`changeId`,`batch:start`,`initializeUnmounted: true`,`drawGrid`; - 函数/方法名:永远带尾随括号
()。
| 推荐写法 | 不推荐写法 |
|---|---|
of `layout()` |
of the layout function |
of `layout()` |
of `layout()` function |
fix `toJSON()` to honor the option |
fix toJSON to honor the option |
四、CI 强制校验:没有变更集就挂 PR
变更集不是可选项,而是被 CI 强制执行的提交要求。根据 CONTRIBUTING.md:
CI 运行
changeset status --since=origin/master,若 PR 修改了公开包中的可发布代码而没有变更集,则 CI 失败。
其中「哪些文件算可发布代码」由 .changeset/config.json 的 changedFilePatterns 精确划定。该配置使用 glob 白名单加排除规则:
- 匹配
"**"(全部文件); - 排除
**/*.test.{js,jsx,ts,tsx,mjs}、**/test/**、**/__tests__/**、**/__mocks__/**、**/bench/**、**/demo/**、**/stories/**、**/.storybook/**、**/dist/**、**/build/**、**/coverage/**、**/coverage.json、**/.tscache/**、**/scripts/**、**/grunt/**、**/Gruntfile.js、**/Makefile、**/rollup.config.*、**/rollup.resources.mjs、**/vite.config.*、**/vitest.workspace.*、**/jest.config.js、**/jest.react18.config.mjs、**/karma.conf.js、**/tsconfig*.json、**/dts-generator.config.js、**/knip.json、**/typedoc.*、**/.prettierrc*、**/.gitignore、**/eslint.config.mjs、**/*.md、**/LICENSE。
也就是说:测试、文档、demo 与构建配置文件改动被豁免;而 .md、LICENSE、lint/构建配置等一律不计入「可发布代码」。当 PR 触及可发布文件但不应触发发布时,使用 yarn changeset add --empty 通过校验。同文件还配置了 baseBranch: "master"、access: "public"、changelog: "./changelog.cjs"(指向上述自定义生成器)、commit: false 以及 updateInternalDependencies: "patch" 等选项。
五、发布链路:从变更集到 npm 与 Git tag
发布完全由 .github/workflows/release.yml 自动化(每次 push 到 master 触发),分两个阶段:
- Version(版本):只要存在挂起的变更集,工作流就持续维护一个名为
changeset-release/master的「Version Packages」PR。该 PR 应用版本提升、写入各包CHANGELOG.md条目,并删除被消费掉的变更集文件。 - Publish(发布):合并该 PR 即触发发布。工作流随后构建整个 workspace,运行
changeset publish(内部通过yarn npm publish发布),并为每个已发布包创建 git tag 与 GitHub Release。
依赖驱动的连带发布
一个包何时会被依赖拖入同一轮发布,取决于版本声明范围(见 CONTRIBUTING.md):
- 仅当新版本超出已声明范围时才会被拖入,且仅限运行时依赖——
devDependencies永远不会导致连带发布; workspace:~会展开为~<core 的当前版本>,因此@joint/layout-directed-graph与@joint/layout-msagl在@joint/core发 minor 或 major 时自动获得一个 patch 补发,但@joint/core发 patch(如4.3.2仍满足~4.3.1)时不触发;@joint/decorators与@joint/react使用workspace:^,因此只在@joint/core发 major 时随同发版。
Linked 联动组的实际版本行为
@joint/core、@joint/layout-directed-graph、@joint/layout-msagl(以及配置中的 @joint/router-avoid)构成 linked 组(见 .changeset/config.json):同轮发布的任意成员共享一个版本号(取该轮最高 bump 类型,应用于组内当前最高版本)。联动只覆盖本轮实际发布的包——组内没有发布内容的包保持当前版本,因此 @joint/core 可以是 4.3.1 而两个 layout 包仍是 4.3.0;其余包各自独立发版。
预发布与快照发布
- 预发布(pre mode):标准做法是在
master上执行yarn changeset pre enter beta,按常规流程发布,结束后执行yarn changeset pre exit; - 快照发布(snapshot release):
yarn changeset version --snapshot搭配yarn changeset publish --tag使用。
六、仓库中的真实样例对照
将规范落到实物上,可以对照 packages/joint-core/CHANGELOG.md 验证格式的实际渲染效果。例如其 4.3.0 Minor Changes 下的条目:
- dia.Paper - add `originX` and `originY` options to `getFitToContentArea()`
- dia.Paper - add typed `EventMap` for IDE autocomplete and type-checking on `on()` calls
- mvc.View - add `classNamePrefix` instance property to override the `joint-` CSS class prefix
- routers.rightAngle - add `minPathMargin`, `sourceMargin`, and `targetMargin` options
这些条目正是「namespace.Class 作用域 + 动词开头描述 + 反引号代码工件 + 方法带 ()」规则的完整体现;而 4.3.2 Patch Changes 中 alg.rightAnglePath: refactor to consolidate duplicate code into helpers 一类则展示了无作用域(包级/架构级)写法的变体。注意补丁条目末尾出现的短 SHA(如 0a2991b),正是 .changeset/changelog.cjs 中 getReleaseLine 追加 (<short SHA>) 的结果,与「一个变更集 = 一条 bullet、续行缩进」的生成逻辑完全吻合。
七、实用速查
| 场景 | 操作 |
|---|---|
| 常规添加变更集 | yarn changeset |
| 手写变更集 | 在 .changeset/ 新建 Markdown,frontmatter 写 "包名": bump类型,正文写 scope - description |
| PR 触及可发布文件但不应发布 | yarn changeset add --empty |
| 校验是否缺变更集 | changeset status --since=origin/master(CI 自动执行) |
| 预发布 beta | yarn changeset pre enter beta / ... / yarn changeset pre exit |
| 快照发布 | yarn changeset version --snapshot + yarn changeset publish --tag |
| 自定义 changelog 行生成 | 见 .changeset/changelog.cjs |
| 可发布文件匹配规则 | 见 .changeset/config.json 的 changedFilePatterns |
总结:Changesets 在 JointJS 中既约束了贡献者的提交习惯(一个 PR 一个变更集、正文单行、作用域规范),又通过 linked 分组与 changedFilePatterns 精确控制了版本演进的粒度。理解 .changeset/ 这套约定,是参与 JointJS 日常开发、读懂其 changelog 与发布节奏的前提。