工程实践

Monorepo 的体验日志:到底该合成一份,还是按包拆开

阅读约 1 分钟

一个 monorepo 在同一个仓库里,容纳了好几个各自独立发布的东西,而体验日志首先必须回答一个问题:读者真正在意的,到底是这个仓库本身,还是里面某一个具体的包。大多数团队从来没有真正认真想过这个问题。他们因为只有一个仓库,就先从一份体验日志开始,随着时间推移不断往里加包,最后落得一个日志,让用 CLI 的人得翻过四十条完全不相关的后端记录,才能找到那条真正发布了自己那个修复的记录。真正决定正确形式的,从来不是仓库本身的结构,而是谁在读这份日志,以及他早已知道自己在找什么。

Monorepo 的体验日志,跟单一仓库的体验日志到底有什么不一样

单一仓库的体验日志有一个默认的读者群体:所有使用这个仓库所构建的那唯一一样东西的人。Monorepo 的读者群体则是按包划分的,同一个仓库里的不同包,经常按照完全不同的节奏发布,面向完全不同的使用者,处于完全不同的稳定程度。一个发布到注册中心的库,跟一个内部管理工具,完全可以住在同一个 monorepo 里,而对读体验日志的人来说,两者之间几乎没有任何共同点。

仓库形态典型读者合适的体验日志
单一可部署应用所有使用该产品的人一份日志,覆盖整个仓库
库的工作区(多个已发布的包)依赖某个具体包的人每个包一份日志
应用加内部工具两类完全不重叠的读者按读者划分,而不是按文件夹划分
应用加自己的 SDK产品的使用者,以及 SDK 的集成者两份日志:产品向和 SDK 向

是不是每一个包都需要自己单独的体验日志

只有拥有独立读者群体的包才需要。一个发布到注册中心的包,需要自己单独的日志,因为安装它的人根本没有理由去读仓库里的其他任何东西,而且像 Lerna 和 Changesets 这样的 monorepo 发布工具,都会为每个包写一份 CHANGELOG.md,就放在它自己的 package.json 旁边。一个只有一个使用者的内部工具,这个使用者就是已经住在同一个仓库里的那个应用,它根本不需要单独的日志;把它的改动直接并进那个应用自己的记录里,远比再维护一份团队之外没人会打开的第二个文件更有用。

判断标准跟判断任何一条记录到底该不该进体验日志,是同一个标准:读者会不会注意到这个变化,会不会在意,以及知道了之后能不能据此采取行动。把这个标准按包应用,而不是按文件夹应用,一个有十二个包的仓库,最后完全可能只留下两份真正需要的体验日志,剩下十个包根本不需要日志。

到底该怎么知道,是哪个包造成了哪一条体验日志记录

在写下每一条记录的那一刻,就用对应的包给它打上标签,而不是事后再去查这个提交到底改了哪些文件。一个修复共享内部库的提交,完全可能在每一个依赖它的包里,各自产生一条体验日志记录,而光靠文件路径,根本没法说清楚这些下游记录里,到底哪一条是读者真正需要看到的;能做出”这一点对使用包 A 的人可见、对使用包 B 的人不可见”这种判断的,只有人。Conventional commits 通过在每次提交里点名具体的包,从机制上帮上了忙,但作用域本身也只能产生一份草稿。那篇文章里同样的两层规则,在这里按包套用依然成立:哪怕作用域标注得再正确,草稿在真正用那个包的读者能懂的话说出来之前,仍然需要人工过一遍。

一份共享体验日志,需要什么是单一仓库的体验日志用不上的

每一条记录最前面、描述之前,都要有一个包的标签,这样浏览日志的读者才能一遍扫过去,就跳过所有跟自己无关的内容。没有这个标签,共享日志读起来就像一份随机拼凑的信息流,一个只关心某一个包的读者,除了硬记住哪几行才算数之外,根本没有别的办法把它筛选出来,而这件事过了第一周基本没人还会坚持做。

## 2026-09-07

### [cli] 新增
- `acme push --dry-run` 会展示将要发送的内容,但实际
  上并不会真的发送出去。

### [core] 修复
- 一个返回空响应体的成功请求,不会再触发重试退避的重置了。

两条记录,两类读者,一眼就能分清楚。像 Changesets 这样的工作流,直接把这种打标签的做法内建进了发布流程本身:贡献者在自己的改动旁边,写一条带着包作用域的简短说明,工具再在发布那一刻,从这些说明里组装出按包划分的体验日志和版本号跳跃,而不是事后试图从一段已经合并在一起的提交历史里,反推出各个包之间的边界。

版本管理跟 monorepo 的体验日志到底是怎么联系在一起的

独立管理版本号的包,需要自己单独的体验日志,因为它们各自有自己独立的版本号,一份共享的体验日志根本没法表达”包 A 从 2.1 升到了 2.2,而包 B 还停在 1.4”这种情况,除非变成一个文件里塞进两份日志。语义化版本控制,应该怎样配合你的体验日志 完整讲解了版本号本身应该怎样对应到体验日志的分类上;在 monorepo 里,这套对应关系必须按包来套用,因为某一个包里的破坏性变更,对不依赖它的兄弟包来说,根本算不上破坏性变更。

一个仓库如果把一个产品作为单一可部署单元来发布,哪怕它内部由许多个内部包构建而成,也完全不会遇到这个问题:这些包始终一起发布,因此共用同一个版本号,用一份体验日志就是正确的做法。

Git 标签在 monorepo 里到底该怎么用

Git 标签、发布记录和你的体验日志该怎样对应 里同样的规则,按包套用之后依然成立:一个拥有自己独立版本号的包,需要自己单独的标签前缀,通常写成 包名@1.4.0,而不是一个说不清到底属于哪个包的、光秃秃的 v1.4.0。一个只用光秃秃版本号打标签的 monorepo,事后根本没法回答”cli 发布 2.2 的时候,core 里到底是什么内容”,因为磁盘上完全没有任何东西,记录下这个标签当初到底属于哪个包。

FAQ

Monorepo 里的每一个包,是不是都需要单独的体验日志? 只有拥有独立读者群体的包才需要,通常也就是发布到注册中心的那些包。一个只有一个内部使用者、而这个使用者已经住在同一个仓库里的包,完全可以并进那个使用者自己的日志里,而不用单独维护一份。

是什么东西,把一条体验日志记录标注成正确的包? 是写下这条记录的人,就在他写下这条记录的那一刻做出的判断,而不是对改动过的文件路径做一次自动扫描。一次共享库的改动,完全可能在每一个依赖它的包里各自产生一条不同的记录,而这些下游记录到底该各自说些什么,只有人能决定。

Monorepo 是不是应该给所有东西都用同一个版本号? 只有在每一个包始终跟其他包一起发布的情况下才成立。如果这些包有朝一日会被独立发布,它们就需要各自独立的版本号,而独立的版本号,要想真正有意义,就需要各自独立的体验日志。

Monorepo 的体验日志工具,是不是能替代人工编辑这一步? 不能。像 Changesets 这样的工具,自动化的只是在发布那一刻收集并组装各个包的说明这件事本身;说明的内容本身,用读者的语言而不是贡献者的语言写出来,跟其他任何体验日志流程一样,依然是一个人的工作。


本文的技术内容未经独立审核。如有错误,请告诉我们,我们会更正。

changeloop 相关页面: changelog 生成器, 开发者文档

changeloop
打造闭环 changelog 的团队。用户提出需求,你的团队交付,提出需求的人得知结果。