三位数字各自代表什么
| 位置 | 何时递增 | 例子 |
|---|---|---|
| MAJOR | 不兼容的 API 变更 | 删除接口、改参数含义、改返回结构 |
| MINOR | 向后兼容的新增功能 | 新增可选参数、新增接口 |
| PATCH | 向后兼容的修复 | 修 bug、补文档、性能改进(不改语义) |
判断的核心是对使用方是否兼容,而不是改动有多大。重构了一万行但对外行为不变,就只是 PATCH。记住版本号的读者是使用方:它回答的是「我能不能安全升级」,而不是「这次改了多少」。
0.x 阶段与预发布
- 0.x.y:公共 API 尚未稳定,MINOR 也可以包含破坏性变更。使用方不应默认 0.x 可安全升级;
- 预发布:用
1.0.0-alpha.1、-rc.1表示,排序低于对应的正式版; - 构建元数据:
1.0.0+build.7,不参与版本优先级比较。
依赖声明里的符号
| 写法 | 含义(以 1.2.3 为例) | 风险 |
|---|---|---|
| 1.2.3(精确) | 只接受这一个版本 | 最安全,但需手动跟进补丁 |
| ~1.2.3 | 允许 1.2.x(PATCH 级) | 低 |
| ^1.2.3 | 允许 1.x.y(MINOR 级) | 中,取决于上游是否守规矩 |
| * 或 latest | 任意版本 | 高,构建不可复现 |
注意 ^0.2.3 在多数生态里只等价于 0.2.x,因为 0.x 的 MINOR 也可能破坏兼容。
四个常见坑
- 破坏性改动只升 MINOR:会让依赖方在例行升级时突然挂掉,这是最常见的失信行为;
- 不提交锁文件:没有 package-lock / Cargo.lock / go.sum 就没有可复现构建;
- 依赖范围过宽:上游一次意外发布就影响所有下游;
- 把版本号当营销手段:为了「大版本」而跳号,会让语义失去意义。
落地建议
- 在 CI 中检查版本号是否随改动递增;
- 用 conventional commits 之类的约定,让版本号能从提交历史推导;
- 每次发布生成变更日志,明确标注破坏性变更与迁移步骤;
- 对外 API 先标 deprecated,至少一个 MINOR 版本后再移除。
实战案例:三个升级事故
- “例行升级把服务搞挂了”:上游把破坏性改动塞进了 MINOR。修复:锁定版本,依赖升级走 CI 回归,不要盲目使用
^。 - “本地能跑、CI 构建结果不同”:没提交锁文件,装到了不同版本。修复:提交 lockfile,让 CI 用确定性安装。
- “为了造势跳大版本号”:语义被营销破坏,使用方无法据此判断升级风险。修复:严格按兼容性递增。
常见问题
内部服务也要语义化版本吗?需要,尤其是被多个团队依赖时;内部也可以只用日期版本,但要在同一体系内保持一致。修安全问题算哪一级?修复本身是 PATCH;若修复改变了行为,则应升 MAJOR。依赖库不遵守语义化版本怎么办?锁定到具体版本,并在升级时人工回归。
版本号速查与常见歧义
1.4.2:主版本·次版本·修订号;次版本加 1 表示向后兼容的新能力,修订号加 1 表示兼容的问题修复;- 0.x 不算稳定:主版本为 0 时任何变更都可能有破坏性,生产依赖应锁定精确版本或使用锁文件;
- 预发布版本:
1.0.0-beta.1优先级低于1.0.0,且不带+build元数据参与比较; - 范围符:
^1.2.3允许 1.x 内的升级,~1.2.3只允许 1.2.x,1.2.3精确锁定; - 锁文件必须提交:没有锁文件时同一份依赖声明在不同机器上可能装出不同结果。
发布流程里的版本动作
- 合并即升版本,避免多个改动挤在一次发布里难以追溯;
- 变更日志按“新增 / 修复 / 破坏性变更”分类,破坏性变更必须置顶并给出迁移示例;
- 发布不可变产物并以版本号归档,回滚时直接切换版本而不是重新构建。
依赖升级的节奏
依赖升级不要“攒在一起”。安全补丁类即使主版本不变也要及时跟进;次版本升级按周或按迭代批量验证;主版本升级单独排期,先读迁移指南并补齐测试。把升级做成定期小批量,远比半年一次的大跳跃风险低。