首页
/ shadcn/ui 单仓库发布体系详解:Changesets 独立版本管理、快照预发布与 pre 模式的完整工作流

shadcn/ui 单仓库发布体系详解:Changesets 独立版本管理、快照预发布与 pre 模式的完整工作流

2026-09-03 15:35:20作者:瞿蔚英Wynne

本文围绕仓库根目录的 RELEASING.md 展开,完整还原 shadcn/ui monorepo 的发布体系:三个 npm 包如何各自独立走版本线,稳定的「Version Packages PR」自动发布流程如何运作,release: beta / release: rc 标签触发的按 PR 快照预发布如何打标与回写评论,以及 pre 模式下持续 -beta.N / -rc.N 火车式预发布的进出方式。读完后,你能基于 release.yml 与 Changesets 配置,在同类 monorepo 中复刻一套无 token、带 OIDC 溯源、预发布不打 latest 的完整发布流水线。

仓库的发布拓扑:三个包、三条独立版本线

shadcn/ui 是一个 pnpm + Turborepo 管理的 monorepo(见 pnpm-workspace.yamlturbo.json),它通过 Changesets 独立发布三个 npm 包:

包名 位置 定位
shadcn packages/shadcn/package.json CLI 与工具链(当前仓库内版本 4.19.0)
@shadcn/react packages/react/package.json 无样式的 headless React 原语(0.3.0)
@shadcn/helpers packages/helpers/package.json 应用开发辅助小工具(0.2.0)

三者各自独立走版本线:一个包的变更永远不会连带提升其他包的版本,除非 changeset 明确要求。这一策略在 RELEASING.md 中被明确声明,并与 .changeset/config.json 中的配置相互印证——fixed: []linked: [] 都为空数组,意味着没有任何一组包被锁定为联动升版;同时 "ignore": ["v4", "tests"] 把演示站点 apps/v4 与测试目录排除在发布之外,"baseBranch": "main" 声明 main 是版本事实的来源分支。

三个可发包均声明了 "publishConfig": { "access": "public" },与 changesets 配置中的 "access": "public" 保持一致。@shadcn/react 的版本历史可直接查看 packages/react/CHANGELOG.md,其中每个条目都带 PR 链接与 commit 哈希——这正是下面要讲的 changelog 自动生成机制的产物。

第 1 步:为每次应发布的变更添加 changeset

所有希望进入 npm 的变更都需要一个 changeset。在本地执行:

pnpm changeset

交互界面会让你选择受影响的包并指定 bump 级别(patch / minor / major)。关键规则:

  • 一个 PR 可以为不同包携带不同级别的多个 changeset,互不干扰;
  • 没有 changeset 的 PR 什么都不会发布——这是整套体系里最重要的护栏:发布范围完全由 .changeset/ 目录下积累的 markdown 文件决定,而不是由 CI 或人工判断。

changeset 文件本质上是存放在 .changeset/ 目录下的 markdown 文件(.changeset/README.md 除外)。这一约定也是后续 CI 判断「分支上有没有可发布内容」的依据,下文预发布流水线中会看到对应的检查逻辑。

第 2 步:稳定版发布——push 到 main 触发的全自动链路

稳定版发布完全由 release.yml 中的 release job 自动化,触发条件是 push 到 main。完整链路分为四段:

2.1 Changeset 在 main 上积累

被合并的 PR 所携带的 changeset 文件随之落到 main 分支,形成待发布的版本增量。

2.2 Changesets Action 开启/更新「Version Packages」PR

release job 的核心一步使用了 changesets/action@v1,其关键参数为:

- name: Create Version PR or Publish to NPM
  id: changesets
  uses: changesets/action@v1
  with:
    setupGitUser: false
    commit: "chore(release): version packages"
    title: "chore(release): version packages"
    version: node .github/changeset-version.js
    publish: npx changeset publish
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    NODE_ENV: "production"

行为上,该 action 会检测 main 上是否有未消费的 changeset:有,就开启(或更新)一个 "Version Packages" PR,把各包 package.json 版本号抬升并写入 changelog;没有,则直接走发布分支。

值得注意的是 version 步骤并没有直接用裸 changeset version,而是执行了一个自定义脚本 .github/changeset-version.js

execSync("npx changeset version", { stdio: "inherit" })
execSync("pnpm install --lockfile-only", { stdio: "inherit" })

脚本注释解释了动机:changeset version 本身不会更新 pnpm-lock.yaml,所以追加一步 pnpm install --lockfile-only 把工作区内包版本提升同步进 lockfile(该脚本注释同时标明其源自 Cloudflare wrangler 的同类做法,并链接了 Changesets 的对应 issue 作为待解决项)。这是一个典型的「Changesets 标准流程 + 仓库特定补丁」组合,值得在同类 monorepo 中借鉴。

2.3 合并 Version PR 触发 changeset publish

当 Version PR 被合并回 main 后,push 事件再次触发 release job,此时没有未消费 changeset,changesets/action 直接进入 publish: npx changeset publish 分支:构建全部包并发布版本领先于 npm 上的包,全部打上 latest 标签。发布前有一步构建:

pnpm build:packages

该命令定义在根 package.json 中,等价于 turbo run build --filter=./packages/*——从源码结构看,它只构建 packages/ 下的三个可发包,绝不触碰 apps/v4(演示站点不参与 npm 发布),保证每个包 dist/ 都是发布前新鲜构建的产物。

2.4 签名与凭据:OIDC 替代 NPM_TOKEN

release job 在调用 changesets 之前先导入 GPG 密钥,对 commit 与 tag 做签名:

- name: Import GPG key
  uses: crazy-max/ghaction-import-gpg@v6
  with:
    gpg_private_key: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }}
    git_user_signingkey: true
    git_commit_gpgsign: true
    git_tag_gpgsign: true

凭据模型上,工作流声明的是 permissions: id-token: write,并在安装依赖前执行 npm install -g npm@latest 以启用 OIDC 支持(见 release.yml 中 "Update npm for OIDC support" 步骤)。这意味着发布走 npm OIDC / provenance,不需要任何 NPM_TOKEN secret——npm 直接信任 GitHub Actions 签发的身份令牌,产物自动携带 provenance。运行环境固定为 Node.js 24 + pnpm 10.33.4(与根 package.jsonpackageManager 字段一致)。

第 3 步:按 PR 的快照预发布——release: beta / release: rc 标签驱动

对于希望「在合并前让他人试用本 PR 改动」的场景,仓库提供了逐 PR 的快照预发布:给 PR 加上 release: betarelease: rc 标签即可。整条链路同样由 release.ymlprerelease job 承担,其触发条件写得很严格:

if: "${{ github.event_name == 'pull_request' && github.repository_owner == 'shadcn-ui' && (contains(github.event.pull_request.labels.*.name, 'release: beta') || contains(github.event.pull_request.labels.*.name, 'release: rc')) }}"

pull_request 事件且 labeled 类型、仓库所有者为 shadcn-ui、且恰好命中一个预发布标签(脚本层还校验了「有且仅有一个」标签,二者同时加会直接抛错)。具体执行五步:

  1. 选择渠道:一个内联 github-script 步骤把标签映射为渠道名——release: betabetarelease: rcrc,并把渠道名与标签名写入 step 输出供后续步骤引用。

  2. 校验分支上存在 changeset:工作流内嵌一段 shell 逻辑扫描 .changeset/*.md(排除 README.md),无 changeset 时仅输出一条 ::notice:: 后终止。这一步至关重要:Version Packages PR 上打标签是 no-op,因为该 PR 已经消费掉了 changeset,扫描结果为空,流水线自然空转。

  3. 打快照版本号

    pnpm exec changeset version --snapshot beta   # 或 rc
    

    为每个携带 changeset 的包盖上一个唯一的 0.0.0-<channel>-<timestamp> 版本号。

  4. 构建并发布pnpm build:packages 之后执行

    pnpm exec changeset publish --tag beta --no-git-tag
    

    --tag <channel> 把快照发进对应 dist-tag,--no-git-tag 则避免在 PR 分支上留下 git tag。

  5. 收集产物并上传工件:执行 .github/collect-prerelease-info.js,把 PR 号、渠道、以及本次实际发布出去的包清单写入 prerelease-info.json 并上传为 prerelease-info artifact。该脚本的识别策略很务实——遍历 packages/*/package.json,凡非 private 且版本号包含 -${channel} 标记的,即认定为本轮被版本化的包(快照版本的渠道标记正是识别依据)。

评论回写与标签清理

发布成功后,prerelease-comment.yml 通过 workflow_run 事件监听 Release 工作流的 completed 状态(且结论为 success、事件源为 pull_request),下载 prerelease-info 工件后:

  • 为每个包拼出一行安装命令,形如:

    pnpm dlx @shadcn/react@0.0.0-beta-20260624120000
    
  • marocchino/sticky-pull-request-comment 以 sticky 评论形式发到对应 PR(同一 PR 多次预发布时是更新而非刷屏);

  • 最后调 API 移除 release: beta / release: rc 标签,完成一次完整的「打标 → 发布 → 回报」闭环。

设计要点:标签选渠道,changeset 选包

这套机制里有两个正交维度:标签决定 dist-tag / 渠道分支上的 changesets 决定哪些包被发布。由于快照版本带时间戳,每次发布的版本号都独一无二——既永远不会触碰 latest,也不会与任何真实发布发生版本冲突,因此同一 PR 上可以反复打标、反复试装。

第 4 步:预发布火车——用 pre 模式跑持续的 -beta.N / -rc.N 序列

按 PR 的快照是「用完即弃」的。当你需要一条持续酝酿的发布线(例如正式的 1.0.0-rc.01.0.0-rc.1…… 逐轮收集反馈),应改用 Changesets 的 pre 模式

pnpm changeset pre enter rc   # 写入 .changeset/pre.json
# ...此后正常的 changeset + Version PR 循环会在 rc tag 上产出 -rc.N 版本...
pnpm changeset pre exit       # 回到 stable;下一个 Version PR 会以 X.Y.Z 发布到 latest

pre enter 会生成 .changeset/pre.json 记录当前所处的预发布渠道与基线版本;此后每一个 Version Packages PR 合并,产出的都是递增的 -rc.N 预发布版本并发布到 rc dist-tag,而不是 latest。当预发布线成熟,pre exit 退出该模式,下一个 Version PR 即以常规 X.Y.Z 版本落到 latest,无缝接回稳定通道。

关键注意事项:48 小时 minimumReleaseAge 与 OIDC 溯源

RELEASING.md 末尾还记录了两条容易踩坑的运维细节:

  1. 新发布的版本无法立即被普通安装解析pnpm-workspace.yaml 中配置了:

    minimumReleaseAge: 2880
    

    即 48 小时——刚发布到 npm 的稳定版/预发布版本,在普通 pnpm install 的解析链路中要等满这个窗口期才可被选中(这是 pnpm 抵御被劫持/恶意抢注新版本包的供应链防护策略)。文件里同时列出了 minimumReleaseAgeExclude@tanstack/react-table 等四个 TanStack 包,并附了删除该例外的 TODO 注释)。因此文档给出的即时验证手段是直接按精确快照版本试装

    pnpm dlx @shadcn/react@0.0.0-beta-20260624120000
    
  2. 发布凭据不落地。如 2.4 节所述,整条发布链使用 npm OIDC(id-token: write + 全局升级到 npm@latest)实现带 provenance 的无 token 发布,仓库中无需维护任何 NPM_TOKEN secret,凭据轮换与泄露面都显著收窄。

参考路径速查

文件 作用
RELEASING.md 发布流程总纲(本文主体依据)
.github/workflows/release.yml 稳定版 Version PR / 发布 + 按 PR 快照预发布的完整 CI 定义
.github/workflows/prerelease-comment.yml 预发布成功后的 PR 评论回写与标签移除
.github/changeset-version.js Version PR 步骤:changeset version + lockfile 同步
.github/collect-prerelease-info.js 汇总本轮快照发布包清单
.changeset/config.json Changesets 策略:独立版本线、忽略 v4/tests、GitHub 风格 changelog
pnpm-workspace.yaml minimumReleaseAge: 2880 及其例外清单
package.json build:packages 等根脚本、pnpm 版本锁定
packages/react/CHANGELOG.md 自动生成 changelog 的实际产物示例

从这套体系可以看到清晰的职责划分:开发者只负责写 changeset,CI 负责版本计算、构建、签名、发布与回报;稳定通道走「积累 → Version PR → 合并即发布」,实验通道走「标签 → 时间戳快照 → PR 评论」,而 pre 模式则覆盖了介于两者之间的「成系列预发布」需求,三条通道共享同一套 changeset 事实来源,互不污染。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384