Hotdry.

Article

用 Changesets 驱动多语言 Monorepo:跨包版本管理与变更日志自动化实战

深入解析如何在包含 Python、Rust、Go 等多种编程语言的多语言 monorepo 中使用 Changesets 实现跨包版本管理与变更日志自动化,并给出可落地的工程参数与监控要点。

2026-04-21web

在现代软件工程实践中,monorepo 架构已经成为中大型团队提升开发效率的重要选择。相比多仓库模式,monorepo 能够实现原子性提交,避免跨仓库依赖带来的兼容性和集成问题。然而,当一个 monorepo 跨越多种编程语言时,版本管理和变更日志的自动化就成为一个棘手的问题。本文将详细介绍如何利用 Changesets 这一工具,在多语言 monorepo 中实现统一的跨包版本管理与变更日志自动化。

为什么选择 Changesets

Changesets 最初是为 JavaScript 和 TypeScript 生态系统设计的版本管理工具,它的核心设计理念是通过「changeset」文件来记录每次变更的内容、影响范围和版本升级类型(major、minor、patch)。这种设计相比传统的基于 commit 信息的版本管理方式有显著优势:它允许开发者为内部工程师和最终用户分别编写不同层次的变更说明,这在实际项目中往往对应着两个截然不同的受众群体。

对于多语言 monorepo 而言,Changesets 的吸引力在于它提供了一个统一的工作流程来管理所有包的版本和变更日志。尽管它原生专注于 JavaScript 生态,但通过自定义脚本和配置,完全可以将其扩展到 Python、Rust、Go 等其他编程语言。这种扩展能力的核心思路是:将以 JavaScript 的 package.json 作为版本的「权威来源」,然后通过自定义同步脚本将版本号传播到各语言对应的清单文件中,如 Python 的 pyproject.toml、Rust 的 Cargo.toml 或 Go 的 go.mod。

核心架构设计

在多语言 monorepo 中部署 Changesets,推荐的目录结构应该将所有包放置在一个统一的 packages 目录下,无论这些包使用何种编程语言。这样做的好处是能够利用 pnpm 或 yarn 的 workspace 特性,让 Changesets 能够统一管理所有包的版本。以下是一个典型的结构示例:

.
├── .changeset
│   ├── config.json
│   └── README.md
├── packages
│   ├── python-one
│   │   ├── pyproject.toml
│   │   └── package.json    # 代理文件
│   ├── rust-one
│   │   ├── Cargo.toml
│   │   └── package.json    # 代理文件
│   └── rust-two
│       ├── Cargo.toml
│       └── package.json    # 代理文件
├── pnpm-workspace.yaml
└── package.json

这里的核心技巧是为每个非 JavaScript 的包创建一个「代理」package.json 文件。这个文件只需要包含最基本的信息:name 和 version。Changesets 正是通过这些代理文件来执行版本升级操作,而真正的版本同步则由我们编写的自定义脚本完成。

关键配置参数

在 .changeset/config.json 中,有几个关键配置需要特别关注。access 应设置为 restricted,因为大多数企业级 monorepo 的包都是内部私有的,不需要发布到公共 npm registry。privatePackages 选项需要启用 version 和 tag,这样 Changesets 就会为内部包创建版本标签,但不会尝试将它们发布到 npm。

对于 polyglot 支持,最重要的配置是将 commit 和 publish 命令留空或指向我们自定义的脚本。这是因为默认的 publish 行为是向 npm 发布包,但在多语言场景下,我们需要自己控制如何发布不同语言的包。以下配置展示了这种设置方式:

{
  "changelog": "@changesets/changelog-git",
  "commit": false,
  "access": "restricted",
  "baseBranch": "main",
  "privatePackages": {
    "version": true,
    "tag": true
  }
}

需要特别注意的是,如果你有 docs 或其他不需要版本化的目录,应该为它们创建独立的 pnpm-workspace.yaml 文件,将其与主 workspace 隔离。否则,pnpm 会尝试将所有目录的依赖合并到根目录的 package-lock.json 中,这会导致不必要的复杂性。

版本同步脚本实现

Changesets 完成版本升级后,会更新所有代理 package.json 文件中的版本号。下一步是将这些版本号同步到各语言原生的清单文件中。这是实现 polyglot 支持的关键环节。

一个实用的做法是使用 Python 脚本配合 uv 执行器来同步版本。这个脚本需要完成三个主要任务:首先读取所有 package.json 文件中更新的版本号,然后根据包的语言类型更新对应的清单文件(如 Cargo.toml、pyproject.toml),最后刷新各语言的锁文件以反映版本变化。

对于 Rust 包的 Cargo.toml,版本号位于 [package] 部分,格式为 version = "x.y.z"。同步时需要使用正则表达式匹配并替换版本字符串。对于 Python 包的 pyproject.toml,版本号位于 [project] 部分,格式相同。同步逻辑可以复用同一个正则表达式模式,只是目标文件路径不同。

锁文件刷新同样重要。对于 Rust 包,需要在每个 Cargo.toml 所在目录运行 cargo update --workspace;对于 Python 包,则运行 uv lock。完整的版本同步流程应该先执行 npx @changesets/cli version 让 Changesets 更新所有代理 package.json,然后运行同步脚本将版本传播到各语言的清单文件,最后刷新所有锁文件。

GitHub Actions 自动化工作流

自动化发布流程通过 GitHub Actions 实现时,有一个重要的技术细节需要特别注意:GitHub 的 on.push.tags 触发器存在两个已知问题。首先,如果你同时推送超过 3 个标签,工作流将不会触发,这在 monorepo 中是常见场景。其次,标签触发的可靠性较低,即使使用个人访问令牌(PAT)也无法完全解决。

推荐的做法是使用 workflow_call 模式,显式地从主工作流调用发布工作流。主工作流在检测到 Changesets 产生了版本更新后(通过检查 changesets/action 的 published 输出),显式调用 Docker 构建或其他发布工作流。

在主工作流中,version 命令应该指向自定义脚本而非直接的 changesets CLI 调用。例如,可以设置为 version: just version,其中 just 是任务运行器,它会依次执行版本号更新和同步脚本。publish 命令可以设置为 npx @changesets/cli publish,但实际上对于私有包来说,由于配置了 privatePackages,这个命令只会创建 Git 标签,不会实际发布到 npm。

监控与回滚策略

在实际生产环境中,建议为版本同步流程添加监控点。首先,每次同步脚本执行后应该记录日志,明确列出哪些包被更新、哪些包保持不变。这些日志可以通过 GitHub Actions 的输出环节捕获,便于后续审计。

其次,建议设置版本一致性检查机制。在 CI 流程中,可以添加一个检查步骤,验证 package.json 中的版本与各语言清单文件中的版本是否一致。如果发现不一致,CI 应该失败并阻止合并。这个检查可以简单地用 grep 或专门的脚本实现。

对于回滚策略,由于 Changesets 使用的是基于文件的版本管理,回滚相对简单:如果需要撤销某个版本的发布,可以删除对应的 .changeset 文件,然后重新运行版本命令。GitHub 上的版本标签也可以通过 git push --delete 远程删除,然后重新触发发布流程。

需要注意的是,GitHub Actions 的工作流触发器存在上述局限性,在设计回滚流程时要考虑到这一点。建议在关键版本发布后,手动验证标签是否正确创建,以及对应的构建是否成功触发。

工程实践参数清单

基于上述分析,以下是在多语言 monorepo 中实施 Changesets 的关键工程参数总结:代理 package.json 必须包含 name、version 和 private: true 字段;.changeset/config.json 中的 privatePackages.version 和 privatePackages.tag 应设置为 true;版本同步脚本应支持 Cargo.toml、pyproject.toml 和 go.mod 三种主流语言清单;锁文件刷新步骤对于确保依赖一致性至关重要;GitHub Actions 工作流应避免依赖 on.push.tags 触发器,改用 workflow_call 模式。

通过这种方式,团队可以获得 Changesets 提供的统一变更日志和版本管理体验,同时保持各语言包的原生开发体验不被破坏。这种架构特别适合中规模的跨语言项目团队,既能享受 monorepo 带来的原子性提交优势,又不需要引入 Bazel、Pants 等重量级的构建系统。


资料来源:本文技术细节参考了 Luke Hsiao 关于在多语言 monorepo 中使用 Changesets 的实践经验(https://luke.hsiao.dev/blog/changesets-polyglot-monorepo/),以及 Changesets 官方文档和 GitHub Issues 中关于 polyglot 支持的讨论。

web