深入解析 Storybook 的自动化发布体系:分支策略、Release PR 与版本管理全流程
本文以 Storybook 官方的发布规范 RELEASING.md 为主体,完整拆解该 monorepo 中"非补丁发布"与"补丁发布"两条发布流水线的运作原理、发布者的标准操作步骤、各类版本升级场景的输入组合,以及紧急情况下手动发布的完整操作序列。读完本文,你将能够理解 Storybook 是如何用 GitHub Actions 与 NodeJS 脚本把"changelog 生成、版本号升级、npm 发包、GitHub Release 创建"这条复杂链路自动化起来的,并能独立复述其分支策略与 canary 发布机制。
1. 发布体系总览
Storybook 的发布过程分为两大类:
- 非补丁发布(Non-patch releases):发布
next分支上的任何内容,既可以是预发布版(prerelease),也可以是正式版(stable); - 补丁发布(Patch releases):从
next挑选需要打回当前稳定小版本(main)的内容。
整个流程建立在自动创建的 "Release Pull Requests" 之上:当这些 PR 被合并时,就会触发一个新版本的发布。由核心团队成员轮值担任的"发布者"(Releaser)会在当前 Release PR 中执行发布流程。该流程由三部分实现:
- NodeJS 脚本,位于 scripts/release/;
- 三个 GitHub Actions 工作流:
所有 CLI 入口都注册在 scripts/package.json 中,以 release: 前缀命名,例如:
| npm script | 对应实现 | 作用 |
|---|---|---|
yarn release:version |
version.ts | 计算并写入新版本号 |
yarn release:write-changelog |
write-changelog.ts | 生成并写入 changelog |
yarn release:pick-patches |
pick-patches.ts | 挑选并 cherry-pick 补丁 PR |
yarn release:publish |
publish.ts | 构建并发布所有 npm 包 |
yarn release:is-pr-frozen |
is-pr-frozen.ts | 判断 Release PR 是否被"冻结" |
yarn release:unreleased-changes-exists |
unreleased-changes-exists.ts | 检查是否存在可发布的变更 |
yarn release:label-patches |
label-patches.ts | 给已发布的补丁 PR 打上 "patch:done" 标签 |
yarn release:generate-pr-description |
generate-pr-description.ts | 生成 Release PR 的描述文本 |
yarn release:cancel-preparation-runs |
cancel-preparation-runs.ts | 发布时取消仍在运行的准备任务 |
1.1 分支策略
理解发布结构前,必须先理解分支模型:
- 所有开发都在
next分支上进行,新特性和 bug 修复都进入这里。该分支的内容将进入下一个预发布版(如v7.1.0-alpha.22); main分支保存当前稳定版的内容(如v7.0.20)。当一个变更同时需要进入下一个 minor/major 和当前 patch 版本时,改动合入next,然后给该 PR 打上 "patch:yes" 标签,发布工作流便会将其挑选回main。这样做的目的是:变更先在预发布版中被验证,再进入稳定版;- 真正的(预)发布不直接发生在
next或main上,而是分别发生在next-release与latest-release上。这一"间接层"的原因见 第 8 节。
简化后的分支关系图如下(原文档中的 mermaid 图):
%%{init: { 'gitGraph': { 'showCommitLabel': false } } }%%
gitGraph
commit
branch latest-release
branch next
commit
branch next-release
commit
commit tag: "7.1.0-alpha.18"
checkout next
merge next-release
commit id: "bugfix"
commit
checkout latest-release
cherry-pick id: "bugfix"
commit tag: "7.0.20"
checkout next-release
merge next
commit tag: "7.1.0-alpha.19"
checkout next
merge next-release
commit
checkout main
merge latest-release
2. Release Pull Requests:发布的"接口"
两个 GitHub Actions 工作流会自动创建两类发布 Pull Request(每类一个)。这些 PR 充当 Releaser 创建新版本的"接口"。高层流程为:
- 当一个 PR 合入
next(或向next推送提交)时,两个 Release PR 都会被(重新)生成; - 它们创建一个新分支——
version-(patch|non-patch)-from-<CURRENT-VERSION>; - 按版本策略计算要升级到的版本号;
- 用检测到的全部变更更新
CHANGELOG(.prerelease).md; - 提交所有内容;
- 强制推送(force push);
- 向
next-release或latest-release发起/更新 Pull Request。
几个关键点:
- PR 会在
next的任何变更上重新生成,也可以手动触发(见 第 4.4 节); - 变更是被 force push 到分支的,因此合并前对发布分支的任何手动修改,都有可能在别人合入
next的新变更而触发工作流时被覆盖。为避免这种情况,要给 PR 打上 "freeze" 标签; - changelog 在准备阶段就已提交,但包的版本号升级和发布要等到后续阶段;
- Release PR 的目标不是其工作分支(
next/main),而是next-release/latest-release。
从源码看,"冻结"机制由 is-pr-frozen.ts 实现:它根据 code/package.json 中的当前版本拼出分支名 version-${patch ? 'patch' : 'non-patch'}-from-${version},fetch 该远端分支,通过其 HEAD commit 找到对应的 open PR,再检查该 PR 是否带有 freeze 标签。两个 prepare 工作流都在开头调用它——例如 prepare-non-patch-release.yml 中的 "Check if pull request is frozen" 步骤,若 frozen == 'true' 且触发事件不是 workflow_dispatch,工作流会执行 gh run cancel 取消自身。这正是"freeze 标签不会阻止手动触发"这一行为的底层实现。
2.1 补丁发布(Patch Releases)
补丁发布通过 cherry-pick 合入 next 但尚未发布、且带 "patch:yes" 标签的所有 PR 的 merge commit 来创建。
有些场景下即使内容不算"可发布"也希望把 PR 挑选回 main(与 non-patch 准备不同,patch 发布不会因内容不可发布而取消)。例如:变更只涉及文档和/或内部构建系统时,创建新 patch 版本可能没意义,但把变更带回 main 是部署文档到生产文档站的唯一途径;也可能需要把内部 CI 的修复 cherry-pick 回去。在这类"所有 cherry pick 都不可发布"的情况下,准备工作流会创建一个 "merging" PR 而不是 "releasing" PR——它不升版本、不更新 changelog,只是把变更 cherry-pick 过去,允许你把它合并进 latest-release → main。这一点在 prepare-patch-release.yml 中有对应实现:当 has-changes-to-release == 'false' 时,它会创建一个标题为 Release: Merge patches to main (without version bump) 的 PR。
准备工作流会顺序地把每个 patch PR cherry-pick 到它的分支上。如果某次 cherry-pick 因冲突或其他原因失败,会被忽略并继续处理下一个 PR;所有失败的 cherry-pick 都会列在 Release PR 的描述中(工作流通过 failed-cherry-picks 输出传给 generate-pr-description),由 Releaser 在发布过程中手动挑选。当 main 与 next 分歧越大(即距上一次稳定 major/minor 发布越久),这类冲突就越常见。
与 non-patch 流程类似,patch 准备工作流会从 main 创建名为 version-patch-from-<CURRENT-STABLE-VERSION> 的新分支,并开一个目标为 latest-release 的 PR。当 Releaser 合并该 PR 后,发布工作流最终会把 latest-release 合并进 main。
下面的示例中,一个 feature 和两个 bug 修复合入了 next,只有两个 bug 修复带 "patch:yes" 标签,因此只有它们进入新的 7.0.19。注意被 cherry-pick 的是合入 next 的 merge commit,而不是 bugfix 分支上的原始提交:
gitGraph
commit
branch latest-release
branch next
checkout latest-release
commit tag: "v7.0.18"
checkout main
merge latest-release
checkout next
commit
branch some-patched-bugfix
commit
commit id: "patch1"
checkout next
merge some-patched-bugfix
branch new-feature
commit
checkout next
merge new-feature
branch other-patched-bugfix
commit id: "patch2"
checkout next
merge other-patched-bugfix
checkout main
branch version-patch-from-7.0.18
cherry-pick id: "patch1"
cherry-pick id: "patch2"
commit id: "write changelog"
checkout latest-release
merge version-patch-from-7.0.18
commit id: "bump versions" tag: "v7.0.19"
checkout main
merge latest-release
2.2 非补丁发布(Non-patch Releases)
工作流:prepare-non-patch-release.yml
非补丁发布基于 next 分支的全部内容。changelog 通过检查 git 历史生成——查找当前预发布版(位于 next-release)与 next 的 HEAD 之间所有的 commit 和 PR。这一逻辑对应 unreleased-changes-exists.ts 与 get-changes.ts:后者以 --first-parent 拉取 from(默认是最新版本 tag 对应 commit)到 to(默认 HEAD)之间的提交,再逐一反查其关联 PR 的标题与标签。
默认的版本策略是递增当前预发布编号(见 3.1 节)。如果当前没有预发布编号(即刚发布了一个稳定的 minor/major 版本),默认策略会走一次 patch bump,例如从 7.2.0 到 7.2.1-0。
只有当确实存在可发布变更时才会创建 next-PR。带 "build" 或 "documentation" 标签的内容不被视为"可发布"(见 6.3 节),不属于用户可见的变更,因此没有发布意义。准备工作流会从 next 创建名为 version-non-patch-from-<CURRENT-NEXT-VERSION> 的分支,开一个目标为 next-release 的 PR;Releaser 合并后,发布工作流会把 next-release 合并回 next。
示例:一个 feature 和一个 bugfix 被创建并发布为新的 7.1.0-alpha.29。图中方点(square dots)标出的提交都会参与 changelog 生成:
%%{init: { 'gitGraph': { 'mainBranchName': 'next' } } }%%
gitGraph
commit
branch next-release
commit tag: "7.1.0-alpha.28"
checkout next
merge next-release
commit type: HIGHLIGHT id: "direct commit"
branch new-feature
commit
commit
checkout next
merge new-feature type: HIGHLIGHT
branch some-bugfix
commit
checkout next
merge some-bugfix type: HIGHLIGHT
branch version-non-patch-from-7.1.0-alpha.28
commit id: "write changelog"
checkout next-release
merge version-non-patch-from-7.1.0-alpha.28
commit id: "bump versions" tag: "7.1.0-alpha.29"
checkout next
merge next-release
2.3 发布(Publishing)
工作流:publish.yml
当 non-patch 或 patch 发布分支被合并进 latest-release / next-release 时,发布工作流被触发。它依次执行以下任务:
- 按已准备好的 PR 中的计划,为所有包升级版本号;
- 安装依赖并构建所有包;
- 将包发布到 npm;
- (若是补丁发布)给所有相关 PR 打上 "patch:done" 标签;
- 在发布分支(
latest-release或next-release)上创建版本 tag,并新建 GitHub Release; - 把发布分支合并进核心分支(
main或next); - (若是补丁发布)把
CHANGELOG.md的变更从main同步到next。
出于安全考虑,该工作流运行在名为 "Release" 的 GitHub environment 中(publish.yml 的 environment: Release),持有发布到 @storybook npm 组织所需 token,且只能从四个"核心"分支访问:main、next、latest-release、next-release。
对照 publish.yml 的实际步骤,可以观察到文档描述的每一步都有对应实现:
- "Apply deferred version bump and commit":读取
code/package.json中的deferredNextVersion,若存在则执行yarn release:version --apply --verbose,提交Bump version from ... to ... [skip ci]并推回发布分支。这解释了为什么 prepare 阶段只做"延迟版本升级"(见 3.2 节); - "Check if publish is needed":通过
yarn release:is-version-published查询 registry,已发布则跳过; - "Check release vs prerelease":
yarn release:is-prerelease决定 dist tag 是next还是latest,以及 GitHub Release 是否标记为 prerelease; - "Publish":调用
yarn release:publish --tag <next|latest>; - "Label patch PRs as picked":仅在
latest-release分支上执行yarn release:label-patches,且刻意设置continue-on-error,但失败时通过 Discord webhook 告警——注释说明这是因为吞掉权限错误曾导致已发布的 PR 再次出现在后续 patch changelog 中; - "Create GitHub Release":若
v<version>的 release 已存在则跳过,否则用 changelog 作为 notes 创建; - "Merge":把发布分支 merge 回
next或main;对于从next-release发布的正式版(非 prerelease),还会把nextforce push 到latest-release和main,保证三个分支内容一致; - "Sync CHANGELOG.md from main to next":补丁发布后把
main上的CHANGELOG.md同步提交到next,即上面流程的第 7 步。
从 publish.ts 的源码可以看到实际的发包命令是 yarn workspaces foreach --all --parallel --no-private ... npm publish --provenance --tolerate-republish --tag <tag>,即并行发布所有非 private 工作区并附带 npm provenance 信息。关于容错:原文档说"任意数量的包发布失败会重试 5 次,跳过已发布的包";而当前源码中重试上限为 MAX_PUBLISH_ATTEMPTS = 3,失败后会先解析 registry 报错识别已被接受的包、再轮询 registry(间隔 15 秒、最长 15 分钟)等待 staged 版本可见,只针对缺失的包重试。可以推断这里的数字随源码演进有过调整,实际行为以仓库代码为准。
3. 版本策略与输入组合
3.1 各场景对应的输入
存在多种类型相同但做法略有不同的发布场景,"如何触发"由 prepare 工作流的 workflow_dispatch 输入决定。prepare-non-patch-release.yml 定义了 release-type(必选,默认 prerelease)与 pre-id(可选字符串)两个输入:
| 场景 | 版本变化示例 | 需要的输入 |
|---|---|---|
| 预发布(默认策略) | 7.1.0-alpha.12 → 7.1.0-alpha.13 |
无,直接触发即可 |
| 预发布晋级 | 7.1.0-alpha.13 → 7.1.0-beta.0 |
Release type: Prerelease;Prerelease ID: beta |
| Minor/Major 正式版 | 7.1.0-rc.2 → 7.1.0 或 8.0.0-rc.3 → 8.0.0 |
Release type: Patch/Minor/Major;Prerelease ID: 留空 |
| 新 major/minor 的首个预发布 | 7.1.0 → 7.2.0-alpha.0 或 8.0.0-alpha.0 |
Release type: Preminor/Premajor;Prerelease ID: alpha |
| 稳定版 patch(默认 patch 场景) | 7.1.0-alpha.13 的子集 → 7.0.14 |
走 patch 工作流,cherry-pick 到 main |
| 更早版本的 patch | 子集 → 6.5.14 |
纯手动:找到对应 tag 检出后按紧急发布流程操作 |
| 即将发布的 patch 的预发布 | 7.0.20 → 7.0.21-alpha.0 |
官方文档中"未定义流程",按需处理 |
不带版本升级的 main 合并 |
无 | 所有未挑选的 patch PR 都不可发布时自动发生;patch PR 标题变为 "Merge patches to main (without version bump)" |
其中 Minor/major 场景是特殊的:它目标分支为 latest-release 而非 next-release,因此完成后合并进 main 而不是 next,完整路径是 next → version-non-patch-from-<CURRENT-VERSION-ON_NEXT> → latest-release → main。
3.2 版本号升级的源码级细节
版本计算的真正实现在 version.ts 中。它用 commander 定义 CLI 参数,用 zod 做组合校验:
-R, --release-type <major|minor|patch|prerelease|premajor|preminor|prepatch>:要使用的升级类型(zod enum 限定这 7 种取值);-P, --pre-id <id>:预发布标识符,如alpha、beta、rc。只有premajor/preminor/prepatch/prerelease可以携带 pre-id,否则校验直接报错;-E, --exact <version>:直接指定精确版本(必须是合法 semver),与--release-type互斥,二者必须恰好提供一个;-D, --deferred:不立即改各包的版本,而是把目标版本写入code/package.json#deferredNextVersion;-A, --apply:读取并移除deferredNextVersion,真正执行升级。它与--deferred、--exact、--release-type均互斥;若code/package.json中没有deferredNextVersion会抛错;-V, --verbose:详细日志。
真正执行升级时(version.ts 中的 bumpVersionSources 与 bumpAllPackageJsons),新版本会写入四类位置:
- code/package.json 的
version字段(这也是所有脚本读取"当前版本"的单一来源); - code/core/src/manager-api/version.ts;
- code/core/src/common/versions.ts;
- 所有工作区包的
package.json(通过getCodeWorkspaces枚举),随后执行yarn install --mode=update-lockfile刷新锁文件。
版本号的计算本身委托给 semver.inc(currentVersion, releaseType, preId)。version.test.ts 中的参数化用例精确覆盖了第 3.1 节表格里的每种输入组合,例如:
prerelease,1.1.1-alpha.5→1.1.1-alpha.6;prerelease+pre-id: beta,1.1.1-alpha.10→1.1.1-beta.0(预发布晋级);patch,1.1.1-rc.10→1.1.1(rc 晋级到稳定);preminor+alpha,1.1.1→1.2.0-alpha.0(新 minor 的首个预发布);premajor+alpha,1.1.1→2.0.0-alpha.0;apply:从deferredNextVersion: 1.2.0应用出1.2.0,同时删除该字段。
测试还断言了 --deferred 模式只写一次文件(即仅设置 deferredNextVersion,不触碰其他文件),这解释了 prepare 工作流为何能在"准备"阶段安全地记录目标版本、而把真正的版本号升级推迟到 publish 工作流执行。
4. 发布者操作手册:How to Release
以下步骤同样会写在 Release PR 的描述中,以指导经验不足的 Releaser。高层工作流:
- 找到准备好的 Pull Request
- 冻结 Pull Request
- 对已合并的 PR 做处理(revert、重命名、重新打标)
- 重新触发工作流,让第 3 步的变更生效
- 做必要的手动修改
- 合并
- 确认 "Publish" 工作流成功结束
4.1 步骤一:找到准备好的 Pull Request
按要发布的类型查找对应标题的 PR:
Release: Prerelease|Minor|Major <NEXT-VERSION>—— 来自next的发布(标题由工作流按Release: ${CAPITALIZED_RELEASE_TYPE} ${TITLE_SUFFIX}${NEXT_VERSION}拼出,见 prepare-non-patch-release.yml 的 "Create or update pull request" 步骤);Release: Patch <NEXT-VERSION>—— 补丁发布;Release: Merge patches to main (without version bump)—— 不升版本的补丁合并。
4.2 步骤二:冻结 PR 并运行 CI
给 PR 打上 "freeze" 标签,阻止后续合入 next 时准备工作流再次运行,这样你可以放心修改而不用担心被他人的提交覆盖。由于 is-pr-frozen 检查只在非 workflow_dispatch 事件时生效,"freeze" 并不会取消手动触发的运行。
同时需要加 "ci:daily" 标签触发 CI 运行(会在全量 CI 以及任何变更上重跑)。默认不跑 CI,是为了避免在真正创建新版本之前产生不必要的重复运行。
4.3 步骤三:QA 每一个已合并的 Pull Request
确认发布内容正确,关键检查项:
- 变更是否适合本次版本升级? 例如:minor 预发布里不允许 breaking change;patch 发布里不应混入新 feature。若不适合,revert 该 PR 并通知作者。patch 发布中,若某 PR 带 "patch:yes" 但你不想让它进本次发布(比如对其信心不足、还需维护者更多输入),可先移除该标签并继续发布(记得重新触发工作流),发布完成后再把标签加回去,让它进入下一次发布。
- PR 标题是否正确? PR 标题会进入面向用户的 changelog(源码层面,get-changes.ts 的
getChangelogText直接用 PRtitle生成- ${title} - ${pull}, thanks @${user}!条目),必须准确、可读,遵循[Area]: [Summary]模式——Area 是被改动部分的仓库区域,Summary 是改了什么。容易把 Area 与标签混淆:build标签表示改动是内部实现,但 "build" 不是合适的 Area 写法;Area 可以是 "Core" 或 "CI"。跨多处改动(如升级依赖)时凭最佳判断,原则是:越精确,日后越易读。 - PR 标签是否正确? 标签会决定 PR 是否进入 changelog(见 6.3 节)。
- 补丁:是否已经在某个预发布版中发布过? 若某补丁 PR 还没进过任何预发布版,应先创建一个预发布版再打 patch。这并非技术硬性要求,而是良好实践——确保变更没有先弄坏预发布版就被发到稳定版。
4.4 步骤四:重新触发工作流
对 PR 标题、标签的修改,甚至 revert,都不会反映在 Release PR 中——因为工作流只在向 next 推送时触发,PR 元数据变化不会触发。因此第 3 步只要有改动,就必须手动重新触发工作流来重新生成 changelog 与版本升级。若没做任何改动,可跳过本步。
重要:触发工作流会 force push 到发布分支,所以必须在手动修改(下一步)之前执行,否则手动修改会被覆盖。另外注意:重新触发后,冻结之后合入 next 的新内容也会进入 Release PR,不能假设还是冻结时看到的那份内容。触发时始终选择 next 分支作为 base,除非你非常清楚自己在做什么。
手动触发 non-patch 工作流时可附加输入——Release type 下拉框(prerelease/prepatch/preminor/premajor/patch/minor/major)与 Prerelease ID 文本框,这正是切换版本策略的地方(见 3.1 节):
4.5 步骤五:手动修改
必要时可以合法地直接向发布分支推送手动修改——例如以自动化工具无法完成的方式修改 changelog,或为让发布能进行而做关键变更。这些修改会随发布一起合并进 next/main。但官方建议尽可能使用自动化流程,以保证 GitHub 是唯一事实来源(single source of truth),让 PR 与 changelog 保持同步。
4.6 步骤六:合并
冻结 PR 时已经触发过一次 CI 运行,若其为绿色即可合并。CI 失败时与核心团队协商——这些 Release PR 几乎是 next/main 的精确拷贝,所以正常情况下 CI 结果应与核心分支一致。
4.7 步骤七:等待 Publish 工作流结束
合并 PR 会触发 publish 工作流,完成最终的版本升级与发布。Releaser 有责任确保它成功结束,应关注到结束——失败时会有 Discord 通知,也可以用那个来监控。
5. 特殊发布场景
5.1 向更早的 minor 版本发布
如果要把变更发布到不是最新的旧 minor 版本,必须本地手动完成。以"当前最新是 8.4.0,要发 v8.3.7"为例,完整 18 步流程:
- 检出匹配目标 minor 的最新 tag 并建分支:
git fetch --all --tagsgit checkout tags/v8.3.6 -b patch-8-3-7
- 做需要的变更,通常是从修复分支 cherry-pick;
yarn install;- 用
yarn task --task compile --no-link构建code下的所有包; - 提交并推送;
- 在 CircleCI 上手动触发 daily CI("Trigger Pipeline",Pipeline: "storybook default",Config Source: "storybook",Branch 设为你的分支,添加参数
name=workflow、value=daily); - 等待 CI 成功;
- 升级所有包版本:
cd scripts然后yarn release:version --release-type patch; - 提交:
git commit -m "Bump version from <CURRENT_VERSION> to <NEXT_VERSION> MANUALLY"; - 在
CHANGELOG.md中添加描述变更的新条目; - 提交:
git commit -m "Update CHANGELOG.md with <NEXT_VERSION> MANUALLY"; - 确认对 Storybook npm 包有写权限——需要是
storybookorg 的管理员,且对 org 外的包也有权限(最简单的确认方式是能看到@storybook/react-vite、storybook、sb、create-storybook这些包的 "Settings" 页); - 获取或生成 npm access token(需授予
storybookorg 与上述 org 外包的访问权); - 用
YARN_NPM_AUTH_TOKEN=<NPM_TOKEN> yarn release:publish --tag tag-for-publishing-older-releases --verbose发布所有包; - 到 npm 确认新版本的 dist tag 为
tag-for-publishing-older-releases; - push;
- 手动创建 GitHub Release:新建 tag
v<VERSION>(如v8.3.7),target 为你的分支,previous tag 为v<PREVIOUS_VERSION>(如v8.3.6),标题v<VERSION>,描述为CHANGELOG.md中新增内容,并取消勾选 "Set as the latest release"; - 把 changelog 变更 cherry-pick 到
next,使其真正可见:检出next→ cherry-pick 你的 changelog 提交 → push。
5.2 紧急情况下本地手动发布
自动化可能坏掉,需要一个应急逃生舱:当自动化失效时,可以完全在本地运行整个发布流程,不必创建 PR、也不必把准备与发布拆开一次做完,但仍须遵守正确的分支策略。
两种选择:本地准备 + 自动 publish 工作流,或全流程本地完成。后者需要 npm registry 的 token(设为 YARN_NPM_AUTH_TOKEN)。可以查看各工作流实际执行了什么并照搬,以下是模拟自动化流程的通用步骤,可酌情偏离:
开始前先确保工作树干净:git clean -xdf。
- 从
next或main(patch)创建新分支; - 拉取所有 tag:
git fetch --tags origin; - 安装依赖:
yarn task --task=install --start-from=install; cd scripts;- (若 patch 发布)cherry-pick:
yarn release:pick-patches- 根据上一步输出手动 cherry-pick 需要的补丁;
- 升级版本:
- 若计划用自动 publish(即止步于第 12 步),用 deferred 方式:
yarn release:version --verbose --deferred --release-type <RELEASE_TYPE> --pre-id <PRE_ID> - 若全程本地发布,不要 defer:
yarn release:version --verbose --release-type <RELEASE_TYPE> --pre-id <PRE_ID>
- 若计划用自动 publish(即止步于第 12 步),用 deferred 方式:
- 查看变更列表(作为自己的 to-do):
yarn release:generate-pr-description --current-version <CURRENT_VERSION> --next-version <NEXT_VERSION_FROM_PREVIOUS_STEP> --verbose - 写 changelog:
yarn release:write-changelog <NEXT_VERSION_FROM_PREVIOUS_STEP> --verbose git add .- 提交:
git commit -m "Bump version from <CURRENT_VERSION> to <NEXT_VERSION_FROM_PREVIOUS_STEP> MANUALLY" - 合并进发布分支:
git checkout <"latest-release" | "next-release">git pullgit merge <PREVIOUS_BRANCH>git push origin
- (若自动 publish 仍在工作,此时应已接管,可跳过后续步骤)
cd ..- 发布:
YARN_NPM_AUTH_TOKEN=<NPM_TOKEN> yarn release:publish --tag <"next" OR "latest"> --verbose - (若 patch 发布)
yarn release:label-patches - 手动创建 GitHub Release:tag 为新版本,target 为
latest-release或next-release; - 合并进核心分支:
git checkout <"next"|"main">git pullgit merge <"next-release"|"latest-release">git push origin
- (若 patch 发布)把
CHANGELOG.md同步到next:git checkout nextgit pullgit checkout origin/main ./CHANGELOG.mdgit add ./CHANGELOG.mdgit commit -m "Update CHANGELOG.md for v<NEXT_VERSION>"git push origin
6. Canary 发布与 FAQ
6.1 Canary Releases
任何 PR 都可以在开发过程中被多次发布为 canary 版本。这是在不通过包管理器做项目间 link 的情况下,于独立项目中试用变更的有效方式。
创建 canary 必须由核心团队(或任何拥有管理员权限的人)手动触发 publish 工作流并传入 PR 号。
在为贡献者创建 canary 发布之前,核心团队成员必须确认被发布的代码不是恶意的。
通过 GitHub UI 创建:
- 打开 publish 工作流的运行界面;
- 右上角点击 "Run workflow";
- "branch" 选项始终选择
next,无论 PR 在哪个分支上; - PR 号输入数字,不带前导 #。
通过 CLI 创建——把 <PR_NUMBER> 替换为实际 PR 号:
gh workflow run --repo storybookjs/storybook publish.yml --field pr=<PR_NUMBER>
对照 publish.yml 的 publish-canary job:它会先校验触发者是 admin("Fail if triggering actor is not administrator"),然后检出该 PR 的 HEAD commit,执行 yarn release:version --exact "0.0.0-pr-$PR_NUMBER-sha-$SHORT_SHA" 设置精确版本,再以 yarn release:publish --tag canary 发布。发布成功后会更新 PR 描述中的 "Canary release" 区块,说明版本如何试用(例如 npx storybook@<version> sandbox);失败则会在 PR 上留言并 @ 触发者。
canary 版本号格式为 0.0.0-pr-<PR_NUMBER>-sha-<COMMIT_SHA>,例如 0.0.0-pr-23508-5ec8c1c3。使用 v0.0.0 是为了确保用户在使用允许预发布的 semver 范围(如 ^7.2.0-alpha.0)时不会意外装到 canary。
注意:所有 canary 发布都使用同一个 "canary" dist tag,技术上可以用
npm install @storybook/cli@canary安装,但没有意义——后续 PR 的发布会迅速覆盖该 tag。因此应当始终安装具体版本字符串,如npm install @storybook/cli@0.0.0-pr-23508-sha-5ec8c1c3。
为什么不做更"聪明"的自动 canary?官方文档给出的理由:
- 对所有 PR 自动发 canary 是不安全的:任何拥有 Write 权限的贡献者(200+ 用户)都可以提交恶意 PR 篡改发布脚本(例如发布带挖矿脚本的 patch 版本)。为此,只有受保护分支(
next、main等)上的工作流可以访问持有 npm token 的 "Release" 环境; - 要求 admin 审批工作流运行也不行:这会给核心团队发大量审批通知,哪怕是核心成员自己触发的;
- 按标签或评论触发也不行:工作流无法按标签/评论内容过滤触发,只能在每次打标签/评论时触发再取消,低效。
6.2 FAQ:何时使用 "patch:yes" 标签?
不是所有 PR 都需要回填到稳定版,所以只有带 "patch:yes" 标签的才会被挑选。判断标准:
- 补丁只针对重要且时效性强的修复,不适合小改进或全新 feature;
- 大幅改变代码架构的 PR 理想情况下也不该打回,因为会提高未来合并冲突的概率;
- 拿不准时问核心团队。
6.3 哪些变更算作"可发布"?
一组特定标签定义了 PR 变更的类型及是否"可发布"(releasable)。可发布变更会出现在 changelog 并触发版本升级,不可发布变更不会。这份标签清单硬编码在 get-changes.ts 中:
export const RELEASED_LABELS = {
'BREAKING CHANGE': '❗ Breaking Change',
'feature request': '✨ Feature Request',
bug: '🐛 Bug',
maintenance: '🔧 Maintenance',
dependencies: '📦 Dependencies',
} as const;
export const UNRELEASED_LABELS = {
documentation: '📝 Documentation',
build: '🏗️ Build',
unknown: '❔ Missing Label',
} as const;
即可发布标签为:BREAKING CHANGE、Feature request、Bug、Maintenance、Dependencies;不可发布标签为:Documentation、Build。PR 在发布时若没有上述任何标签,也被视为不可发布。文档类变更不进 npm 包、不改变行为;"build" 类变更(测试、CI 等)纯属内部实现。
"为什么没有 Release PR 被准备?" 通常就是因为 next 上只有不可发布变更,准备工作流自我取消了——不升版本、不写 changelog 的"发布"没有意义。可以在两个 prepare 工作流的运行历史中查看它们是否被 cancel。
6.4 如何修改发布工具/流程?
整个流程基于 .github/workflows/ 下的 GitHub Actions 工作流与 scripts/release/ 脚本,懂行的维护者可以直接修改。简答:"如何改"——把它当作一个普通 PR 来做,并且把该改动也 patch 回 main。
更详细的说明:脚本分别从 main 或 next 运行,所以修改发布脚本时,必须把它 patch 回 main 才能对 patch 发布生效;若需立即生效,还要手动 cherry-pick 到 main。工作流文件通常从 next 运行,但为一致性也建议 patch 回 main;而 "publish" 工作流运行在 latest-release 和 next-release 上,因此改动必须 patch 回去。
6.5 为什么改完标题/标签后要重新触发工作流?
因为工作流只在 next 的 push 上触发,PR 元数据变化(标题、标签、revert)不会触发它。所以改过任何 PR 后必须手动重新触发以重新生成 changelog 与版本升级。你也可以手动改 changelog,但那意味着 PR 及其标题/标签不再是唯一事实来源。
6.6 输入与版本升级的对应关系
每个版本场景及其触发输入都在 3.1 节 的表格中描述;version.test.ts 的参数化用例则给出了每种输入组合对应的精确输出,是判断"哪个输入产生哪个版本"的最终依据。
7. 为什么需要独立的发布分支
更简单的分支方案是:把版本分支直接合并回 main/next,然后在该分支上直接触发发布(Changesets 等工具就是这么做的)。问题在于:你可能发布掉一部分不属于准备好的 Release PR 的变更——它们既没经过 QA,也不在 changelog 里。
例如,Releaser 正用冻结分支做发布,QA 期间另一位成员把 "some-simultaneous-bugfix" 合入了 next:
%%{init: { 'gitGraph': { 'mainBranchName': 'next' } } }%%
gitGraph
commit type: HIGHLIGHT
branch new-feature
commit
commit
checkout next
merge new-feature type: HIGHLIGHT
branch some-simultaneous-bugfix
commit
checkout next
branch version-non-patch-from-7.1.0-alpha.28
commit id
checkout next
merge some-simultaneous-bugfix type: HIGHLIGHT id: "whoops!"
merge version-non-patch-from-7.1.0-alpha.28 tag: "v7.1.0-alpha.29"
如果在 tag v7.1.0-alpha.29 的最后提交上发布,发出去的是那一刻的全部内容(所有方点),包括 QA 途中合入的 "whoops!" bugfix——而它从未属于这个 Release PR(PR 是在 bugfix 合并前准备的)。
相反,从 next-release 发布再合并回 next,该 bugfix 就不会进入本次发布,而是进入下一次:
%%{init: { 'gitGraph': { 'mainBranchName': 'next' } } }%%
gitGraph
commit type: HIGHLIGHT
branch next-release
branch new-feature
commit
commit
checkout next
merge new-feature type: HIGHLIGHT
branch some-simultanous-bugfix
commit
checkout next
branch version-non-patch-from-7.1.0-alpha.28
commit id: "write changelog"
checkout next
merge some-simultanous-bugfix id: "whoops!"
checkout next-release
merge version-non-patch-from-7.1.0-alpha.28
commit id: "bump versions" tag: "v7.1.0-alpha.29"
checkout next
merge next-release
branch version-non-patch-from-7.1.0-alpha.29
commit id: "write changelog again"
checkout next-release
merge version-non-patch-from-7.1.0-alpha.29
commit id: "bump versions again" tag: "v7.1.0-alpha.30"
checkout next
merge next-release
这背后的原理是:"未发布变更"的判定方式是——列出 HEAD 历史中的所有 commit,减去最新版本 tag 历史中的 commit。由于 bugfix 不在上一个版本的历史中,它会被计入下一次发布的变更列表,从而自然进入 v7.1.0-alpha.30。
8. 小结
Storybook 的发布体系可以用一句话概括:"next/main 承载开发,next-release/latest-release 承载发布,Release PR 是人与自动化之间的契约"。其设计亮点在于:
- 用可再生成的 Release PR(force push 覆盖 + freeze 标签保护)保证 changelog 与 PR 元数据始终同步,GitHub 是唯一事实来源;
- 用延迟版本升级(
deferredNextVersion)把"准备"与"发布"两个阶段解耦,准备工作流不触碰各包版本,publish 工作流合并后才真正 bump 并发包; - 用标签体系(patch:yes / freeze / ci:daily / patch:done 与可发布标签)驱动整个自动化决策;
- 用发布分支间接层杜绝"发布内容与 QA 内容不一致"的竞态;
- 保留本地手动发布与 canary 两条逃生通道,兼顾紧急修复与开发中的快速试用。
相关实现与文档入口:发布规范、发布脚本集、prepare-non-patch-release.yml、prepare-patch-release.yml、publish.yml。
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 StartedRust0624
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
