shadcn/ui 单仓库发布体系详解:Changesets 独立版本管理、快照预发布与 pre 模式的完整工作流
本文围绕仓库根目录的 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.yaml 与 turbo.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.json 中 packageManager 字段一致)。
第 3 步:按 PR 的快照预发布——release: beta / release: rc 标签驱动
对于希望「在合并前让他人试用本 PR 改动」的场景,仓库提供了逐 PR 的快照预发布:给 PR 加上 release: beta 或 release: rc 标签即可。整条链路同样由 release.yml 的 prerelease 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、且恰好命中一个预发布标签(脚本层还校验了「有且仅有一个」标签,二者同时加会直接抛错)。具体执行五步:
-
选择渠道:一个内联
github-script步骤把标签映射为渠道名——release: beta→beta,release: rc→rc,并把渠道名与标签名写入 step 输出供后续步骤引用。 -
校验分支上存在 changeset:工作流内嵌一段 shell 逻辑扫描
.changeset/*.md(排除 README.md),无 changeset 时仅输出一条::notice::后终止。这一步至关重要:Version Packages PR 上打标签是 no-op,因为该 PR 已经消费掉了 changeset,扫描结果为空,流水线自然空转。 -
打快照版本号:
pnpm exec changeset version --snapshot beta # 或 rc为每个携带 changeset 的包盖上一个唯一的
0.0.0-<channel>-<timestamp>版本号。 -
构建并发布:
pnpm build:packages之后执行pnpm exec changeset publish --tag beta --no-git-tag--tag <channel>把快照发进对应 dist-tag,--no-git-tag则避免在 PR 分支上留下 git tag。 -
收集产物并上传工件:执行 .github/collect-prerelease-info.js,把 PR 号、渠道、以及本次实际发布出去的包清单写入
prerelease-info.json并上传为prerelease-infoartifact。该脚本的识别策略很务实——遍历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.0、1.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 末尾还记录了两条容易踩坑的运维细节:
-
新发布的版本无法立即被普通安装解析。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.4 节所述,整条发布链使用 npm OIDC(
id-token: write+ 全局升级到npm@latest)实现带 provenance 的无 token 发布,仓库中无需维护任何NPM_TOKENsecret,凭据轮换与泄露面都显著收窄。
参考路径速查
| 文件 | 作用 |
|---|---|
| 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 事实来源,互不污染。
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 StartedRust0623
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