版本号语义:SemVer、CalVer 等版本规范详解

说到版本号,很多团队一开始都觉得这玩意儿有啥好纠结的?不就是 1.0、2.0 往上加嘛。嗯,我以前也这么想。直到有一次,一个第三方库从 2.3.4 升级到了 2.3.5,我以为只是个小修小补,结果 CI 直接炸了——API 签名变了,项目里十几个文件报错。

从那天起,我才真正意识到:版本号不是随便写写的。它是一份契约。一份告诉下游“你这次更新到底有多疼”的契约。

SemVer:语义化版本,业界的“通用语言”

SemVer,全称 Semantic Versioning,说白了就是一套标准化的版本号起草规则。它的核心公式很简单:MAJOR.MINOR.PATCH。

字段 含义 你对下游的影响
MAJOR(主版本) 做了不兼容的 API 修改 直接破坏已有接口,必须改代码才能兼容
MINOR(次版本) 新增了向下兼容的功能 新功能来了,但旧代码不受影响
PATCH(修订号) 向下兼容的问题修复 纯 bug 修补,换了就完事

我见过一个挺好的项目,却从 1.0 直接跳到 12.0。一问才知道,只是为了“显得迭代快”。这种搞法,说实话挺不负责任的。你想想看,下游的依赖维护者看到这种版本号,根本不敢升。

为什么必须是三位数?

二位数(比如 1.0 → 2.0)不也能表示更新吗?可以,但信息量不够。三位数才够传递“这次改动到底有多大”的信号。我习惯用一句话总结:PATCH 修漏洞,MINOR 加功能,MAJOR 随它去。

核心原则:版本号必须递增。一旦发布了一个包,那个版本号就永远锁定,不能改内容,只能重发一个新的版本。

SemVer 的适用场景

我个人觉得,SemVer 最适合那些有公开 API 的库或框架。比如 npm、Maven、Cargo 生态,基本都认这套。你发布一个 npm 包,如果遵守 SemVer,工具链就能自动帮你算依赖——^1.2.3 表示只锁主版本,~1.2.3 表示只锁主次版本。这就是计算机可读的契约。

一个小技巧:如果你在开发一个内部服务,API 不太稳定,可以用 0.x 开头。SemVer 规定,主版本号为 0 的阶段,任何改动都可能是不兼容的。这给了你自由度,也给了下游警告。

CalVer:基于日历的版本,简单粗暴

SemVer 好用,但它有个软肋:没有时间信息。你拿到一个 SemVer 版本,比如 4.3.1,你根本不知道它是什么时候发布的。这时候 CalVer 就派上用场了。

CalVer 的格式很多样,常见的包括:

  • YY.MM:比如 Ubuntu 版本 22.04、24.04
  • YY.MM.PATCH:比如某个库 23.05.1
  • YYYY.MM.DD:比如一些工具每日构建版本 2025.04.17

我记得有个团队,用的库后缀是 “.2024Q2”。我刚开始还以为是营销号瞎写的,后来才知道,那是 CalVer 里的季度编号版本。

注意:CalVer 不代表兼容性!它只告诉你“这玩意是哪个时间点出的”。依赖 CalVer 的项目,通常版本号外的文档或公告会额外说明兼容性。如果你只看日期就随意升级,可能会踩大坑。

SemVer 与 CalVer 对比

维度 SemVer CalVer
核心目的 告知兼容性 告知发布时间
典型格式 MAJOR.MINOR.PATCH YY.MM 或 YYYY.MM.DD
信息量 高(兼容边界清晰) 低(不直接说明兼容)
适合场景 库、框架、公开API 用户端应用、发行版、CLI工具
自动依赖解析 非常成熟 几乎没有

当然,这两者不是非此即彼。很多大型项目会用混合方案。比如内核的 Longterm 版本同时包含语义和年份。

预发布版本与构建元数据

SemVer 其实还留了一个扩展位:预发布标识和构建元数据。格式是 MAJOR.MINOR.PATCH-预发布+构建信息。比如 2.0.0-alpha.1+build.20250417。这里要注意:

  • 预发布部分(半字线后、加号前):表示这个版本不稳定,优先级低于正式版。比如 1.0.0-alpha 小于 1.0.0
  • 构建元数据(加号后):只起描述作用,不参与版本比较。比如 1.0.0+build.001 和 1.0.0+build.002 是完全相同的版本。
我曾经踩过的坑:有一次,我们把预发布版本写成了 3.0.0-beta2,没有加短横线。SemVer 解析器直接把它当成了 PATCH 版本“beta2”,把它排在了 3.0.0 后面。从那以后,我对预发布格式再也不敢偷懒。

版本规范的知识体系图

下面这张图,帮你理清版本号的几大规范以及它们的关系。这是本章的核心结构,我自己画过好几版,选了最清晰的一版贴在这里。

版本号规范分类体系 SemVer 语义版本 CalVer 日历版本 MAJOR.MINOR.PATCH 预发布标识 构建元数据 YY.MM.PATCH YYYY.MM.DD 季度编号 版本号是契约:可读、可算、可追 SemVer 告诉你“兼容不兼容” CalVer 告诉你“什么时候出的” 二者可组合使用

几个常见的版本号陷阱

最后,我快速列几个实际工作中常看到的陷阱,帮你省点时间:

  • 版本号不进反退:比如发布完 2.0.0,又发了一个 1.9.9。这会直接搞崩大多数包管理器。
  • 忘了更新版本号:改了代码,版本号忘了改。这是那种“看着没问题,一上线就出事”的典型。
  • 乱用 0.x:有人为了让版本号看起来“稳”,把 0.9.9 之后直接写 1.0.0。但里面全是破坏性变更。这不叫稳,这叫骗。
我的个人习惯是:用一个 CHANGELOG.md 跟踪版本历史,每个版本号伴随一句“为什么改”。这比事后翻 git log 快多了。而且面试的时候也能简单讲明白那些版本背后的设计决策。