← 返回文章列表

语义化版本实践:MAJOR.MINOR.PATCH 与依赖范围怎么定

命令行入门

三位数字各自代表什么

位置何时递增例子
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 也可能破坏兼容。

四个常见坑

  1. 破坏性改动只升 MINOR:会让依赖方在例行升级时突然挂掉,这是最常见的失信行为;
  2. 不提交锁文件:没有 package-lock / Cargo.lock / go.sum 就没有可复现构建;
  3. 依赖范围过宽:上游一次意外发布就影响所有下游;
  4. 把版本号当营销手段:为了「大版本」而跳号,会让语义失去意义。

落地建议

  • 在 CI 中检查版本号是否随改动递增;
  • 用 conventional commits 之类的约定,让版本号能从提交历史推导;
  • 每次发布生成变更日志,明确标注破坏性变更与迁移步骤;
  • 对外 API 先标 deprecated,至少一个 MINOR 版本后再移除。

实战案例:三个升级事故

  1. “例行升级把服务搞挂了”:上游把破坏性改动塞进了 MINOR。修复:锁定版本,依赖升级走 CI 回归,不要盲目使用 ^。
  2. “本地能跑、CI 构建结果不同”:没提交锁文件,装到了不同版本。修复:提交 lockfile,让 CI 用确定性安装。
  3. “为了造势跳大版本号”:语义被营销破坏,使用方无法据此判断升级风险。修复:严格按兼容性递增。

常见问题

内部服务也要语义化版本吗?需要,尤其是被多个团队依赖时;内部也可以只用日期版本,但要在同一体系内保持一致。修安全问题算哪一级?修复本身是 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 精确锁定;
  • 锁文件必须提交:没有锁文件时同一份依赖声明在不同机器上可能装出不同结果。

发布流程里的版本动作

  • 合并即升版本,避免多个改动挤在一次发布里难以追溯;
  • 变更日志按“新增 / 修复 / 破坏性变更”分类,破坏性变更必须置顶并给出迁移示例;
  • 发布不可变产物并以版本号归档,回滚时直接切换版本而不是重新构建。

依赖升级的节奏

依赖升级不要“攒在一起”。安全补丁类即使主版本不变也要及时跟进;次版本升级按周或按迭代批量验证;主版本升级单独排期,先读迁移指南并补齐测试。把升级做成定期小批量,远比半年一次的大跳跃风险低。