软件开发的行家们对版本发布流程和节奏都非常熟悉,以至于他们会将这些知识内化,并认为每个人都应该了解这些“显而易见的事情”。但事实并非如此,对于外行来说,这就像雾里看花一样模糊不清。因此,为了整个 Julia 社区,甚至其他编程语言社区,我认为有必要将 Julia 的开发过程清晰地记录下来。在这篇文章中,我将阐述:
不同类型的版本
每种版本允许和不允许的修改
版本发布流程的各个阶段
如何根据风险承受能力选择合适的版本
发布流程中的各个阶段和关键事件
这些材料是从discourse论坛和Slack协作交流群中收集整理的。所有信息都是公开的,我只是将它们集中到一起。如果大家觉得这篇文章很有帮助,我们会考虑将其发展成一份正式的文档。从宏观上来说,Julia 遵循SemVer标准制定的“语义化版本”。但是 SemVer 在微观上提供了很多自由度,供用户自行解释。这篇文章就是为了填补这些微观细节而存在的。
SemVer 的版本号格式为**主版本号.次版本号.修订号**。Julia 的补丁版本会增加版本号的最后一位,也就是**修订号**。例如,从1.2.3
到1.2.4
表示发布了一个补丁版本。
根据 SemVer,补丁版本只能包含 bug 修复、低风险的性能改进和文档更新。当然,对于什么是 bug 修复,不同的人会有不同的理解。造成这种分歧的原因是有些人会将 bug 当成特性,并在此基础上编写代码。总的来说,我们发布补丁版本时会非常谨慎,并使用 PkgEval 来确保尽可能少的现有代码遇到兼容性问题[1]。我们有理由相信,用户可以放心升级到最新的补丁版本。
[1] | PkgEval工具可以运行所有 Julia **第三方库**(package)的测试套件。它确保我们不会无意中造成兼容性问题。一旦发现兼容性问题,我们会一方面检查我们的版本是否违反了 SemVer,另一方面(无论责任方是谁)向该第三方库发送**pull 请求**(pull request,PR)。 |
我们认为,除非是为了修复某个 bug,否则补丁版本也应该避免修改内部代码。虽然通常来说在任何版本中,对公开**应用程序接口**(API)以外的部分进行修改都是可以接受的,但我们仍然谨慎地避免这样做,以将不兼容的风险降到最低。
通常,补丁版本大约每月发布一次,并且会基于当前的几个**活跃**(active)版本**分支**(branch)(稍后详细介绍)。如果当月没有足够的 bug 修复,那么这个月也可能跳过。
大约在发布补丁版本五天前,我们会在**反向移植**(backport)分支上运行 PkgEval。如果一切顺利,我们会将其**合并**(merge)并**冻结**(freeze)这些版本分支,并在 discourse 上宣布可以开始测试了。如果接下来的五天里一切正常,这些版本分支会被打上新的**版本标签**(tag)。
次要版本会增加版本号的中间一位,也就是**次版本号**。例如,从1.2.3
到1.3.0
表示发布了一个次要版本。
次要版本包含 bug 修复、新**特性**(feature)和一些“小改动”。这些小改动理论上可能导致兼容性问题,但实际上很少引起兼容性问题。更重要的是,我们通过 PkgEval 完全避免了兼容性问题的发生。
次要版本也会大量地**重构**(refactor)内部代码。之前提到过,我们规定补丁版本只允许在修复 bug 的前提下**小范围**地重构内部代码,次要版本就成为了我们**大范围**重构的工作场所。如果你的程序依赖于我们的内部代码而不是公开的应用程序接口,那么你可能会遇到兼容性问题。事实上,你之所以在补丁版本中幸免于难,是因为我们在补丁版本中执行了比 SemVer 更严格的标准。任何出于某种需要而依赖我们内部代码的用户,在升级次要版本时都应该格外小心。
次要版本每**四个月**发布一次,也就是每年发布三次。每四个月,我们在 discourse 上宣布当前开发版本将在两周后冻结。在冻结当天,我们为次要版本创建release-1.3
分支[2]。该分支会被打上版本标签,并且不允许再添加新的特性。
[2] | 译者注:release-1.3 是本文写作时的版本分支。 |
主要版本会增加版本号的第一位,也就是**主版本号**。例如,2.0.0
表示发布了一个主要版本。
根据 SemVer,主要版本可以进行大刀阔斧的修改。但是,在现实中,我们非常清楚我们将如何塑造 Julia 的代码,并且不会做出天翻地覆的改变。大部分用户级代码将在 Julia 2.0
版本中完整保留[3]。我们不希望无缘无故地打破所有规则。
[3] | 译者注:2.0 是本文写作时的未来主要版本。 |
主要版本的职责在于修正明显的 API 设计缺陷,每个人都会因为能够摆脱这种糟糕的、令人困惑的 API 而感到高兴。主要版本也允许修改底层代码,这可能会导致某些**第三方库**(package)不兼容,但这却是从根本上改进语言所必须付出的代价。
一些用户喜欢随时更新 Julia 以获得最新最酷的特性。另一些用户甚至乐此不疲地每天重新编译 Julia 的 master 分支,以成为第一个尝鲜的人。还有一些用户恰恰相反,一年到头也懒得升级一次。理想情况下,我们希望为每个存在的次要版本永远提供 bug 修复服务。如果我们拥有无限的资源,我们会将每个 bug 修复都反向移植到每个兼容的版本分支上。理想很丰满,现实很骨感。我们的资源只能让我们维护几个活跃的版本分支。因此,我们退而求其次,决定在任何时间点上最多只维护四个活跃分支:
master
分支:这是所有新特性的发源地,大部分 bug 修复的栖息之地,也将在未来成为具有划时代意义的2.0
版本的摇篮。
**不稳定版本**(unstable release)分支(当前为release-1.3
):在这个分支上,新特性已经被冻结,但 bug 修复和性能改进仍然被允许。通常,bug 修复首先在master
上完成,然后反向移植到该分支。时机成熟后,该分支会被打上版本标签(当前为1.3.0
),并以新的稳定版本分支的身份活跃。不稳定版本分支并不是一直存在的:它只存在于**特性冻结**(feature freeze)之后,下一个次要版本发布之前。在此之后它都不会出现,直到四个月后的下次特性冻结。
**稳定版本**(stable release)分支(当前为release-1.2
):这个版本分支跟踪最新发布的次要(或主要)版本。这个分支永远存在,并且通过反向移植从master
获取所有可用的 bug 修复。未来的补丁版本(例如1.2.1
)会基于这个分支创建。当不稳定版本分支升级为新的稳定版本分支时,这个旧的稳定版本分支会被废弃。
**长期支持** (long term support, LTS)分支(当前为release-1.0
):这个稍旧的版本分支在其生命周期内将持续获得 bug 修复。即使某些 bug 修复不能完全反向移植到该分支,我们也会额外花费精力妥善地修复该分支上的 bug。一个旧的 LTS 分支将在另一个分支成为新的 LTS 分支时退役。
现在只剩下一个问题:LTS 分支何时会更换?release-1.0
是我们目前唯一确定的 LTS 分支。在获得四个补丁版本后,该分支相当稳定并被广泛支持。但是,它从master
获得的 bug 修复补丁会越来越少,并且越来越多的第三方库会放弃对其的支持(这些库需要使用 Julia 新版本中的特性)。当合适的时机到来时,我们不得不选择一个新的 LTS 分支并宣布停止维护1.0.x
系列。这个新的 LTS 分支可能是1.4
或1.8
,也可能是2.0
。我们现在还无法预测,但这一天终将到来。幸运的是,即使如此,1.0.x
系列的用户也不必一定要升级版本。他们可以使用这个旧版本,并只与与该版本兼容的第三方库版本交互。到那时,它将成为最稳定、测试最充分的 Julia 版本。所以,只要你不需要新特性,你就可以放心继续无限期地使用它。另外,如果有人或某个组织出于自身利益,愿意继续维护某个旧版本分支,也就是**挑选**(cherry-pick)反向移植并运行 PkgEval 以确保兼容性,我们很乐意接受这些帮助从而发布更多的版本。因此,你始终可以通过自己维护或雇人维护来获得更长期的支持。就目前来说,release-1.0
仍然将继续是一个优秀的、稳定的 LTS 分支。并且,当我们打算更换 LTS 分支时,我们会提前发布大量的**警告**(warning)。
不同的用户有不同的**风险承受能力**(risk tolerance)。一些风险承受能力高的用户可以轻松地发现和报告零星的 bug,并查明为什么某个第三方库与 Julia 的新版本不兼容。另一些风险承受能力低的用户希望使用经过充分测试、广泛兼容的版本。还有一些用户介于这两个极端之间。大致可以将大多数用户根据风险承受能力分为以下四类:
**高风险承受能力**(high risk tolerance):“人生只有一次,我在 master 分支上翩翩起舞。况且,master 分支在未来的相当长一段时间内不会有破坏兼容性的更新[4]。现在 master 只偶尔出现 bug,不过就算出现 bug,我也可以帮忙解决。”
[4] | 译者注:根据前文,破坏兼容性的更新在开发2.0 版本时才会出现。 |
**普通风险承受能力**(normal risk tolerance):“我想要能用的东西,我不想要 master 分支上忽隐忽现的 bug。所以我会坚守最新的稳定版本并打上最新的补丁,这样我的系统既安全又高效。唯一的烦恼是当我使用的第三方库因为依赖淘汰的 Julia 内部代码而在新版本上失效时,我需要等上一段时间第三方库作者才会更新。”
**低风险承受能力**(low risk tolerance):“我保守,厌恶风险。我使用当前的 LTS 分支,因为它已经经历了充分的测试。当 LTS 分支更换时,我将升级到新的 LTS 分支。因为新的 LTS 分支在成为长期支持之前已经经历了数个补丁版本,所以 bug 应该已经被修复,第三方库不兼容问题也应该已经被解决。”
**极低风险承受能力**(very low risk tolerance):“我极端厌恶风险。除了严重的 bug 和安全问题,我从不升级 Julia(或其他任何东西)。我运行一个已经不再被支持的 LTS 版本,但这个版本已经经历了两位数的补丁,相当可靠。如果我需要修复一个新的 bug,我将自己动手反向移植。”
这些不同类型的需求很好地诠释了 LTS 分支的关键特性:
它被充分地打上补丁,非常可靠;
每个想要支持它的第三方库都已经发布了支持它的库版本。
如果一个新的 LTS 分支满足这两个条件,低风险承受能力用户就会升级到该版本,因为他们相信该新的 LTS 分支可靠、经过充分调试,并且所需的第三方库已经向其提供了支持(可能需要同时更新库版本)。我们将从实践中学习新的 LTS 分支在独当一面之前需要滞后稳定分支多少版本。
我们已经讨论了各种版本以及它们允许的修改,但我们还没有深入讨论这些版本的发布流程。在这节里,我将详细描述这些细节,例如从master
上的新特性到次要版本的发布,再到为次要版本发布补丁。在这节里,“bug”一词不仅指代传统意义上的错误代码,也同时指代性能问题(运行效率不可接受的低代码)。在 Julia 语言中,性能至关重要,我们经常将性能问题视为不可绕过的 bug。以下是一连串围绕x.y.0
次要版本展开的各个阶段和关键事件:
开发(development),4个月
在master
分支上
开发新特性,修复 bug 等等。
标记x.y.0-alpha
(非强制)
新版本的早期预览——尚未特性冻结,并且可能存在已知 bug
标记x.y.0-beta
(非强制)
新版本的稍后预览——仍然未特性冻结,并且可能存在已知 bug
x.y.0
特性冻结
创建release-x.y
这个不稳定版本分支
不接受新特性,只接受 bug 修复
新特性会被合并到master
分支,而不是x.y.z
分支
稳定化(stabilization),1-4个月
在release-x.y
分支上
修复所有已知的阻碍发布的 bug
标记x.y.0-rc1
修复所有已知的阻碍发布的 bug
标记x.y.0-rc2
修复所有已知的阻碍发布的 bug
...
标记x.y.0-rcN
一周内没有出现阻碍发布的 bug
标记x.y.0
维护(maintenance),直到宣布x.y
停止维护
在release-x.y
分支上
向后移植 bug 修复到release-x.y
分支上
标记x.y.1
(一到两个月后)
向后移植 bug 修复到release-x.y
分支上
...
一眼望去,你就能发现这是一条漫长的征途。尤其是稳定化阶段,它的耗时忽长忽短,从几周到几个月不等,难以预测。高质量的愿望和按时发布的期待相互冲突,就像一根两头尖的针。一方面,我们不想在调试完成之前就匆忙发布,以免因为粗制滥造而让人失望。另一方面,我们不想因为在调试上花费过多的时间而导致无法按时发布次要版本——尽管我们都知道软件开发,尤其是复杂的程序语言开发,跳票是家常便饭的事。
为了解决这个矛盾,我们想出了一个好主意。如果我们同时进行一个版本的稳定化和下一个版本的开发,我们就有望按时完成版本迭代。每个次要版本的开发阶段占用固定的四个月时间,x.y
版本的开发阶段一结束,x.(y+1)
版本的开发阶段就立即开始。雷打不动地,我们每四个月进行一次特性冻结:一旦我们决定了特性冻结的日子,你要么加把劲在这之前**合并**(merge)你开发的特性,要么索性等下一个版本。这个操作方法也意味着master
分支永远开放用于接受新特性,而不会像不稳定分支那样在稳定化阶段冻结。
由于开发和稳定化的时间重叠,如果版本候选过程耗时过长,很有可能x.y.0
的最终版本会在x.(y+1).0
特性冻结时发布。一个最好的例子就是1.2.0
版本和1.3.0
版本。虽然这在 discourse 上引起了一些困惑和惊讶,但这种副作用是维持可预测发布周期所必要的。1.2
版本的稳定化阶段不寻常的长,但这并没有什么好奇怪的。我们时时检视我们的开发流程,反思如何改进。一个可能的改进是更频繁地调用 PkgEval 以及自动化这个过程。这样我们就能尽早地知道何时我们破坏了与第三方库的兼容性。调用 PkgEval 越早,调用 PkgEval 越频繁,我们就越容易锁定破坏兼容性的变更。如果有人愿意帮助改进 Julia 的发布流程,一个行之有效的方法就是帮我们多多调用 PkgEval,而且这不需要什么高深的技术知识。
有一点需要注意,特性冻结只冻结了特性,不冻结 bug 修复。Bug 修复在任何时间在任何分支上都是允许的。修复 bug 永远不会迟。只有一种情况 bug 修复不会进入版本分支,那就是该分支已经被废弃了。即使如此,如果有人愿意修复废弃分支的 bug 并发布一个新版本,我们举双手欢迎,只不过我们不自己带头罢了。
虽然**预发布版本**(pre-release)是版本发布流程的标准组成部分,并不是所有人都对 alpha 和 beta 版本乃至**候选版本**(release candidate)的意义了若指掌。这些预发布版本有什么意义?我起初对此也懵懵懂懂,直到我开始自己发布软件版本。这些预发布版本其实是一种**沟通**,一种与所有依赖你软件的用户进行的沟通。它们向你的用户发出信号:“亲,来试试看这个。”每个预发布版本向各种用户请求不同的反馈:
alpha 版本说道:“我的特性还不齐全,而且几乎一定有 bug,但请给我一些关于这些重要新特性的反馈,以便于我在木已成舟之前做出适当的修改。”
beta 版本和 alpha 版本很相似,但更完善,包含较少的 bug。我们只在0.6
和0.7
版本发布过 beta 版本(两者之前都已有 alpha 版本)[5]。
[5] | 0.7 就是包含**弃用**(deprecation)的1.0 版本。 |
候选版本说道:“这下总算快完成了,请测试并告诉我们是否有 bug。如果不这样做,我们发布的版本可能会包含影响你应用的 bug。”候选版本,只要不包含阻碍发布的 bug,随时都可能成为下一个正式版本。
所以,下次当你看到一个预发布版本,不要错过尝试它的机会!让我们知道它是否为你正常工作。如果你这样做的话,最终版本就会给你带来平滑、高质量的使用体验。
关于 bug 修复,一个(次要)版本的生命并不随着其打上x.y.0
标签而结束。后面一系列叫做x.y.z
的补丁版本正在翘首以待呢。这又是怎么回事?所有活跃分支都需要修复 bug,但 bug 修复通常进行于最新的分支,随后才反向移植到之前的活跃分支。例如,master
上有个 bug,这个 bug 会以**pull 请求**(pull request,PR)的方式被修复。同时,这个 bug 每波及一个活跃分支,该 PR 就会被贴上相应的backport x.y
的**GitHub 标签**(label)。当前的活跃分支为master
,release-1.3
(不稳定),release-1.2
(稳定),和release-1.0
(LTS),这个 PR 会被贴上相应的backport 1.3
, backport 1.2
,和backport 1.0
标签。这个代码改动随后通过挑选(使用git cherry-pick -x
)应用于这些分支中的每一个,并成为下一个补丁版本的一部分。如果修复成功,测试通过,则皆大欢喜。如果失败,则需要通过额外的手工劳动修复这些分支上的 bug。
一旦某个版本分支积累了足够的 bug 修复,并且经历了足够的时间,一个新的补丁版本x.y.z
就诞生了。相关消息会在 discourse 上提前五天公布,以便于用户测试新版本。我们目前没有精力或资源为补丁版本制作**二进制程序体**(binary)或候选版本——它们太多了。因此,你要么使用一个**每日构建**(nightly build),要么自己从源码编译。如果你想助我们一臂之力,自动化并精简补丁版本发布流程是另一个高影响力的工作[6]。
[6] | 译者注:前一个高影响力的工作是帮助调用 PkgEval。 |
但愿你读完这篇关于 Julia 版本发布流程和政策的综述后有所启迪。我们最想看到的是你们当中的某些人读完之后参与到 Julia 的事业中,同时也希望通过揭秘 Julia 的发布流程,我们降低了成为 Julia 开发人员的门槛。
译者:郑文杰