首页
/ 深入解析 Storybook 的自动化发布体系:分支策略、Release PR 与版本管理全流程

深入解析 Storybook 的自动化发布体系:分支策略、Release PR 与版本管理全流程

2026-09-06 23:47:12作者:尤峻淳Whitney

本文以 Storybook 官方的发布规范 RELEASING.md 为主体,完整拆解该 monorepo 中"非补丁发布"与"补丁发布"两条发布流水线的运作原理、发布者的标准操作步骤、各类版本升级场景的输入组合,以及紧急情况下手动发布的完整操作序列。读完本文,你将能够理解 Storybook 是如何用 GitHub Actions 与 NodeJS 脚本把"changelog 生成、版本号升级、npm 发包、GitHub Release 创建"这条复杂链路自动化起来的,并能独立复述其分支策略与 canary 发布机制。

1. 发布体系总览

Storybook 的发布过程分为两大类:

  1. 非补丁发布(Non-patch releases):发布 next 分支上的任何内容,既可以是预发布版(prerelease),也可以是正式版(stable);
  2. 补丁发布(Patch releases):从 next 挑选需要打回当前稳定小版本(main)的内容。

整个流程建立在自动创建的 "Release Pull Requests" 之上:当这些 PR 被合并时,就会触发一个新版本的发布。由核心团队成员轮值担任的"发布者"(Releaser)会在当前 Release PR 中执行发布流程。该流程由三部分实现:

所有 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。这样做的目的是:变更先在预发布版中被验证,再进入稳定版;
  • 真正的(预)发布不直接发生在 nextmain 上,而是分别发生在 next-releaselatest-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 创建新版本的"接口"。高层流程为:

  1. 当一个 PR 合入 next(或向 next 推送提交)时,两个 Release PR 都会被(重新)生成;
  2. 它们创建一个新分支——version-(patch|non-patch)-from-<CURRENT-VERSION>
  3. 按版本策略计算要升级到的版本号;
  4. 用检测到的全部变更更新 CHANGELOG(.prerelease).md
  5. 提交所有内容;
  6. 强制推送(force push)
  7. next-releaselatest-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)

工作流:prepare-patch-release.yml

补丁发布通过 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-releasemain。这一点在 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 在发布过程中手动挑选。当 mainnext 分歧越大(即距上一次稳定 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)与 nextHEAD 之间所有的 commit 和 PR。这一逻辑对应 unreleased-changes-exists.tsget-changes.ts:后者以 --first-parent 拉取 from(默认是最新版本 tag 对应 commit)到 to(默认 HEAD)之间的提交,再逐一反查其关联 PR 的标题与标签。

默认的版本策略是递增当前预发布编号(见 3.1 节)。如果当前没有预发布编号(即刚发布了一个稳定的 minor/major 版本),默认策略会走一次 patch bump,例如从 7.2.07.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 时,发布工作流被触发。它依次执行以下任务:

  1. 按已准备好的 PR 中的计划,为所有包升级版本号;
  2. 安装依赖并构建所有包;
  3. 将包发布到 npm;
  4. (若是补丁发布)给所有相关 PR 打上 "patch:done" 标签;
  5. 在发布分支(latest-releasenext-release)上创建版本 tag,并新建 GitHub Release;
  6. 把发布分支合并进核心分支(mainnext);
  7. (若是补丁发布)把 CHANGELOG.md 的变更从 main 同步到 next

出于安全考虑,该工作流运行在名为 "Release" 的 GitHub environment 中(publish.ymlenvironment: Release),持有发布到 @storybook npm 组织所需 token,且只能从四个"核心"分支访问:mainnextlatest-releasenext-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 回 nextmain;对于从 next-release 发布的正式版(非 prerelease),还会把 next force push 到 latest-releasemain,保证三个分支内容一致;
  • "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.127.1.0-alpha.13 无,直接触发即可
预发布晋级 7.1.0-alpha.137.1.0-beta.0 Release type: Prerelease;Prerelease ID: beta
Minor/Major 正式版 7.1.0-rc.27.1.08.0.0-rc.38.0.0 Release type: Patch/Minor/Major;Prerelease ID: 留空
新 major/minor 的首个预发布 7.1.07.2.0-alpha.08.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.207.0.21-alpha.0 官方文档中"未定义流程",按需处理
不带版本升级的 main 合并 所有未挑选的 patch PR 都不可发布时自动发生;patch PR 标题变为 "Merge patches to main (without version bump)"

其中 Minor/major 场景是特殊的:它目标分支为 latest-release 而非 next-release,因此完成后合并进 main 而不是 next,完整路径是 nextversion-non-patch-from-<CURRENT-VERSION-ON_NEXT>latest-releasemain

3.2 版本号升级的源码级细节

版本计算的真正实现在 version.ts 中。它用 commander 定义 CLI 参数,用 zod 做组合校验:

  • -R, --release-type <major|minor|patch|prerelease|premajor|preminor|prepatch>:要使用的升级类型(zod enum 限定这 7 种取值);
  • -P, --pre-id <id>:预发布标识符,如 alphabetarc只有 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 中的 bumpVersionSourcesbumpAllPackageJsons),新版本会写入四类位置:

  1. code/package.jsonversion 字段(这也是所有脚本读取"当前版本"的单一来源);
  2. code/core/src/manager-api/version.ts
  3. code/core/src/common/versions.ts
  4. 所有工作区包的 package.json(通过 getCodeWorkspaces 枚举),随后执行 yarn install --mode=update-lockfile 刷新锁文件。

版本号的计算本身委托给 semver.inc(currentVersion, releaseType, preId)version.test.ts 中的参数化用例精确覆盖了第 3.1 节表格里的每种输入组合,例如:

  • prerelease1.1.1-alpha.51.1.1-alpha.6
  • prerelease + pre-id: beta1.1.1-alpha.101.1.1-beta.0(预发布晋级);
  • patch1.1.1-rc.101.1.1(rc 晋级到稳定);
  • preminor + alpha1.1.11.2.0-alpha.0(新 minor 的首个预发布);
  • premajor + alpha1.1.12.0.0-alpha.0
  • apply:从 deferredNextVersion: 1.2.0 应用出 1.2.0,同时删除该字段。

测试还断言了 --deferred 模式只写一次文件(即仅设置 deferredNextVersion,不触碰其他文件),这解释了 prepare 工作流为何能在"准备"阶段安全地记录目标版本、而把真正的版本号升级推迟到 publish 工作流执行。

4. 发布者操作手册:How to Release

以下步骤同样会写在 Release PR 的描述中,以指导经验不足的 Releaser。高层工作流:

  1. 找到准备好的 Pull Request
  2. 冻结 Pull Request
  3. 对已合并的 PR 做处理(revert、重命名、重新打标)
  4. 重新触发工作流,让第 3 步的变更生效
  5. 做必要的手动修改
  6. 合并
  7. 确认 "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

确认发布内容正确,关键检查项:

  1. 变更是否适合本次版本升级? 例如:minor 预发布里不允许 breaking change;patch 发布里不应混入新 feature。若不适合,revert 该 PR 并通知作者。patch 发布中,若某 PR 带 "patch:yes" 但你不想让它进本次发布(比如对其信心不足、还需维护者更多输入),可先移除该标签并继续发布(记得重新触发工作流),发布完成后再把标签加回去,让它进入下一次发布。
  2. PR 标题是否正确? PR 标题会进入面向用户的 changelog(源码层面,get-changes.tsgetChangelogText 直接用 PR title 生成 - ${title} - ${pull}, thanks @${user}! 条目),必须准确、可读,遵循 [Area]: [Summary] 模式——Area 是被改动部分的仓库区域,Summary 是改了什么。容易把 Area 与标签混淆:build 标签表示改动是内部实现,但 "build" 是合适的 Area 写法;Area 可以是 "Core" 或 "CI"。跨多处改动(如升级依赖)时凭最佳判断,原则是:越精确,日后越易读。
  3. PR 标签是否正确? 标签会决定 PR 是否进入 changelog(见 6.3 节)。
  4. 补丁:是否已经在某个预发布版中发布过? 若某补丁 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 节):

手动触发非补丁发布工作流时的输入表单:release-type 选择器与 prerelease identifier 文本框

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 步流程:

  1. 检出匹配目标 minor 的最新 tag 并建分支:
    1. git fetch --all --tags
    2. git checkout tags/v8.3.6 -b patch-8-3-7
  2. 做需要的变更,通常是从修复分支 cherry-pick;
  3. yarn install
  4. yarn task --task compile --no-link 构建 code 下的所有包;
  5. 提交并推送;
  6. 在 CircleCI 上手动触发 daily CI("Trigger Pipeline",Pipeline: "storybook default",Config Source: "storybook",Branch 设为你的分支,添加参数 name=workflowvalue=daily);
  7. 等待 CI 成功;
  8. 升级所有包版本:cd scripts 然后 yarn release:version --release-type patch
  9. 提交:git commit -m "Bump version from <CURRENT_VERSION> to <NEXT_VERSION> MANUALLY"
  10. CHANGELOG.md 中添加描述变更的新条目;
  11. 提交:git commit -m "Update CHANGELOG.md with <NEXT_VERSION> MANUALLY"
  12. 确认对 Storybook npm 包有写权限——需要是 storybook org 的管理员,且对 org 外的包也有权限(最简单的确认方式是能看到 @storybook/react-vitestorybooksbcreate-storybook 这些包的 "Settings" 页);
  13. 获取或生成 npm access token(需授予 storybook org 与上述 org 外包的访问权);
  14. YARN_NPM_AUTH_TOKEN=<NPM_TOKEN> yarn release:publish --tag tag-for-publishing-older-releases --verbose 发布所有包;
  15. 到 npm 确认新版本的 dist tag 为 tag-for-publishing-older-releases
  16. push;
  17. 手动创建 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";
  18. 把 changelog 变更 cherry-pick 到 next,使其真正可见:检出 next → cherry-pick 你的 changelog 提交 → push。

5.2 紧急情况下本地手动发布

自动化可能坏掉,需要一个应急逃生舱:当自动化失效时,可以完全在本地运行整个发布流程,不必创建 PR、也不必把准备与发布拆开一次做完,但仍须遵守正确的分支策略。

两种选择:本地准备 + 自动 publish 工作流,或全流程本地完成。后者需要 npm registry 的 token(设为 YARN_NPM_AUTH_TOKEN)。可以查看各工作流实际执行了什么并照搬,以下是模拟自动化流程的通用步骤,可酌情偏离:

开始前先确保工作树干净:git clean -xdf

  1. nextmain(patch)创建新分支;
  2. 拉取所有 tag:git fetch --tags origin
  3. 安装依赖:yarn task --task=install --start-from=install
  4. cd scripts
  5. (若 patch 发布)cherry-pick:
    1. yarn release:pick-patches
    2. 根据上一步输出手动 cherry-pick 需要的补丁;
  6. 升级版本:
    1. 若计划用自动 publish(即止步于第 12 步),用 deferred 方式:yarn release:version --verbose --deferred --release-type <RELEASE_TYPE> --pre-id <PRE_ID>
    2. 若全程本地发布,不要 defer:yarn release:version --verbose --release-type <RELEASE_TYPE> --pre-id <PRE_ID>
  7. 查看变更列表(作为自己的 to-do):yarn release:generate-pr-description --current-version <CURRENT_VERSION> --next-version <NEXT_VERSION_FROM_PREVIOUS_STEP> --verbose
  8. 写 changelog:yarn release:write-changelog <NEXT_VERSION_FROM_PREVIOUS_STEP> --verbose
  9. git add .
  10. 提交:git commit -m "Bump version from <CURRENT_VERSION> to <NEXT_VERSION_FROM_PREVIOUS_STEP> MANUALLY"
  11. 合并进发布分支:
    1. git checkout <"latest-release" | "next-release">
    2. git pull
    3. git merge <PREVIOUS_BRANCH>
    4. git push origin
  12. (若自动 publish 仍在工作,此时应已接管,可跳过后续步骤)
  13. cd ..
  14. 发布:YARN_NPM_AUTH_TOKEN=<NPM_TOKEN> yarn release:publish --tag <"next" OR "latest"> --verbose
  15. (若 patch 发布)yarn release:label-patches
  16. 手动创建 GitHub Release:tag 为新版本,target 为 latest-releasenext-release
  17. 合并进核心分支:
    1. git checkout <"next"|"main">
    2. git pull
    3. git merge <"next-release"|"latest-release">
    4. git push origin
  18. (若 patch 发布)把 CHANGELOG.md 同步到 next
    1. git checkout next
    2. git pull
    3. git checkout origin/main ./CHANGELOG.md
    4. git add ./CHANGELOG.md
    5. git commit -m "Update CHANGELOG.md for v<NEXT_VERSION>"
    6. git push origin

6. Canary 发布与 FAQ

6.1 Canary Releases

任何 PR 都可以在开发过程中被多次发布为 canary 版本。这是在不通过包管理器做项目间 link 的情况下,于独立项目中试用变更的有效方式。

创建 canary 必须由核心团队(或任何拥有管理员权限的人)手动触发 publish 工作流并传入 PR 号。

在为贡献者创建 canary 发布之前,核心团队成员必须确认被发布的代码不是恶意的。

通过 GitHub UI 创建:

  1. 打开 publish 工作流的运行界面;
  2. 右上角点击 "Run workflow";
  3. "branch" 选项始终选择 next,无论 PR 在哪个分支上;
  4. PR 号输入数字,不带前导 #

通过 CLI 创建——把 <PR_NUMBER> 替换为实际 PR 号:

gh workflow run --repo storybookjs/storybook publish.yml --field pr=<PR_NUMBER>

对照 publish.ymlpublish-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 版本)。为此,只有受保护分支(nextmain 等)上的工作流可以访问持有 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

更详细的说明:脚本分别从 mainnext 运行,所以修改发布脚本时,必须把它 patch 回 main 才能对 patch 发布生效;若需立即生效,还要手动 cherry-pick 到 main。工作流文件通常从 next 运行,但为一致性也建议 patch 回 main;而 "publish" 工作流运行在 latest-releasenext-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.ymlprepare-patch-release.ymlpublish.yml

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