[译]语义化版本管理

译自:语义化版本管理 2.0.0

摘要

对于一个给定的版本号 MAJOR.MINOR.PATCH (主、次、补丁),其变化的规律是:

  1. MAJOR version (主版本) 会在 API 发生不可向下兼容的改变时增大。
  2. MINOR version (次版本) 会在有向下兼容的新功能加入时增大。
  3. PATCH version (补丁版本) 会在bug以向下兼容的方式被修复时增大。

我们还可以根据预发布、构建元数据 (build metadata) 的实际需求,在 MAJOR.MINOR.PATCH 格式之上扩展出额外的标记。

介绍

在软件管理领域,存在一个叫做“dependency hell (依赖地狱)”的坑。随着系统越变越大,你集成了越多的软件包,也越发觉得,有一天,你会陷入绝望。

对于有很多依赖关系的系统来说,发布新版本的软件包会迅速变成一场噩梦。如果依赖性规定得太紧,你会陷入 version lock (版本锁,即每次软件包的升级无法产生新的版本)。如果依赖性规定得太松,你会不可避免的面对 version promiscuity (版本泛滥,假设未来版本是需要考虑兼容性的)。当 version lock 和 version promiscuity 让你的项目无法安全而又轻松的向前推进时,这就是所谓的 dependency hell。

作为一种解决问题的办法,我提出了一套简单的规则和要求来表明版本号该如何确定和增加。这套规则基于但不仅限用于已经广泛存在的开源闭源软件的一般实践。为了让这个系统工作起来,你首先需要声明一个公有的 API,它可以由文档组成或在代码层面强制实现,且必须是清晰准确的。一旦你标识了你的公有 API,你就可以通过不同的版本号的增加来交流 API 的各种改变。设想一个形如 X.Y.Z 的版本,不影响 API 的 bug 修复会增大补丁版本,向下兼容的 API 增加或改变会增大次版本,而不兼容的 API 改变会增大主版本。

我把这套系统称作“语义化版本管理”。在这套系统之下,版本号及其改变传递了代码背后的含义,以及每个相邻版本之间的变化。


语义化版本管理规范 (SemVer)

原文中的关键字 "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" (必须、禁止、要求、应该、不应该、推荐、可以、可选的) 在 RFC 2119 中有相应的解释和描述。

  1. 使用语义化版本管理的软件必须声明一个公有 API。该 API 可声明于代码或文档中,并且精确而全面。
  2. 一个普通的版本号必须遵循 X.Y.Z 的格式,其中 X、Y、Z 都是非负整数,禁止包含前置的 0。X 是主版本,Y 是次版本,Z 是补丁版本。每个元素必须以数字形式变大。比如:1.9.0 -> 1.10.0 -> 1.11.0。
  3. 一旦版本化的软件包被发布,那么该版本的内容就禁止被更改了。今后的任何变化都必须通过新版本的发布而产生。
  4. 主版本 0 (0.Y.Z) 表示初始开发阶段。在这个阶段任何事情都是可以更改的,公有 API 不应该被认为是稳定的。
  5. 1.0.0 版本定义了公有 API。从该版本往后,版本号的增加取决于发布的公有 API 及其变化。
  6. 只有当向下兼容的 bug 修复被引入时,补丁版本 Z (x.y.Z | x > 0) 必须被增大——bug 修复的定义是通过内部改变修复错误的特性。
  7. 如果新的向下兼容的功能被引入公有 API、如果任何公有 API 功能被废弃,次版本 Y (x.Y.z | x > 0) 必须被增大。如果显著的新功能或改进通过私有代码被引入,次版本可以被增大。次版本的被增大可以包括补丁级别的变化。当次版本被增大时,补丁版本必须被重置为 0。
  8. 如果任何不向下兼容的变化被引入时,主版本 X (X.y.z | X > 0) 必须被增大。主版本的被增大可以包括次级别和补丁级别的变化,当主版本被增大时,次版本和补丁版本必须被重置为 0。
  9. 一个预发布版本可以表示为在补丁版本之后加上一个连字符,再加上一系列由点分隔开的标识符。标识符必须仅由 ACSII 字母数字和连字符组成 [0-9A-Za-z-]。标识符禁止为空。数字标识符禁止有前置的 0。预发布版本比相应的普通版本重要性更低。一个预发布版本意味着该版本不稳定,也许没有满足相应版本既定的兼容性需求。比如: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+01、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。

为什么要使用语义化版本管理?

这并不是什么新的或革命性的东西。事实上你的做事习惯可能已经很接近它了。但问题是“接近”是不够的。如果不接受一些正式的规范,版本号对依赖性管理是没有实际意义的。基于上述想法,给定名词和定义,它就变得易于交流。一旦这些意图变得清晰,灵活 (且不过分的) 的依赖性规范就会最终产生。

一个简单的例子就可以向人们展示语义化版本管理会使依赖地狱成为历史。想像一个叫做“消防车”的库,它需要一个名叫“梯子”的经过语义化版本管理的软件包。当消防车被创建时,梯子的版本是 3.1.0。因为消防车使用一些在 3.1.0 被首次引入的功能,你可以安全的制定梯子的依赖关系为大于 3.1.0 且小于 4.0.0。现在当梯子的版本 3.1.1 和 3.2.0 可用时,你可以把它们发布到你的软件包管理系统之中并很清楚它们可以和现存的依赖性软件和平共处。

作为一个有责任感的开发者,你一定想要验证任何被公示的软件包的功能升级。在现实世界中这是一个混乱的地方,我们对此需要警惕但又无能为力。我们能做的就是让语义化版本管理提供给你一个清晰的路线,去发布和升级软件包,无需在依赖性软件包的不同版本中翻滚,省去你的时间和烦恼。

如果这一切是你所渴望的,你需要做的就是声明你开始遵循上述规则来进行语义化的版本管理。在你的 README 中附带这个网站的链接,让其他人了解这个规则,并从中获益。

问答时间

  • 我应该如何处理 0.y.z 的初始化开发阶段呢?

最简单的事情就是当你开始初始化开发时,从 0.1.0 开始,然后在后续的发布过程中不断增加次版本。

  • 我怎么知道什么时候发布 1.0.0?

如果你的软件已经用到了产品环境,它可能应该已经是 1.0.0 了。如果你有一个稳定的用户依赖的 API,它应该是 1.0.0 了。如果你担心很多向下兼容的问题,它可能应该已经是 1.0.0 了。

  • 这和快速开发快速迭代的理念是否有冲突?

主版本 0 是快速开发时期。如果你每天都在改变 API 你应该还处在 0.y.z 或一个独立的开发分支中,这是为下一个主版本服务的。

  • 如果最小的向下不兼容的公有 API 改变都会导致新的主版本,那么我岂不是很快就做到 42.0.0 了?

这是一个开发责任感和长远意识的问题。不兼容的改变不应该很轻松就被引入到一个被大量依赖的软件当中。这必定导致升级的代价昂贵。改变主版本才可以发布不兼容的改变也在变相的促使你思考这一变化带来的影响及其投入产出比。

  • 写出整个 API 的文档是一项艰巨的工作!

作为一名开发者,撰写软件文档以供其他人使用是你的责任。管理软件复杂度是保障一个项目高效运作的及其重要的部分,如果没人知道如何使用你的软件,什么方法使用起来比较安全,项目将会变得很困难。长期来看,语义化版本管理以及一个被良好定义的公有 API 能够保障每个人每件事都运转顺利。

  • 如果我在此版本中不小心发布了一个不向下兼容的改变,我该怎么办?

当你意识到你打破了语义化版本管理规范之后,立即修复这个问题并且发布一个新的次版本去修复此问题并恢复向下兼容性。甚至在这个周期里,不要接受任何其它版本的发布。如果方便合适的话,记录下出错的版本并向你的用户告知这一问题以便他们警惕这个出错的版本。

  • 如果我更新了我自己的依赖性但是没有改变公有 API,我该怎么做?

这回被认为是兼容的,因为它并没有影响到公有 API。软件显式依赖你的软件包相同的依赖,应该有自身的以来规范,作者自然会注意任何冲突。决定这个改变是一个补丁级别还是次级别,取决于你把依赖关系的改变用在了修复一个bug上还是用在了引入新功能上。我通常会在后期期待额外的代码,很明显这是一个次级别的增大。

  • 如果我不经意改变了公有 API,而且它并没有遵循版本号的改变 (比如把补丁发布引入到了一个错误的主版本),我该怎么办?

做出做合理的判断。如果你有一大群用户,因为有意把行为改回到公有 API 会收到剧烈的影响,那么最好发布一个主版本,尽管实际的改动也许只是发布一个补丁。记住,语义化版本管理就是通过版本号的变化传递信息。如果这些改变对用户很重要,就用版本号去通知他们。

  • 我该如何处理废弃的功能?

废弃已存在的功能是软件开发的一个正常部分。而且它经常需要提前行动。当你废弃部分你的公有 API 时,你应该做两件事:(1) 更新你的文档,让用户知道这一变化,(2) 创建一个此版本发布的任务。当你发布主版本完全移除功能之前,至少要有一个次版本发布,该发布包含废弃的动作,以便让用户可以平稳的过度到新的 API。

  • 语义化版本管理是否存在版本字符串的长度限制?

没有限制,但要合理使用。比如一个 255 字符的版本字符串就算是有点长了。同样的,规范系统可以实行自己的字符串长度限制。

关于

语义化版本管理规范由 Gravatars 发明者、Github 的联合创始人 Tom Preston-Werner 撰写。 如果你想留下宝贵意见,请来 Github 开一个 issue 吧

License

Creative Commons - CC BY 3.0

2 条早期评论

  • 姓名
    thankwsx
    评论日期
    2013/12/17 05:31:29
    不错,有没有推荐加入到官方网站去? 目前官网没有简体中文版的翻译哦
  • 姓名
    thankwsx
    评论日期
    2013/12/17 05:36:50
    有没有将这个翻译推荐给官方啊?官网目前没有中文简体的版本哦。