Changesets:从包级变更意图到可恢复发布计划
Monorepo 的版本难题不在于包多,而在于一次改动可能改变多个消费者契约,并沿内部依赖继续传播。PR 仍然打开时,作者和评审者最清楚“谁受影响、属于哪类变化”;等代码进入主线后再从 diff 猜版本,信息已经损失。Changesets 把这份判断保存为仓库文件,随后将多份声明折叠成 release plan,再更新版本、Changelog 并发布包。
changeset 文件保存的是消费者影响
实验基线使用 Changesets CLI 3.0.0,运行时要求 Node ^22.11 || ^24 || >=26。它是一次主要版本变化,升级旧仓库时应先在临时分支比较配置、release plan 和生成文件。
npm install --save-dev --save-exact @changesets/cli@3.0.0
npx changeset init
npx changeset --version初始化产生 .changeset/config.json 和说明文件。开发阶段的声明是带 frontmatter 的 Markdown:
---
"@acme/api-client": minor
"@acme/react": patch
---
Add cursor pagination and adapt the React query hook.文件名可以由交互命令生成,内容必须由人审阅。摘要描述消费者能够观察到的行为,不应只写 issue 编号或“update package”。patch 表达既有契约内的修复,minor 表达向后兼容能力,major 表达不兼容变化;数据库 schema、配置默认值、错误码、CLI 输出、CSS selector、事件 payload 和类型收窄都可能改变契约,不能按代码行数机械决定。
并非每个 PR 都要声明。内部重构、测试或 CI 配置可以免除,但仓库需要可审计的免声明规则。强制生成空洞 patch 会污染 Changelog;完全靠作者自觉又会漏掉真实影响。比较稳妥的做法是让路径和标签辅助分类,由评审者确认消费者证据。
配置决定包图怎样传播
{
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}baseBranch 决定比较基线,引用不存在或过旧时,缺失声明判断会失真。commit: false 让 CLI 不替开发者悄悄提交生成文件,保留 index、签名和代码评审入口。fixed 适合必须锁步演进的一组包;它会扩大版本变化集合,不是“依赖很多”时的通用开关。linked 约束相关包的版本协调,不等于任意一包变化都强制发布全部包。
updateInternalDependencies 影响内部依赖变化如何传播,必须用真实 workspace 图验证。ignore 只适合短期过渡;被忽略包与正常包同时出现在声明中,或内部依赖必须跨越忽略边界时,工具可能拒绝生成计划。私有包是否版本化也应由内部消费者策略决定,不能用 private 掩盖仍需沟通的行为变化。
包重命名容易留下悬空声明。Changeset 引用 package.json.name,不是目录路径;移动目录不会替历史文件改包名。合并前运行状态检查,并确认每个声明包仍存在。
status 是 PR 阶段的证据入口
git cat-file -e 'origin/main^{commit}'
npx changeset status --since=origin/main
git diff -- .changesetchangeset status 会解析声明并计算候选 release plan。输出为空不一定代表没有消费者影响,也可能是 base 引用错误、浅克隆、包名失效或仓库策略没有将缺失声明转为失败。门禁应区分 required、not-required、invalid 和 tool-failed,避免把工具异常记录成“无需发布”。
正向实验可以修改一个测试包,运行 npx changeset 选择包和 bump 类型,再确认 status 输出。反向实验把声明中的包名改成不存在的值,CLI 应非零退出;恢复真实包名后再次运行,证明失败来自输入而不是缓存。实验只在临时分支进行,不执行 version 或 publish。
评审不能止于“文件存在”。包集合要与代码和公共契约对应,bump 类型要能解释消费者迁移,摘要要足以进入 Changelog。底层包发生 major 时,依赖方是否升级取决于依赖范围、重新导出和使用方式,而不是固定加一条 patch。
release plan 把多份声明折叠成一次决定
主线可能积累多份针对同一包的 changeset。Changesets 读取这些文件、当前版本、内部依赖与配置,生成每个包的最终 bump 和 Changelog 片段。这个 release plan 才是版本任务的输入;直接手改 package.json 会绕过传播计算。
npx changeset status --output=tmp/release-plan.json
npx changeset version
git diff -- .changeset package.json packages '**/CHANGELOG.md' '*lock*'changeset version 会消费已纳入计划的声明,更新包版本、内部依赖范围、Changelog 和锁文件。它应在专用版本分支或 Version PR 中运行,让生成结果经过评审。命令结束并不等于结果正确,尤其要检查底层 major 是否传播、被忽略包是否阻断、固定组是否扩大、Changelog 是否丢失迁移说明。
生成版本文件与发布外部制品是两种副作用。前者仍可通过 Git 分支审阅和重算;后者会创建 Registry 版本与 Tag,通常不可覆盖。生产流水线只保留一个发布入口,不允许开发分支和多个定时任务同时执行。
publish 前先固定身份和不可变输入
发布任务从受保护分支读取已经评审的版本提交,先验证工作树干净、Registry 身份、目标包集合和版本不存在,再执行:
set -eu
test -z "$(git status --porcelain)"
npx changeset status
npm whoami
npx changeset publish真实流水线还应固定 Node、包管理器、CLI 与 lockfile,采用短期身份或最小权限 Token,并限制 fork 和不可信脚本接触发布凭据。publish 会逐包调用包管理器,不能假定多包发布具有事务性。某些包成功、后续包失败时,重跑前必须先查询 Registry 和 Git Tag,判断哪些外部状态已经存在。
恢复原则是前滚缺失部分,不覆盖已发布版本。核对每个计划包的 Registry 版本、dist-tag、Tag、Changelog 和 release 记录;已经存在且摘要一致的包视为完成,缺失包修复原因后继续。若已发布内容错误,使用弃用、后续修复版本和清晰公告处理,不尝试复用同一个不可变版本号。
预发布和 Snapshot 是不同通道
预发布模式显式改变 release plan 的状态。进入、退出都要提交 .changeset/pre.json 等状态文件,并在隔离分支验证:
npx changeset pre enter next
npx changeset version
npx changeset pre exit预发布 dist-tag、分支和消费者必须对应,不能让测试版意外占用稳定通道。退出预发布后重新审查最终稳定版本,不能把一串测试发布简单视为已经完成正式版评审。
Snapshot 适合短期验证构建,不承担稳定 SemVer 历史。其版本模板、Registry 保留和清理策略需要明确,不能让 snapshot 混入 latest 或长期依赖范围。即使是临时包,也应绑定 commit SHA 和构建证据,防止同名内容漂移。
与 semantic-release 的边界
Changesets 由显式包级声明驱动,尤其适合 Monorepo 和 Version PR;semantic-release 主要从提交历史、分支和插件链推导版本并执行发布。两者可以服务不同仓库,但同一个包不应由两个生产入口同时计算和发布版本。
semantic-release 主线在家族 18 负责其自己的分支、Channel、插件和外部副作用。Changesets 产品的声明、release plan、version、预发布与 publish 以当前入口为准,其他任务页只消费其生成结果。
故障按 Git、包图和外部状态分层
声明存在却不进入计划时,先核对 baseBranch、--since、文件是否已提交、包名和 workspace 发现范围。每个 PR 都产生低信息 patch,说明仓库没有区分消费者影响,需要修订路径分类和免声明评审,而不是继续生成噪声。
内部依赖传播超出预期时,回查 fixed、linked、updateInternalDependencies、workspace 协议和依赖范围。手工删除工具计算出的关联 bump 可能留下无法安装的版本组合,应先修正包边界或配置,再重算 release plan。
Version PR 长期反复冲突时,检查是否有多个机器人或人工任务同时生成版本文件。把生产入口收敛为一个,重新基于主线计算;不要在冲突中手工拼接已消费的 changeset,否则同一意图可能重复或丢失。
升级、迁移与退出
主要版本升级先保存当前配置、代表 changeset、release plan 与生成 diff,在临时分支使用新 CLI 重算。比较包集合、bump、内部依赖、Changelog、预发布和 publish 输出,任何变化都要有产品变更说明或仓库决策支撑。CLI、changelog 模块和 commit 配置都可能执行代码,依赖升级按供应链代码审查。
移除 Changesets 前先盘点主线和未合并分支中的 .changeset/*.md。这些文件保存尚未消费的意图,直接删除会丢失审计信息。替代系统必须迁移包名、bump 类型、摘要、PR 关联和未完成 release plan;外部已经发布的版本只能按 Registry 事实接续。
可靠的 Changesets 流程能从任一版本反查声明和评审,也能在发布中断后回答“哪些包已存在、哪些尚未发布、下一动作会不会覆盖状态”。如果团队只能看到一个绿色机器人评论,却不能重建 release plan 和外部副作用,版本自动化仍然不可恢复。
