Semantic Versioning 2.0.0

本文章翻译自 https://semver.org/
中文版在这里 https://semver.org/lang/zh-CN/,但读起来太困难。这里搞一版更教程化, 但又不失严谨的。

一个良好的版本号的结构与改动规则,向用户传达了我们软件中改动的影响级别。
作者在这篇官方文档里,给出了对 semver 的精准定义。

搬运正文

概述

给定一个版本号: MAJOR.MINOR.PATCH (主版本号.次版本号.补丁版本号):

  1. 当你修改了 API,使其(与之前版本)不兼容时,递增 MAJOR,
  2. 当你用向后兼容的方式加了些功能时,递增 MINOR,
  3. 当你用向后兼容的方式解决了几个 Bug 时,递增 PATCH

预发布版本 附加的标签,以及与编译相关的额外信息,可以作为 MAJOR.MINOR.PATCH 这种格式的扩展,加到版本号的后面。

引言

在软件管理的世界里, 有个可怕的地方, 叫 "Dependency Hell"[1]. 你的系统规模增长的越大, 集成到系统里的软件包越多, 你就越有可能发现, 某天, 你已经深深的陷入了这种绝望之地.

在那些有很多依赖包的系统里, 发布新的软件包版本很快就会变成一个梦靥. 如果依赖要求太紧, 你可能会陷入 "版本锁定" [2]. 如果版本要求太松, 你又不可避免的遭受"版本滥交"[3]之痛. [4]. 而所谓的 "Dependency Hell", 就是当 "版本锁定" 和/或 "版本滥交" 阻止你简单, 安全的推动项目前行的时候, 你的处境.

作为这个问题的一个解决方案, 我提议一套简单的规则和要求, 以规定如何分配和增长版本号. 这些规定基于, 但不限于已经在各种闭源, 开源软件中广泛使用的普遍惯例. 要想这套理论奏效, 首先你得声明一个公开的 API. API 可能是由文档组成的, 也可能是直接使用代码实现的. 但不管怎样, 重要的是这个 API 是清晰和精确的. 一旦你确定了你的 API, 使用增加特定的版本号的方式, 来传达 API 的改动. 考虑一个 X.Y.Z (MAJOR.MINOR.PATCH) 的版本号格式: 那么, 不影响 API 的Bug修复: 递增补丁版本号Z; 向后兼容的 API 的添加或修改: 递增次版本号; 不向后兼容的 API 的修改: 递增主版本号.

这套理论我称作 "Semantic Versioning", 那些个版本号以及他们的改变传达着与 底层代码, 以及从一个版本到另一个版本改了什么 相关的含义.

Semantic Versioning 规范

  1. 使用 Semantic Versioning 的软件 必须 声明一个公共的 API. 这个 API 可能是定义在代码里的, 或者仅仅存在于文档里, 不论用什么方式实现, 它都必须精确而全面.

  2. 一个正常的版本号必须使用 X.Y.Z 的格式, 其中 X, Y, 和 Z 都是非负的整数, 并且 必须不能 包含前导零.
    X 是主版本号, Y 是次版本号, 而 Z 是补丁版本号. 每个元素都必须以数字的方式递增. 举例: 1.9.0 -> 1.10.0 -> 1.11.0.

  3. 一旦一个打了版本的包被发布出去了, 那个版本的内容就 不能 再修改了. 任何修改 必须 作为一个新的版本重新发布.

  4. 主版本为零 (0.y.z) 的版本, 是用作初始开发阶段的. 任何东西都可能在任意的时间被更改. 这时候我们不应该认为它的 API 是稳定的.

  5. 1.0.0 版本表明对外公开 API 的形成. 从此之后, 版本号的递增方式取决于这个公开的API, 以及它如何修订.

  6. 补丁版本号Z (x.y.Z | x > 0) . 如果只有向后兼容的bug修复被引入的化, 补丁版本号 Z 必须 递增. "Bug修复"是指一个修正错误行为的内部修改.

  7. 次版本号Y (x.Y.z | x > 0). 如果一个新的, 向后兼容的功能被引入到了公开 API 里, 次版本号 必须 递增. 如果公开 API 的任何功能被标记为 "已弃用的", 次版本号 必须 递增. 如果大量的新功能或改进被引入到私有代码里的时候, 次版本号 可以 递增. 次版本号的改变 可以 包含补丁级别的改动. 当递增了次版本号的时候, 补丁版本号 必须 清零.

  8. 主版本号X (X.y.z | X > 0). 如果任何的向后不兼容的改动被引入到了公开 API中, 主版本号 必须 递增. 它的递增 可以 包含次版本和补丁级的改动. 当主版本号递增时, 次版本号和补丁版本号 必须 清零.

  9. 一个预发布版本 可以 通过在补丁版本号后面追加一个短线, 以及一系列的用点分割的标识符 来描述. 标识符 必须 仅包含 ASCII 的 阿拉伯数字和短线 [0-9A-Za-z-]. 标识符 必须不 为空. 数字标识符 不能 包含前导零. 预发布版本比对应的正常版本的优先级要低. 预发布版本表明, 它不稳定, 并且可能不满足其对应的正常版本所预定的兼容性要求. 例子: 1.0.0-alpha, 1.0.0-alpha.1, 1.0.0-0.3.7, 1.0.0-x.7.z.92.

  10. 编译时的附加信息, 可以 通过在补丁版本号后面追加一个加号, 以及一系列的用点分割的标识符 来描述. 标识符 必须 仅包含 ASCII 的 阿拉伯数字和短线 [0-9A-Za-z-]. 标识符 必须不 为空. 在比较版本优先级的时候, 编译附加信息 应该 被忽略. 因此, 两个只有编译附加信息不同的版本, 具有相同的优先级. 编译附加信息的举例: 1.0.0-alpha+001, 1.0.0+20130313144700, 1.0.0-beta+exp.sha.5114f85.

  11. 优先级是指在排序的时候怎样比较不同的版本. 计算优先级的时候, 必须 将版本号以 "主版本号", "次版本号", "补丁版本号", "预发布标识符" 的顺序拆分. 优先级取决于, 在从左至右依次比较这些个标识符的时候, 发现的第一个差别. "主版本号", "次版本号", "补丁版本号" 总是以数字的方式参加比较. 举例: 1.0.0 < 2.0.0 < 2.1.0 < 2.1.1.
    当"主版本号", "次版本号", "补丁版本号" 都相同的时候, 预发布版本比正常的版本优先级要低. 举例: 1.0.0-alpha < 1.0.0.
    如果两个预发布版本有相同的 "主版本号", "次版本号", "补丁版本号", 优先级就 必须 通过比较点分割的标识符来确定, 从左至右依次比较, 直到发现一个不同: 只有数字的标识符号以数值高低比较, 有字母或连接号时则逐字以 ASCII 的排序来比较. 数字的标识符号比非数字的标识符号优先级低. 若开头的标识符号都相同时, 字段比较多的预发布版本号优先层级高. 举例: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0.

为什么使用 Semantic Versioning ?

(我的这套理论) 并不是什么新的或者革命性的点子,实际上,很可能你已经做了些很接近这个的工作。但问题在于,仅仅是“接近”并不够。如果不遵守有点儿"官方"的协定,版本号对于管理依赖来说基本没什么用。通过给我上面的思想命名,并给予其清晰的定义,将你的意图传达给你的软件的用户就变得很简单了。一旦意图清晰明了,最终一个灵活的(但也不是过于灵活的)软件依赖要求就可以搞定了。
我们可以通过一个简单的例子,展示一下 Semantic Versioning 可以让 “Dependency Hell” 成为历史。考虑一个叫做 “消防车” 的版本库,他需要一个使用了 Semantically Version 的软件包 “梯子”。当 “消防车” 刚创建的时候,“梯子”在3.1.0 版本。因为“消防车”使用了 “梯子” 3.1.0 新引入的功能,你可以简单的指定,你的“消防车” 对“梯子”的依赖要求是:大于等于 3.1.0 ,但小于 4.0.0 。现在,梯子 "3.1.1" 和 "3.2.0" 好了,你可以把他们发布到你的软件包管理系统,并且你知道他们跟现有的、跟它有依赖关系的包是兼容的。
作为一个负责人的开发者,你当然想验证任何软件包升级都是正常的、跟你宣传的一样。但现实世界是一团乱麻,对此我们除了小心再小心之外没有更好的办法。你能做的,是让 “Semantic Versioning” 提供你一个合理的方法去发布、升级软件包,而不必去搞许多新版的依赖包,节省了你的时间,免去了许多麻烦。
如果所有这些听起来挺给力的,要开始使用 “Semantic Versioning” 的话,你只需要声明你正在这么搞,然后遵守这些规范就好了。把这个网站链接到你的 README 文档里,以便其他其他人也可以了解这些规则,并从中受益。

FAQ

  • 在初始开发阶段,怎么去处理 0.y.z 的版本号?
    一个简单的做法是, 使用 0.1.0 作为第一版初始开发版本号,然后为随后的发布包递增次版本号(minor version)
  • 我怎么知道什么时候发布 1.0.0 版?
    如果你的软件已经在生产环境了(已经上线了), 很可能已经 1.0.0 了。如果你做好了一个 从此用户可以信赖的、稳定的API版本,你应该发布1.0.0。如果你正为向后兼容的事情心烦,你应该早就 1.0.0 了。
  • 这东西难道不会阻挠快速开发、快速迭代吗?
    为零的主版本就是为了快速开发的。如果你每天都在改 API,你要么还在 0.y.z,要么在另外一个开发分支上,为下一个主版本做准备。
  • 如果,哪怕是微小的 API 的不兼容改动,主版本都要蹦,我岂不是很快就到 42.0.0 版了?
    这是个关于为开发负责,以及前瞻性的问题。在有许多代码依赖之的软件中,不应该轻率的做不兼容的改动。升级招致的代价可能是相当大的。不得不通过递增主版本号来发行不兼容的改版,意味着你将充分考虑改动所带来的影响,并且评估所涉及的 成本/收益 比。
  • 整理个 API 的文档太费事儿了!
    为供他人使用的软件编写适当的文件,是你作为一名专业的开发者应尽的职责。“管理项目复杂度” 是保持项目高效的非常重要的一部分,而如果没有人知道如何使用你的软件,或者不知道哪些函数可以放心的调用的话,就不好做。Semantic Versioning,以及对 良好定义的API 的坚持,可以让每个人、每件事情都顺利进行。
  • 要是我不小心把一个不向后兼容的改动当成一个次版本号发布了怎么办?
    一旦发现你破坏了 Semantic Versioning 规范,马上解决这个问题,然后发布一个新的次版本,以恢复向后兼容。即使在这种情况下,直接修改已经发行的版本也是不可接受的。如合适,在文档里写明这个有问题的版本,并将这个问题告知你的用户,以便用户知晓这个出问题的版本。
  • 如果我在没有更新API的前提下,更新了我自己(软件)的依赖,应该怎么做?
    由于没有影响到公共 API,这将被当做是兼容的。那些使用了跟你一样的依赖包的软件,应该也有自己的依赖要求,并且如果有冲突的话,他们的作者会注意到的。要判定改动是属于补丁级别还是次版级别,要看你更新依赖包是为了修复Bug,还是添加新功能。对于后者,我通常觉着会有额外的代码,这种情况下,显然是一个次版本号级别的递增。
  • 如果我变更了公共 API 但无意中未遵循版本号的改动怎么办呢?(意即在补丁级的发布中,误将重大且不兼容的改变加到了代码之中)
    自行做最佳的判断。如果改回 API 预期的行为将强烈的影响你的大量受众,那么可能最好再发一个主版本吧,即使这个修复仅仅被当做一个补丁版本。记住,Semantic Versioning 所做的就是,通过版本号的改动传达含义。若这些改变对你的使用者很重要,那就通过版本号来告知他们。
  • 我该如何处理即将弃用的功能?
    弃用现存的功能,是软件开发中正常的一部分,也通常是向前发展所必须的。当你弃用部份 API 时,你应该做两件事:(1)更新你的文档让使用者知道这个改变(2)发布一个新的、仍然包含这些已经弃用的API 的次版本。在你从新的主版本里完全移除这些已弃用的功能之前,至少要有一个次版本 仍然包含这些已经弃用的 API,这样使用者才能平滑地转移到新版 API。
  • Semantic Versioning 对于版本的字串长度是否有限制呢?
    没有,但自行判断。举例来说,一个包含255个字符的版本字符串很可能太过分了。并且,特定的系统对于字串长度可能会有他们自己的限制。

  1. "依赖地狱". 因为不好翻译, 就不译了.

  2. "version lock" (如果不为每一个依赖包都发布一个新版本, 就没办法升级某个软件包).

  3. "version promiscuity" (承担了与太多未来版本兼容的责任, 远远超过合理的需要).

  4. 依赖过紧举例: 假设软件 A 依赖软件 B, 声明 A 的当前版需要 B 的 v1.1 版本, A 的下一个版本需要 B 的 v1.2 版本. 这就过紧了. 这样如果要将A升级到下一个版本, 你就不得不同时发布 B 的 v1.2 版本; 依赖过松举例: 声明 A的当前版本需要B, 只要 B 的版本大于 v1.1 即可. 这样子A 负担过重了, 处理与太多的B的未来版本的兼容问题, 没什么必要.

推荐阅读更多精彩内容