工程实践

语义化版本控制,应该怎样配合你的体验日志

阅读约 1 分钟

语义化版本控制会在调用方还没有读过任何一条体验日志记录之前,就先告诉她一次发布到底能带来多大的伤害。从 2.4.1 升到 2.5.0,说的是:新增了能力,什么都不会坏。从 2.5.0 升到 3.0.0,说的则是:更新之前,先把这条记录读一遍。体验日志和版本号本该用两种不同的形式,去主张同一件事情,而两者之间的大多数摩擦,恰恰就出现在它们说法不一致的那些时刻,而这种情况发生的频率,往往比这份规范本身暗示的要高得多。

版本里的每一个数字,到底承诺了什么

语义化版本控制定义了三个数字,MAJOR.MINOR.PATCH,每一个都配有一条严格的规则,说明到底是什么触发了它。MAJOR 的跃升,意味着一次不兼容的变更:一个正确、且已经存在的集成有可能会注意到,也因此不得不为之做出改变的东西。MINOR 的跃升,意味着新增了向后兼容的功能:已有的东西什么都不会坏,只是多了一些新的可用能力。PATCH 的跃升,意味着一次向后兼容的修复:行为变得更贴近文档所描述的样子,而任何刻意依赖旧行为的人,都不应该注意到任何变化。

跃升含义记录应该读起来像
MAJOR (1.x.x -> 2.0.0)一次不兼容的变更“更新之前需要采取行动”
MINOR (1.2.x -> 1.3.0)新增的、兼容的能力“从现在起就能用了,别的什么都没变”
PATCH (1.2.3 -> 1.2.4)一次兼容的修复“现在的行为终于和文档描述的一致了”

这张表反过来读,也同样是一个检验:如果一条记录读起来和它所在的那一行对不上,那要么是版本号本身标错了,要么是这条记录把实际发生的事情说得太轻或者太重了。

就版本管理而言,什么才算是不兼容

判断的标准,和判断某个东西到底该不该收进 API 体验日志里的标准是一样的:一个针对旧行为而写、且从那以后一直没有被改动过的正确调用方,会不会因为这次变更而表现出不一样的行为。什么是不兼容的变更,又该如何发布它完整讲解了这个判断过程,也包括那些看起来不兼容、实际上并不是的情况,以及那些看起来很小、实际上并不小的情况。就版本管理而言,简单来说:只要答案是肯定的,那这次跃升就是 MAJOR,跟这次变更在内部实际改动了多少代码完全没有关系。版本号追踪的,是对调用方造成的后果,而不是团队付出的努力。

一条体验日志记录,应该怎样和一次版本跃升相对应

一条记录,对应一个跃升分类,而且要从一开始就明确说出来。表格里的这个模式会一直延续下去:一条不兼容的记录,放在引入它的那个版本下面,先以警告的方式写出来,再补上说明。一条新增功能的记录,放在它所属的 MINOR 版本下面,以”现在可用了”的方式写出来。一条修复的记录,放在它所属的 PATCH 版本下面,以”已经纠正”的方式写出来。把不同分类混在同一条记录里,比如把一次不兼容的变更硬塞进一段和它毫不相关的修复说明里,正是读者会因此错过那唯一真正重要的信息的原因。

## 3.0.0 (2026-09-07)

### Changed
- **BREAKING:** `GET /reports` 现在会把金额以最小货币单位(分)的整数
  形式返回,而不再是浮点数。请更新所有直接读取`amount` 字段的代码。

## 2.9.0 (2026-09-01)

### Added
- 报表现在可以按 `status` 进行筛选了。

## 2.8.4 (2026-08-28)

### Fixed
- `GET /reports?status=` 在遇到无法识别的状态时,原本会返回一个空页面
  ,而不是 400 错误,现在已经修正。

从上往下读,版本号和板块标签其实把同一件事情说了两遍,而这正是它们存在的意义:一个只是快速扫一眼标题的读者,在打开任何一行内容之前,就已经能对风险大小做出正确的判断了。

破坏性变更这条规则,在 1.0.0 之前也一样适用吗

不一样,而大多数关于”这到底算不算破坏性变更”的困惑,恰恰就出在这里。语义化版本控制明确说明, 主版本号为零,也就是 0.y.z,代表的是初始开发阶段:任何东西都可能在任何时候发生变化,公共 API 也不应该被视为稳定的。从 0.4.0 升到 0.5.0 完全可以带着一个破坏性变更,而不违反规范, 因为主版本号那份保证,要到项目真正发布 1.0.0 之后才开始生效。一条体验日志记录依然欠读者同 样的诚实,说清楚到底破坏了什么;唯一变化的是,在 1.0.0 到来之前,版本号本身还不是那个可以依 赖的信号。

如果你的产品根本不发布离散的版本呢

大多数 SaaS 产品都是持续部署的,也从来不会向调用方展示任何版本号,但这并不会消除对这套自律的需要,消失的只是那个本该承载它的数字而已。这时候,一条体验日志记录就必须独自完成全部的工作:清楚地说明这次变更到底是不兼容的、新增的、还是一次修复,用的还是语义化版本控制所使用的那同样三个词,哪怕根本没有一个版本字段可以把它们挂上去。有些团队会维护一个纯粹内部使用的版本号,唯一的目的就是把体验日志的记录锚定到某个可以被引用的东西上,却从来不会把这个版本号直接展示给调用方看。

这具体又是怎么应用到 API 体验日志上的

比起几乎其他任何场景都要更加严格,因为 API 的调用方是代码,不是那种可以对着一次意外的变更耸耸肩就翻篇的人。API 体验日志:该公开什么内容,又是谁在读它完整讲解了这份文档的完整形态;而这里的版本管理自律,正是让它的 breaking 和 additive 两个板块保持诚实的东西。一个同时提供多个版本的 API,比如在一段迁移窗口期里同时并行提供 v1 和 v2,实际上是在整个接口的规模上应用语义化版本控制,而不只是针对某一个软件包,而且同样这三个词组成的词汇表,仍然适用于每一条记录。

Keep a Changelog 对版本管理是怎么说的

它直接以名字和语义化版本控制关联在一起,并推荐了和本文所使用的完全相同的一套分类词汇:Added、Changed、Deprecated、Removed、Fixed、Security。Keep a Changelog,实践版讲解了该如何采用这份规范,也包括团队通常会在哪些地方偏离它。这种重合并不是巧合:这两份规范其实是在从相反的两端,尝试解决同一个问题,一份负责把版本号标准化,另一份则负责把解释版本号的那条记录标准化。

FAQ

是不是每一条体验日志记录都需要一个版本号? 如果产品本身会发布版本,那就需要,因为这个数字能让读者不用先读记录本身,就能直接跳到”这件事到底对我影响有多大”这个问题上。如果产品是持续部署、根本没有版本字段的,那这条记录的措辞本身,就必须独自承担起传达这个信号的责任。

MAJOR 跃升和一条不兼容变更的记录之间有什么区别? 它们本该用两种方式描述同一个事件。版本号是机器可读的信号(调用方的工具链可以对它做出反应);体验日志记录则是人类可读的解释,说明到底具体发生了什么变化。

PATCH 发布有可能是不兼容的吗? 按定义来说,不应该是。如果还是发布出去了,不要修改已发布的版本,也不要重新打标签:SemVer FAQ 的建议是发布一个恢复兼容性的新版本,如果这个破坏性变更要保留,就发布一个新的 MAJOR 版本,并在文档里注明出问题的那个版本,让用户知道要跳过它。

纯粹内部的变更需要版本跃升吗? 不需要。语义化版本控制追踪的是公开接口。一次对调用方没有任何可观察影响的重构,即便在内部确实是一项相当可观的工程工作,也既不需要版本跃升,也不需要一条体验日志记录。


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

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

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