首页
/ React Router 发布流程全解:change 文件、版本脚本与 GitHub Actions 自动发布流水线

React Router 发布流程全解:change 文件、版本脚本与 GitHub Actions 自动发布流水线

2026-09-05 23:06:01作者:牧宁李

React Router 的版本发布几乎完全由自动化流水线完成:开发者只需把描述变更的 change 文件随 PR 合入 main,后续的分支管理、版本号更新、CHANGELOG 生成、npm 发布和 GitHub Release 创建全部由脚本与 GitHub Actions 接手。本篇基于仓库中的 DEVELOPMENT.mdscripts/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.yml Github workflow

落到仓库中,这两个组成部分分别是:

  1. 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)。
  2. .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>.<描述>.mdbump 必须是 majorminorpatchunstable 四种之一(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 描述的常规发布步骤如下(原文骨架):

  1. 把所有待发布的改动连同 change 文件合并到 main
  2. release.yml 发现 main 分支存在 change 文件时:
    • 触发 “PR” 步骤,运行 scripts/changes/pr.ts
    • main 创建或更新版本化发布分支,如 release-v7-pr
    • 运行 scripts/changes/version.ts:更新版本、生成 changelog、删除 change 文件;
    • release-v<major>-prmain 打开一个 PR;
  3. 该 PR 合并后,release.yml 再次针对 main 运行,此时发现没有 change 文件:
    • 触发 needs-publish 检查,查询 npm 判断当前 react-router 版本是否已发布;
    • 若需要发布,触发 “publish” 步骤,运行 scripts/changes/publish.ts:发布所有包到 npm、打 tag 并推送、创建 GitHub Release;
  4. release-v<major>-pr 分支在 PR 合并后可以删除。

结合 release.yml 的 job 编排,可以看清上述每一步的实际落点。工作流由 pushmain/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.jsonversion,用 npm view "react-router@$version" version 对比 npm 上是否已存在同版本,据此输出 should_publish
  • pull_request:当 has_change_files == 'true' 时运行 pnpm changes:pr,携带 FORMAT_PAT token;
  • 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 取当前分支,并强制其必须是 mainhotfixv7 三者之一(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-L51pr.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” 三步,具体实现为对每个待发布包:

  1. updatePackageJson:把包的 package.jsonversion 写为计算出的下一版本;
  2. updateChangelog:把新条目插入该包 CHANGELOG.md 第一个 ## 版本条目之前(找不到则追加到文末);
  3. 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:publishrelease.yml#L170-L209),用于从任意分支发布实验版本。

迭代一个处于 open 状态的 release PR

DEVELOPMENT.md 的 “Iterating a release PR” 一节说明:release PR 尚未合并时如需补充改动,正确做法是不要直接改 release 分支,而是:

  1. main 拉分支做改动;
  2. 按需添加 change 文件;
  3. 推到 GitHub 并向 main 发 PR;
  4. Review/批准后将 PR 合并到 main
  5. 这会触发上述 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 原文步骤):

  1. 从对应版本的 git tag 创建 hotfix 分支并推送到远端:

    git checkout -b hotfix {tag}
    git push origin --set-upstream hotfix
    
  2. hotfix 再拉分支做修复,连同 change 文件以 PR 形式合入 hotfix

  3. release.yml 检测到 hotfix 分支上有 change 文件,触发 “PR” 步骤(pr.ts):

    • hotfix 创建或更新 hotfix-v7-pr 之类的版本化分支;
    • 更新新分支上的版本号、生成对应的 CHANGELOG.md 条目、删除 change 文件;
    • hotfix-v<major>-prhotfix 分支发 PR;
  4. 该 PR 合并后,工作流再次针对 hotfix 运行且发现无 change 文件,触发 “publish” 步骤(publish.ts):发布所有包、打 tag 推 tag、创建 GitHub Release;

  5. 最后把 hotfix 分支合并回 main 并推送,然后删除 hotfix 分支。

从实现上看,这一流程与常规流程共用同一套脚本:pr.ts 通过当前分支名判断前缀(baseBranch === "hotfix" ? "hotfix" : "release"pr.ts#L72-L73),PR 标题也会相应显示 “Hotfix Release vX.Y.Z”;而 release.ymlpush 触发器本身就包含 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 文件的合法性在 validatepr.ts/version.ts 内部会被重复校验(parseAllChangeFiles 失败即退出),版本与 changelog 的形态可先经 --preview/--dry-run 确认,符合 DEVELOPMENT.md 所描述的“mostly automated” 但每步皆可人工预检的设计。

小结

React Router 的发布体系是一条以 change 文件为唯一输入 的可重复流水线:add.ts 产出 change 文件 → release.ymlcheck 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 的对应关系,是维护这套流程(或将其迁移到自己的多包仓库)的关键。

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