首页
/ tldraw SDK 发布机制详解:非语义化版本策略、minor/patch 发布流程与 GitHub Actions 实现

tldraw SDK 发布机制详解:非语义化版本策略、minor/patch 发布流程与 GitHub Actions 实现

2026-09-06 17:41:53作者:钟日瑜

tldraw SDK 采用一套区别于 NPM 常规实践的发布策略:major 版本极少、minor 版本按月度节奏发布且可能包含破坏性变更、patch 版本用于无法等待下一周期的热修复。本文以仓库根目录的 RELEASES.md 为主体,结合 .github/workflows/publish.ymlinternal/scripts/publish-new.tsinternal/scripts/publish-patch.ts 等发布脚本源码,完整还原 tldraw 从版本号规划、npm 发包、release 分支管理到文档站同步的全套发布流水线。

版本策略:为什么 tldraw 不遵循语义化版本(SemVer)

多数 JavaScript 包严格遵循 语义化版本 约定,但 tldraw 有意偏离了这一惯例。其版本管理规则如下:

  • major 版本升级非常罕见,仅保留给某种"范式转移"级别的重大变化;
  • minor 版本按固定节奏发布,截至文档撰写时为每月一次,且可能包含破坏性变更。团队通过提前数个版本发布警告(deprecation warnings)以及提供迁移工具来降低破坏性变更的影响面;官方建议用户以与发布节奏相近的频率升级 tldraw,并务必检查 release notes
  • patch 版本用于 bugfix 和 hotfix,即那些无法等待下一个周期版本发布的修复。

这套策略的实际含义是:对使用者而言,minor 升级不能想当然地视为"安全升级",而必须阅读每个版本的发布说明。这也是为什么仓库中维护着从 apps/docs/content/releases/v2.0.0.mdxapps/docs/content/releases/v5.3.0.mdx 的逐版本发布说明文档,且存在专门的迁移技能文档(如 apps/docs/content/releases/migration-skill.mdx)来辅助用户跨版本迁移。

发布新 minor/major 版本:从 production 分支手动触发

新周期版本一律从 production 分支发布,通过手动触发 publish.yml 工作流完成。操作步骤:

  1. 打开 publish.yml 对应的 Actions 页面,选择 production 分支,点击 "Run workflow";
  2. 将 publish type 设置为 new
  3. 填写表单:保持默认值即发布新的 minor 版本;如需发布 major 版本,在下拉框中选择该选项;
  4. 如需发布指定版本号,选择 override 选项并填入精确版本号,如 3.4.0

点击 run 之后,GitHub Actions 会依次执行以下事情:

  • 更新各 package.json 中的版本号;
  • 更新 changelog;
  • 用 changelog 条目中的发布说明在 GitHub 上创建新 release;
  • 将新包发布到 npm;
  • 为该版本创建新的 release 分支,例如版本 3.4.0 会创建名为 v3.4.x 的分支。

工作流如何判定发布模式

publish.yml 的第一步 "Determine publish mode" 通过事件类型和引用(ref)推导发布模式,源码中的判定逻辑与 RELEASES.md 描述一一对应,并暴露了文档之外的更多细节:

触发事件 条件 模式 对应脚本
push 推送到 main canary internal/scripts/publish-prerelease.ts
push 推送到 production next internal/scripts/publish-prerelease.ts
push 推送到匹配 ^v[0-9]+\.[0-9]+\.x$ 的分支 patch internal/scripts/publish-patch.ts
pull_request 被打上 publish-packages 标签 internal internal/scripts/publish-prerelease.ts
workflow_dispatch 用户手动指定 manual / new internal/scripts/publish-manual.ts / internal/scripts/publish-new.ts

new 模式在源码中还有严格的护栏校验(publish.yml):必须位于 production 分支;bump 类型只能是 minormajoroverride;使用 override 时必须提供版本号,反之提供版本号却没有选择 override 也会报错。

publish-new.ts:一次新发布的完整调用链

核心脚本 internal/scripts/publish-new.tsmain() 函数展示了 RELEASES.md 中"GitHub action will do the following things"背后的真实实现:

  1. 校验分支:执行 git rev-parse --abbrev-ref HEAD,若不是 production 直接抛错(publish-new.ts);
  2. 计算下一个版本号getNextVersion() 通过 npm show tldraw versioninternal/scripts/lib/publishing.ts 中的 getLatestTldrawVersionFromNpm)读取 npm 上最新版本,再用 semverinc('minor'|'major') 递增;override 则直接使用传入版本。源码中已明确断言 prerelease 版本不再被新发布流程支持;
  3. 查找 draft release:在 GitHub 上分页拉取所有 release,找到名为 v{nextVersion} 且处于 draft 状态的 release,并要求其带有 release notes 正文。找不到时错误信息会列出所有可用的 draft release,便于排查。也就是说,发布说明(changelog 正文)在发布前就以 draft release 的形式预先写好,发布动作只是把它转正;
  4. 统一写版本号setAllVersions(nextVersion, { stageChanges: true })internal/scripts/lib/publishing.ts)遍历 packages/ 下所有非 private 包,改写各自的 package.json 版本字段,同时更新根目录 lerna.jsonversion 字段(当前为 5.3.2)以及仓库内所有 **/*/version.ts 文件,随后执行 yarn refresh-assetsgit add 这些变更。这解释了"monorepo 内所有包版本同步递增"的机制;
  5. 打 tag 并创建 release 分支:提交名为 v{version} 的 commit,创建同名 annotated tag,然后执行 git push origin HEAD:refs/heads/v{major}.{minor}.x --follow-tags——这就是 RELEASES.md 所说"为版本 3.4.0 创建 v3.4.x 分支"的具体实现(publish-new.ts);
  6. 同步文档与示例站publishProductionDocsAndExamplesAndBemo() 将 HEAD 强推为远端的 docs-productionbemo-production 分支(internal/scripts/lib/publishing.ts),供文档站部署使用;
  7. 转正 draft release:调用 Octokit 的 updateRelease 将 draft 置为正式发布,tag 与 name 均为 v{version}
  8. 上传静态资产并发布 npm 包uploadStaticAssets(nextVersion) 之后调用 publish()。若此步失败,源码注释明确提示"在本地运行 publish-manual.ts 脚本"作为兜底;
  9. 回刷 main 分支版本:通过 triggerBumpVersionsWorkflow()main 分支派发 bump-versions.yml 工作流,把所有包版本同步为 npm 上的最新版。

npm 发包的实现细节

publish() 函数(internal/scripts/lib/publishing.ts)有几个值得注意的工程细节:

  • 发布顺序topologicalSortPackages() 按包间 workspace 依赖做拓扑排序,保证依赖被发布先于依赖方;
  • 认证方式:走 npm 的 trusted publisher OIDC 流程——CI 授予 id-token: write 权限后,yarn 自动用 GitHub 签发的 OIDC token 换取短期发布 token,仓库中无需长期 npm token;
  • 必须用 yarn npm publish 而非 npm publish:这样 yarn 会把发布 tarball 中的 workspace:* 依赖描述符改写为具体的兄弟包版本,否则会原样带上 workspace:* 导致 monorepo 外部用户安装失败;
  • 幂等与重试:若 npm 报 "cannot publish over the previously published versions" 则视为已发布并跳过;每个包的 publish 带 5 次重试,之后还会 HEAD 请求 registry.npmjs.org 轮询确认版本已可访问(最多 50 次、每次间隔 10 秒),再进入下一个包;
  • dist-tag:正式包打 latest,带 prerelease 标识的版本按 prerelease 前缀打 tag(patch 场景见下文)。

发布 patch 版本:cherry-pick 到 release 分支,合并即自动发布

RELEASES.md 给出的 patch 发布手工流程共 6 步,这里完整继承并结合自动发布逻辑说明:

  1. 确保 git 仓库是最新的

    git fetch
    
  2. 检出最新的 release 分支

    每个 major/minor 版本在发布时会获得自己的 release 分支,命名规则是v 开头、以 .x 结尾(如 v2.0.x)。patch 一律从这些分支发布。查看最新版本的命令是:

    npm show tldraw version
    

    然后给版本号加上 v 前缀、把 patch 位替换为 x 来检出对应分支。例如最新版是 3.4.3,则执行:

    git checkout v3.4.x
    

    也可以给更老的 release 分支打补丁。例如最新版是 3.4.3 但需要给 2.8.2 打补丁:

    git checkout v2.8.x
    
  3. 基于 release 分支新建工作分支

    git checkout -b david/my-helpful-patches
    

    把分支名换成能表达本次补丁内容的名字。

  4. cherry-pick 需要纳入补丁的提交

    git cherry-pick <commit-hash>
    

    一次补丁可以 cherry-pick 多个提交,把多个 bugfix 打进同一个 patch 版本。

  5. 推送分支并创建指向 release 分支的 PR

  6. 合并该 PR

合并之后 patch 发布会自动完成publish.yml 监听到匹配 v*.*.x 的分支 push,以 patch 模式运行 internal/scripts/publish-patch.ts。changelog 与版本号的更新会提交回 release 分支,并刻意不提交回 main(main 的版本同步由前述 bump-versions 工作流单独负责)。

publish-patch.ts:自动发布的防御性设计

这个脚本的实现比文档描述多了大量保护逻辑,理解它们有助于判断"什么情况下合了 PR 却不会出新包":

  • 分支名校验:当前分支必须匹配 /^v(\d+)\.(\d+)\.x$/,否则直接报错退出;
  • 跳过首发提交:若当前 HEAD 恰好带有 v{major}.{minor}.0 这个 tag(即本 minor 的初始发布提交),说明是分支刚创建时的 push,跳过 patch 流程,避免误发 v3.4.1
  • 无包变更则只更新文档getAnyPackageDiff()internal/scripts/lib/didAnyPackageChange.ts)会下载 npm 上已发布版本的 tarball,与本仓库 yarn pack 出的本地 tarball 逐文件做二进制比对;若完全一致(典型场景:只 cherry-pick 了文档改动),则不发布新版本,仅在"这是最新版分支"时推送 docs-production/bemo-production 分支更新文档站。比对时特意忽略了 tldraw 包根部的 DOCS.mdRELEASE_NOTES.md——这两个文件是在发布时由 internal/scripts/generate-tldraw-package-docs.tsapps/docs/content 生成的,文档改动会改变它们,如果不忽略就会误判为包有变更;
  • 孤儿版本处理:如果上一次 patch 发布在"版本号 commit 已推上去、npm publish 尚未完成"时被中断,本地 packages/tldraw/package.json 的版本会领先于 npm。此时脚本以本地版本为基准递增,直接跳过这个"孤儿版本",防止 git commit(无变更)与 git push --follow-tags(tag 已存在于其他提交上)失败把分支永久卡死;
  • 版本号递增与 tag:从基准版本 inc('patch') 得到下一版本,setAllVersions 统一改写版本,提交消息为 v{version} [skip ci],打同名 tag 后 git push --follow-tags
  • changelog 生成extractChangelog(prevTag, 'HEAD')internal/scripts/extract-draft-changelog.tsx)用 git cherry 找出区间内的提交,跳过标题含 [skip ci] 的提交,只保留触及 packages/ 目录(排除 dotcom-sharedworker-shared)的条目;从提交信息中提取 "Release Notes" 与 "API Changes" 小节,并根据提交信息中 - [x] bugfix/improvement/feature/api/other 复选框归类,最终按 Bug Fixes / Improvements / Features / API Changes / Other Changes 分组生成 changelog,作为 GitHub release 的正文。这也解释了为什么团队的提交模板里要求写 "Change type" 复选框与 "Release Notes" 小节——它们直接被发布流水线消费;
  • dist-tag 策略:若该分支正是 npm 上的最新版本线,patch 发布到 latest tag;否则发布到 revision tag,避免覆盖 latest。由于版本号不含 prerelease 标识,semver 范围(如 ^1.0.0)的用户依然能拿到更新(publish-patch.ts);
  • 失败告警:工作流在失败时会 checkout 本地的 discord-fail-notify action 并向 Discord 部署频道推送通知(publish.yml)。

此外,new 以及"最新版 patch/manual"成功后,publish.yml 还会级联触发 publish-templates.ymldeploy-templates.yml,把 templates/ 下的示例工程同步到独立仓库并用新 SDK 版本重新部署——这是 RELEASES.md 未展开、但对维护模板仓库很重要的下游动作。

文档站与 npm 发布同步

RELEASES.md 对文档的说明是:文档站与 npm 包同步发布——每次发布新版本时,文档站会自动更新,确保文档始终对应 tldraw 的最新版本。

如果希望独立于周期版本单独发布一个文档改动,走与 patch 发布完全相同的流程(cherry-pick 到 release 分支、合 PR):流水线会自动检测到各包内容没有变化,从而只更新文档站、不产生新的 npm 版本。源码层面的对应关系是:

小结:分支、tag 与发布物的对应关系

综合 RELEASES.md 与源码,tldraw 的发布模型可以概括为:

分支 用途 发布产物
main 开发主干,版本滞后 通过 bump-versions 工作流周期性同步版本号
production 下一版本集成的集成分支 手动触发 new 发布 minor/major
v{major}.{minor}.x 每个版本的 release 分支 合并 PR 自动发布 patch
tag v{version} 每次发布打点 对应 GitHub release + npm 包 + 文档站快照

对使用者的直接启示是:升级 minor 版本前应阅读对应版本的 release notes(仓库中 apps/docs/content/releases/ 目录与 GitHub release 均可查阅);在 ^3.0.0 这类 semver 范围内,patch 修复会自动流转到 latestrevision tag,无需额外操作。

登录后查看全文
热门项目推荐
相关项目推荐