tldraw SDK 发布机制详解:非语义化版本策略、minor/patch 发布流程与 GitHub Actions 实现
tldraw SDK 采用一套区别于 NPM 常规实践的发布策略:major 版本极少、minor 版本按月度节奏发布且可能包含破坏性变更、patch 版本用于无法等待下一周期的热修复。本文以仓库根目录的 RELEASES.md 为主体,结合 .github/workflows/publish.yml、internal/scripts/publish-new.ts、internal/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.mdx 到 apps/docs/content/releases/v5.3.0.mdx 的逐版本发布说明文档,且存在专门的迁移技能文档(如 apps/docs/content/releases/migration-skill.mdx)来辅助用户跨版本迁移。
发布新 minor/major 版本:从 production 分支手动触发
新周期版本一律从 production 分支发布,通过手动触发 publish.yml 工作流完成。操作步骤:
- 打开 publish.yml 对应的 Actions 页面,选择
production分支,点击 "Run workflow"; - 将 publish type 设置为
new; - 填写表单:保持默认值即发布新的 minor 版本;如需发布 major 版本,在下拉框中选择该选项;
- 如需发布指定版本号,选择
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 类型只能是 minor、major 或 override;使用 override 时必须提供版本号,反之提供版本号却没有选择 override 也会报错。
publish-new.ts:一次新发布的完整调用链
核心脚本 internal/scripts/publish-new.ts 的 main() 函数展示了 RELEASES.md 中"GitHub action will do the following things"背后的真实实现:
- 校验分支:执行
git rev-parse --abbrev-ref HEAD,若不是production直接抛错(publish-new.ts); - 计算下一个版本号:
getNextVersion()通过npm show tldraw version(internal/scripts/lib/publishing.ts 中的getLatestTldrawVersionFromNpm)读取 npm 上最新版本,再用semver的inc('minor'|'major')递增;override则直接使用传入版本。源码中已明确断言 prerelease 版本不再被新发布流程支持; - 查找 draft release:在 GitHub 上分页拉取所有 release,找到名为
v{nextVersion}且处于 draft 状态的 release,并要求其带有 release notes 正文。找不到时错误信息会列出所有可用的 draft release,便于排查。也就是说,发布说明(changelog 正文)在发布前就以 draft release 的形式预先写好,发布动作只是把它转正; - 统一写版本号:
setAllVersions(nextVersion, { stageChanges: true })(internal/scripts/lib/publishing.ts)遍历packages/下所有非 private 包,改写各自的package.json版本字段,同时更新根目录 lerna.json 的version字段(当前为5.3.2)以及仓库内所有**/*/version.ts文件,随后执行yarn refresh-assets并git add这些变更。这解释了"monorepo 内所有包版本同步递增"的机制; - 打 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); - 同步文档与示例站:
publishProductionDocsAndExamplesAndBemo()将 HEAD 强推为远端的docs-production与bemo-production分支(internal/scripts/lib/publishing.ts),供文档站部署使用; - 转正 draft release:调用 Octokit 的
updateRelease将 draft 置为正式发布,tag 与 name 均为v{version}; - 上传静态资产并发布 npm 包:
uploadStaticAssets(nextVersion)之后调用publish()。若此步失败,源码注释明确提示"在本地运行publish-manual.ts脚本"作为兜底; - 回刷 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 步,这里完整继承并结合自动发布逻辑说明:
-
确保 git 仓库是最新的
git fetch -
检出最新的 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 -
基于 release 分支新建工作分支
git checkout -b david/my-helpful-patches把分支名换成能表达本次补丁内容的名字。
-
cherry-pick 需要纳入补丁的提交
git cherry-pick <commit-hash>一次补丁可以 cherry-pick 多个提交,把多个 bugfix 打进同一个 patch 版本。
-
推送分支并创建指向 release 分支的 PR;
-
合并该 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.md与RELEASE_NOTES.md——这两个文件是在发布时由 internal/scripts/generate-tldraw-package-docs.ts 从apps/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-shared、worker-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 发布到
latesttag;否则发布到revisiontag,避免覆盖latest。由于版本号不含 prerelease 标识,semver 范围(如^1.0.0)的用户依然能拿到更新(publish-patch.ts); - 失败告警:工作流在失败时会 checkout 本地的
discord-fail-notifyaction 并向 Discord 部署频道推送通知(publish.yml)。
此外,new 以及"最新版 patch/manual"成功后,publish.yml 还会级联触发 publish-templates.yml 与 deploy-templates.yml,把 templates/ 下的示例工程同步到独立仓库并用新 SDK 版本重新部署——这是 RELEASES.md 未展开、但对维护模板仓库很重要的下游动作。
文档站与 npm 发布同步
RELEASES.md 对文档的说明是:文档站与 npm 包同步发布——每次发布新版本时,文档站会自动更新,确保文档始终对应 tldraw 的最新版本。
如果希望独立于周期版本单独发布一个文档改动,走与 patch 发布完全相同的流程(cherry-pick 到 release 分支、合 PR):流水线会自动检测到各包内容没有变化,从而只更新文档站、不产生新的 npm 版本。源码层面的对应关系是:
- 文档站部署由
publishProductionDocsAndExamplesAndBemo()强推docs-production分支驱动(internal/scripts/lib/publishing.ts),patch 流程在"分支无包变更"或"当前是最新版线"时都会执行该推送; - internal/scripts/lib/didAnyPackageChange.ts 的 tarball 比对是"只更新文档"这一行为的关键判定依据;
- 与此同时,update-release-notes.yml 工作流在每次推送
production分支时自动运行,通过 Agent 技能重新生成 apps/docs/content/releases/ 下的发布说明文档并向main发起 PR,保证next.mdx与对应的 draft GitHub release 保持同步。
小结:分支、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 修复会自动流转到 latest 或 revision tag,无需额外操作。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0625
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00