React Router 发布流程全解:change 文件、版本脚本与 GitHub Actions 自动发布流水线
React Router 的版本发布几乎完全由自动化流水线完成:开发者只需把描述变更的 change 文件随 PR 合入 main,后续的分支管理、版本号更新、CHANGELOG 生成、npm 发布和 GitHub Release 创建全部由脚本与 GitHub Actions 接手。本篇基于仓库中的 DEVELOPMENT.md 与 scripts/changes/ 下的 TypeScript 脚本、.github/workflows/release.yml,讲清楚这条“change 文件驱动”的发布链路是如何工作的,帮助你在该仓库(或参考其模式搭建类似多包发布系统)中理解从一次普通发版到 hotfix 热修复的完整操作路径。
发布系统的两个组成部分
DEVELOPMENT.md 对发布流程的总述是:
Releases are handled by a mostly automated process consisting of:
- TypeScript scripts in
scripts/changes/- The
release.ymlGithub workflow
落到仓库中,这两个组成部分分别是:
scripts/changes/目录下的 TypeScript 脚本,各自对应发布链路的一个环节:- add.ts:交互式创建 change 文件(对应根
package.json中的changes:add脚本); - changes.ts:change 文件的解析、校验与 changelog 内容生成的核心库;
- version.ts:更新版本号、生成 CHANGELOG、删除 change 文件并创建 release 提交(
changes:version); - pr.ts:创建或更新版本化的 release PR(
changes:pr); - publish.ts:发布所有包到 npm、打 tag、创建 GitHub Release(
changes:publish); - validate.ts:单独校验所有 change 文件与包级 CHANGELOG 是否齐备(
changes:validate)。
- add.ts:交互式创建 change 文件(对应根
- .github/workflows/release.yml 工作流。值得注意的是,该文件头部注释解释了为什么所有发布逻辑集中在这一个文件里——npm 的 Trusted Publishing 配置只允许指定单个 GitHub workflow 文件,因此稳定版发布与实验性(experimental)发布共用这一个文件,靠不同的触发条件区分对应 job。
change 文件:一切发布的起点
整个流程的“输入”是每个包目录下 .changes/ 目录里的 Markdown 文件。以仓库中真实存在的 change 文件 packages/react-router/.changes/patch.submit-relative-option.md 为例,其内容只有一句话:
Properly respect the `relative` option in `useSubmit`/`fetcher.submit` when resolivng the `action` path
文件名与内容遵循严格的约定,这些规则由 changes.ts 中的 parsePackageChanges 函数实现并强制校验:
- 文件名格式为
<bump>.<描述>.md,bump必须是major、minor、patch或unstable四种之一(unstable会在后续被规范化为 semver 递增,见 changes.ts#L16-L36)。例如minor.add-feature.md; - 文件不能为空,且首行不能以
-或*开头的 bullet 符号——因为生成 CHANGELOG 时项目符号会自动补上; - 正文标题只允许 4~6 级(
####到######),因为 change 文件最终嵌套在已经使用 1~3 级标题的 CHANGELOG 中; - 破坏性变更(以 "BREAKING CHANGE" 前缀标注)必须搭配
major.前缀,否则校验报错; - 每个包的
.changes/目录必须存在且包含.gitkeep(例如 packages/react-router/.changes/.gitkeep),以便该目录在 change 文件被清理后依然保留在仓库里。
创建 change 文件不需要手写文件名:运行 pnpm changes:add(即 add.ts),脚本会用 prompts 交互地让你选择包、选择 bump 类型,并根据描述自动生成去掉英文停用词后的 slug 文件名。另外,仓库还有 change-file-check.yml 工作流,在 PR 层面检查变更是否附带了 change 文件。
常规发布流程(main 分支)
DEVELOPMENT.md 描述的常规发布步骤如下(原文骨架):
- 把所有待发布的改动连同 change 文件合并到
main; release.yml发现main分支存在 change 文件时:- 触发 “PR” 步骤,运行 scripts/changes/pr.ts;
- 从
main创建或更新版本化发布分支,如release-v7-pr; - 运行 scripts/changes/version.ts:更新版本、生成 changelog、删除 change 文件;
- 从
release-v<major>-pr向main打开一个 PR;
- 该 PR 合并后,
release.yml再次针对main运行,此时发现没有 change 文件:- 触发
needs-publish检查,查询 npm 判断当前react-router版本是否已发布; - 若需要发布,触发 “publish” 步骤,运行 scripts/changes/publish.ts:发布所有包到 npm、打 tag 并推送、创建 GitHub Release;
- 触发
release-v<major>-pr分支在 PR 合并后可以删除。
结合 release.yml 的 job 编排,可以看清上述每一步的实际落点。工作流由 push 到 main/hotfix/v7 分支触发(另有 workflow_dispatch 用于实验性发布),并且设置了 concurrency 组且 cancel-in-progress: false——注释里解释了原因:发布流程一旦开始就要完整跑完,后续提交(比如格式修正)不应打断发布与评论动作。
四个 job 的分工:
check:先用find packages/*/.changes -name "*.md" ! -name "README.md"扫描是否有 change 文件(输出has_change_files)。若没有,再执行needs-publish步骤:读取packages/react-router/package.json的version,用npm view "react-router@$version" version对比 npm 上是否已存在同版本,据此输出should_publish;pull_request:当has_change_files == 'true'时运行pnpm changes:pr,携带FORMAT_PATtoken;publish:当should_publish == 'true'时执行pnpm build后运行pnpm changes:publish --branch=${{ github.ref_name }};comment:仅在main分支发布成功后运行pnpm run release-comments,自动在已发布的 issue/PR 下追加“已随 vX.Y.Z 发布”的评论(对应 release-comments.ts)。
pr.ts:release PR 的创建与更新
pr.ts 的行为比文档描述多出几个值得注意的细节:
- 脚本开头通过
git rev-parse --abbrev-ref HEAD取当前分支,并强制其必须是main、hotfix或v7三者之一(pr.ts#L40-L46); - 发布分支名由 bump 的主版本号决定:
release-v<major>-pr(hotfix 场景为hotfix-v<major>-pr),主版本号取自本次 releases 中第一个包的下一个版本(pr.ts#L70-L73); - 若解析后 没有任何待发布的 change 文件,脚本不会报错退出,而是检查是否存在同名的陈旧 release PR,若有则自动关闭并注明原因(pr.ts#L75-L92)——这正是“迭代 release PR”后 PR 能被自动刷新/收敛的机制;
- 非预览模式下必须提供
GITHUB_TOKEN;随后把 git 身份配置为 "Remix Run Bot",执行git checkout -B <branch>+git reset --hard origin/<base>,再运行pnpm changes:version,最后git push origin <branch> --force,并通过 GitHub API 创建(带pkg:react-router标签)或更新 PR; - PR 正文由
generatePrBody生成,包含版本对照表和各包 changelog 预览;由于 GitHub 对 PR 正文有 65,536 字符限制,脚本以 60,000 字符为安全上限,超限时会按包截断 changelog 部分并提示“完整内容见 PR diff”(pr.ts#L50-L51、pr.ts#L253-L283)。PR 头部固定写着“This PR is managed by the release workflow. Do not edit it manually.”。
version.ts:版本、CHANGELOG 与清理
version.ts 对应文档中 “Runs version.ts: Updates versions / Generate changelogs / Deletes change files” 三步,具体实现为对每个待发布包:
updatePackageJson:把包的package.json的version写为计算出的下一版本;updateChangelog:把新条目插入该包CHANGELOG.md第一个##版本条目之前(找不到则追加到文末);deleteChangeFiles:删除.changes/下除README.md外的所有.md文件。
此外它还会更新根目录 CHANGELOG.md:聚合所有包的变更(每条前缀包名),可选地引入人工撰写的 scripts/changes/whats-changed.md(存在时作为 “What's Changed” 段落并入,消费后删除,见 version.ts#L140-L183),附上 v<old>...v<new> 的 compare 链接,并用 updateTableOfContents 重新生成根 CHANGELOG 顶部 <details> 折叠块内的目录(复用 GitHub 的标题锚点算法,含重复标题计数)。最后 commitChanges 用生成的 commit message 执行 git add . && git commit。
该脚本支持两个命令行选项(version.ts#L4-L9):
--no-commit:只改文件不提交,便于人工 review;--preview(对应根命令pnpm changes:preview):只打印将要发生的版本变更、changelog 内容与 commit message,不做任何修改。
publish 环节:pnpm 发布、tag 与 GitHub Release
publish.ts 由 CI 的 publish job 以 pnpm changes:publish --branch=<分支名> 调用,参数为:
--branch(必填):本次发布来自的分支,决定行为分支——main分支创建的 GitHub Release 会被标记为 Latest(publish.ts#L62-L63);--skip-ci-check:跳过“仅限 CI 运行”的安全检查(脚本在非 CI 环境且非 dry-run 时默认拒绝运行,publish.ts#L183-L190);--dry-run:不真正发布,而是查询 npm 计算哪些版本未发布,并预览将要创建的 GitHub Release(tag、名称、正文)。
实际发布命令是:
pnpm publish --recursive --filter "./packages/*" --access public --no-git-checks --report-summary
--report-summary 让 pnpm 在根目录输出 pnpm-publish-summary.json,脚本随后读取该文件得到“实际发布了哪些包、什么版本”,据此为每个已发布包创建形如 react-router@x.y.z 的 git tag(已存在则跳过)、git push --tags,并仅为 react-router 主包创建 GitHub Release(正文指向根 CHANGELOG 对应版本锚点,已存在则跳过)。若某个 Release 创建失败,脚本会汇总打印并提示需要手动补建,同时以非零码退出。
从源码结构还可以看到一个特殊分支:当发布分支为 v7 时,发布被拆成两阶段——除 react-router-dom 外的所有包以 version-7 dist-tag 发布,react-router-dom 单独以 latest 发布(publish.ts#L200-L209)。这与 pr.ts 中允许 v7 作为发布分支的设定相互印证,属于维护 v7 旧版本线时的专用路径。
工作流中还有一个文档未展开但同属 release.yml 的 job:experimental-release,由 workflow_dispatch 手动触发并指定分支,执行 experimental:version(打 tag 并推送)+ pnpm build + experimental:publish(release.yml#L170-L209),用于从任意分支发布实验版本。
迭代一个处于 open 状态的 release PR
DEVELOPMENT.md 的 “Iterating a release PR” 一节说明:release PR 尚未合并时如需补充改动,正确做法是不要直接改 release 分支,而是:
- 从
main拉分支做改动; - 按需添加 change 文件;
- 推到 GitHub 并向
main发 PR; - Review/批准后将 PR 合并到
main; - 这会触发上述
pr.ts逻辑——由于 change 文件又出现了,工作流会重新运行version.ts并强制推送更新已存在的release-v<major>-pr分支与 PR(git checkout -B+--force推送 +updatePr刷新标题和正文)。
反过来,如果所有 change 文件都已处理完、main 上不再有 change 文件,pr.ts 会检测到 releases.length === 0 并自动关闭陈旧的 release PR,保证 PR 状态始终与 change 文件一致。
Hotfix 发布流程
Hotfix 与常规流程的差别在于基线分支不是 main,而是从要修复的那个版本 tag 拉出的 hotfix 分支(DEVELOPMENT.md 原文步骤):
-
从对应版本的 git tag 创建 hotfix 分支并推送到远端:
git checkout -b hotfix {tag} git push origin --set-upstream hotfix -
从
hotfix再拉分支做修复,连同 change 文件以 PR 形式合入hotfix; -
release.yml检测到hotfix分支上有 change 文件,触发 “PR” 步骤(pr.ts):- 从
hotfix创建或更新hotfix-v7-pr之类的版本化分支; - 更新新分支上的版本号、生成对应的
CHANGELOG.md条目、删除 change 文件; - 从
hotfix-v<major>-pr向hotfix分支发 PR;
- 从
-
该 PR 合并后,工作流再次针对
hotfix运行且发现无 change 文件,触发 “publish” 步骤(publish.ts):发布所有包、打 tag 推 tag、创建 GitHub Release; -
最后把
hotfix分支合并回main并推送,然后删除hotfix分支。
从实现上看,这一流程与常规流程共用同一套脚本:pr.ts 通过当前分支名判断前缀(baseBranch === "hotfix" ? "hotfix" : "release",pr.ts#L72-L73),PR 标题也会相应显示 “Hotfix Release vX.Y.Z”;而 release.yml 的 push 触发器本身就包含 hotfix 分支,因此无需额外配置。需要留意的是 publish 脚本中 GitHub Release 的 Latest 标记仅在 main 分支发布时设置,hotfix 发布不会抢占 Latest。
本地可用的校验与预览命令
结合根 package.json 的 scripts 定义,围绕这套流程常用的命令如下(均在仓库根目录执行):
| 命令 | 作用 |
|---|---|
pnpm changes:add |
交互式创建 change 文件(选包、选 bump 类型、写描述) |
pnpm changes:validate |
校验所有包的 change 文件与 CHANGELOG.md 是否齐备,有错时退出码为 1 |
pnpm changes:preview |
以 preview 模式运行 version.ts,只打印将发生的版本/CHANGELOG 变更 |
pnpm changes:version |
真正更新版本号、CHANGELOG 并创建 release commit(CI 中由 pr.ts 调用) |
pnpm changes:pr --preview |
预览将创建/更新的 release PR 的分支、标题与正文,不做任何修改(pr.ts 的 --preview 模式下无需 GITHUB_TOKEN) |
pnpm changes:publish --branch=main --dry-run |
干跑发布:列出将要发布的包与版本、预览 GitHub Release,不产生 tag |
这些预览/校验入口让发布动作在合并与推送之前都可以被完整复核:change 文件的合法性在 validate 与 pr.ts/version.ts 内部会被重复校验(parseAllChangeFiles 失败即退出),版本与 changelog 的形态可先经 --preview/--dry-run 确认,符合 DEVELOPMENT.md 所描述的“mostly automated” 但每步皆可人工预检的设计。
小结
React Router 的发布体系是一条以 change 文件为唯一输入 的可重复流水线:add.ts 产出 change 文件 → release.yml 的 check job 识别分支状态 → pr.ts 在版本化分支上运行 version.ts 并自动开/更 release PR → PR 合并后 needs-publish 比对 npm 状态 → publish.ts 完成 npm 发布、git tag 与 GitHub Release,最后 comment job 回填 issue/PR 评论。hotfix 场景把同样的脚本复用在从版本 tag 派生的 hotfix 分支上,只改变分支前缀与 Latest 标记行为。理解 scripts/changes/ 中每个脚本与 release.yml 中各 job 的对应关系,是维护这套流程(或将其迁移到自己的多包仓库)的关键。
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