版本号管理与语义化版本
版本号这东西,看起来不就是几个数字加点吗?很多人这么想,但实际踩坑的不少。
我之前审计过一个项目,从 1.2.3 直接跳到了 2.0.0,结果发现只改了几个内部函数名,对外接口完全兼容。
这种随意升降级,直接把下游依赖搞崩了。所以今天好好聊聊语义化版本规范(SemVer),让版本号能真正「说话」。
语义化版本规范(SemVer)是什么
说白了,SemVer 是一套约定:版本号要能表达改动的影响范围。
格式是 主版本号.次版本号.补丁号,比如 2.3.1。
每一个数字的递增,都对应一种明确的变更级别。这样你一看就知道:到底要不要改代码、能不能放心升级。
核心原则:版本号必须按照规则递增,不能乱跳。
- 补丁号(Patch):修复 bug,不新增功能,不影响兼容性
- 次版本号(Minor):新增功能,但向后兼容
- 主版本号(Major):做了不兼容的修改
我自己的习惯是,每次发布前先检查 Changelog,确认变更类型再决定升哪一位。否则很容易出现“主版本号一直 1,但接口早就面目全非”的情况。
版本号组成详解
标准格式:MAJOR.MINOR.PATCH。还可以附加预发布版本和构建元数据:
| 部分 | 含义 | 示例 |
|---|---|---|
| MAJOR | 不兼容的 API 修改 | 从 1.x 升到 2.x |
| MINOR | 向下兼容的新功能 | 从 1.2.x 升到 1.3.x |
| PATCH | 向下兼容的 bug 修复 | 从 1.2.3 升到 1.2.4 |
| 预发布 | alpha/beta/rc,用短横线连接 | 1.2.3-alpha.1 |
| 构建元数据 | 构建标识,用加号连接,不参与优先级 | 1.2.3+build.20231001 |
预发布版本和构建元数据是可选的后缀,我一般在正式发布前用 -rc.1 做候选版本。这样团队内部可以先行验证,而用户知道这还没稳定。
小提示:构建元数据只用于构建信息传递,比如 CI 构建号;它不参与版本优先级比较。比如 1.0.0+build.1 和 1.0.0+build.2 是同一个版本。
如何判断该升主版本、次版本还是补丁版本
很多新手会纠结:到底升哪一位?我总结了一个简单决策流程:
- 有没有 API 不兼容的改动? 有 → 升主版本号(MAJOR)。
- 有没有新增功能,且向下兼容? 有 → 升次版本号(MINOR)。
- 只有 bug 修复,功能没变? → 升补丁号(PATCH)。
- 如果以上都发生了? 取最高级别:哪怕只加了一个 bug,但同时 API 不兼容,也必须升主版本。
举个例子:假如你修复了登录闪退(补丁),又加了个新接口(次版本),同时把老接口签名改了(主版本)——那你只能升主版本号,并把次版本和补丁版本重置为 0,变成 新MAJOR.0.0。
我之前在一家金融 SDK 公司审计过他们的升级流程。他们有个历史包袱:版本号从 1.9.0 升到 1.10.0 时,误认为次版本号可以随意跳,结果下游有家券商因为依赖了 ^1.9.0 自动拉到了 1.10.0,新功能里有个废弃接口直接报错。那一次我被叫去救火……后来我给他们强制加了规则:每次版本号变化都要经过 兼容性检查清单。
注意:SemVer 只适用于公开 API。如果你发的 SDK 没有稳定接口(比如内部工具库),建议先用 0.x 版本表示开发阶段。0.x 下可以随时做不兼容改动,但也要在文档里明确告知。
预发布版本与优先级
当版本号包含预发布标签时,优先级规则会变:
1.0.0-alpha<1.0.0-alpha.1<1.0.0-beta<1.0.0-rc<1.0.0- 数字比较:
1.0.0-alpha.2>1.0.0-alpha.10?不对!是字符串比较,所以alpha.10比alpha.2小(因为 '2' 字符比 '1' 大?别急,看规则) - 实际规则:先用点分隔成段,数值部分按数值比较,字母部分按字母序比较。例如
alpha.2和alpha.10:2<10(数值比较),所以alpha.2<alpha.10。
我建议尽量用纯数字做预发布版本号,比如 -rc.1、-rc.2,避免字母数字混合带来混乱。
常见陷阱与避坑指南
我曾经遇到的几个经典错误:
- 「我发誓没改接口」——骗人:有人以为改了内部实现不影响版本号,但实际上修改了
@deprecated标记的接口行为也算不兼容。 - 「先升主版本,后面再降回来」:不可能的。版本号必须单向递增。一旦发布 2.0.0,就不能再发布 1.9.1,除非你走另一个分支。
- 「构建元数据影响优先级」:不,
1.0.0+build.100和1.0.0是同一版本,不能用来区分灰度。
避坑指南:每次发布前,务必对照 API 变更清单,逐项确认兼容性。最好在 CI 中自动跑兼容性测试(比如 Java 用 japicmp,Go 用 gorelease),从工具层面防止手滑。
一个简单的 SemVer 决策流程图
下面这张图展示了我自己常用的版本号递增决策流程:
这个流程你可以在发布前快速过一遍,避免拍脑袋升版本。实际使用中,我还会配合 semver compare 工具(比如 npm 的 semver 包)来自动判断版本关系,减少人工失误。
// 一个简单的 SemVer 正则(用于校验格式)
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
// 你可以在 CI 脚本中用这个来校验版本号是否符合 SemVer
我个人的最佳实践:每次发布前,除了版本号,还要更新 CHANGELOG.md,按 ## [MAJOR.MINOR.PATCH] - YYYY-MM-DD 格式记录。这样审计时一眼就能看出版本升级周期和变更内容。
总结一下:版本号不是装饰,它是你和下游开发者之间的一份契约。尊重 SemVer,就是尊重彼此的信任。下次你看到某个库突然从 1.2.3 升到 2.0.0,你可以直接告诉同事:“别慌,先看 Changelog,大概率 API 有大变化。如果只是 bug 修复,那就是他们不遵守规范了。” 嗯,这也就是为什么我们要在审计中较真版本号的原因。