Julia 版本发布流程

2019年9月7日 | Stefan Karpinski (JuliaHub)

软件开发的行家们对版本发布流程和节奏都非常熟悉,以至于他们会将这些知识内化,并认为每个人都应该了解这些“显而易见的事情”。但事实并非如此,对于外行来说,这就像雾里看花一样模糊不清。因此,为了整个 Julia 社区,甚至其他编程语言社区,我认为有必要将 Julia 的开发过程清晰地记录下来。在这篇文章中,我将阐述:

这些材料是从discourse论坛和Slack协作交流群中收集整理的。所有信息都是公开的,我只是将它们集中到一起。如果大家觉得这篇文章很有帮助,我们会考虑将其发展成一份正式的文档。从宏观上来说,Julia 遵循SemVer标准制定的“语义化版本”。但是 SemVer 在微观上提供了很多自由度,供用户自行解释。这篇文章就是为了填补这些微观细节而存在的。

  1. 补丁版本(Patch releases)
  2. 次要版本(Minor releases)
  3. 主要版本(Major releases)
  4. 长期支持(Long term support)
  5. 不同的版本分支对应着不同的风险承受能力
  6. 发布流程
  7. 预发布版本有什么用?
  8. 版本维护
  9. 结论

补丁版本(Patch releases)

次要版本(Minor releases)

主要版本(Major releases)

长期支持(Long term support)

一些用户喜欢随时更新 Julia 以获得最新最酷的特性。另一些用户甚至乐此不疲地每天重新编译 Julia 的 master 分支,以成为第一个尝鲜的人。还有一些用户恰恰相反,一年到头也懒得升级一次。理想情况下,我们希望为每个存在的次要版本永远提供 bug 修复服务。如果我们拥有无限的资源,我们会将每个 bug 修复都反向移植到每个兼容的版本分支上。理想很丰满,现实很骨感。我们的资源只能让我们维护几个活跃的版本分支。因此,我们退而求其次,决定在任何时间点上最多只维护四个活跃分支:

现在只剩下一个问题:LTS 分支何时会更换?release-1.0是我们目前唯一确定的 LTS 分支。在获得四个补丁版本后,该分支相当稳定并被广泛支持。但是,它从master获得的 bug 修复补丁会越来越少,并且越来越多的第三方库会放弃对其的支持(这些库需要使用 Julia 新版本中的特性)。当合适的时机到来时,我们不得不选择一个新的 LTS 分支并宣布停止维护1.0.x系列。这个新的 LTS 分支可能是1.41.8,也可能是2.0。我们现在还无法预测,但这一天终将到来。幸运的是,即使如此,1.0.x系列的用户也不必一定要升级版本。他们可以使用这个旧版本,并只与与该版本兼容的第三方库版本交互。到那时,它将成为最稳定、测试最充分的 Julia 版本。所以,只要你不需要新特性,你就可以放心继续无限期地使用它。另外,如果有人或某个组织出于自身利益,愿意继续维护某个旧版本分支,也就是**挑选**(cherry-pick)反向移植并运行 PkgEval 以确保兼容性,我们很乐意接受这些帮助从而发布更多的版本。因此,你始终可以通过自己维护或雇人维护来获得更长期的支持。就目前来说,release-1.0仍然将继续是一个优秀的、稳定的 LTS 分支。并且,当我们打算更换 LTS 分支时,我们会提前发布大量的**警告**(warning)。

不同的版本分支对应着不同的风险承受能力

不同的用户有不同的**风险承受能力**(risk tolerance)。一些风险承受能力高的用户可以轻松地发现和报告零星的 bug,并查明为什么某个第三方库与 Julia 的新版本不兼容。另一些风险承受能力低的用户希望使用经过充分测试、广泛兼容的版本。还有一些用户介于这两个极端之间。大致可以将大多数用户根据风险承受能力分为以下四类:

  1. **高风险承受能力**(high risk tolerance):“人生只有一次,我在 master 分支上翩翩起舞。况且,master 分支在未来的相当长一段时间内不会有破坏兼容性的更新[4]。现在 master 只偶尔出现 bug,不过就算出现 bug,我也可以帮忙解决。”

    [4] 译者注:根据前文,破坏兼容性的更新在开发2.0版本时才会出现。
  2. **普通风险承受能力**(normal risk tolerance):“我想要能用的东西,我不想要 master 分支上忽隐忽现的 bug。所以我会坚守最新的稳定版本并打上最新的补丁,这样我的系统既安全又高效。唯一的烦恼是当我使用的第三方库因为依赖淘汰的 Julia 内部代码而在新版本上失效时,我需要等上一段时间第三方库作者才会更新。”

  3. **低风险承受能力**(low risk tolerance):“我保守,厌恶风险。我使用当前的 LTS 分支,因为它已经经历了充分的测试。当 LTS 分支更换时,我将升级到新的 LTS 分支。因为新的 LTS 分支在成为长期支持之前已经经历了数个补丁版本,所以 bug 应该已经被修复,第三方库不兼容问题也应该已经被解决。”

  4. **极低风险承受能力**(very low risk tolerance):“我极端厌恶风险。除了严重的 bug 和安全问题,我从不升级 Julia(或其他任何东西)。我运行一个已经不再被支持的 LTS 版本,但这个版本已经经历了两位数的补丁,相当可靠。如果我需要修复一个新的 bug,我将自己动手反向移植。”

这些不同类型的需求很好地诠释了 LTS 分支的关键特性:

如果一个新的 LTS 分支满足这两个条件,低风险承受能力用户就会升级到该版本,因为他们相信该新的 LTS 分支可靠、经过充分调试,并且所需的第三方库已经向其提供了支持(可能需要同时更新库版本)。我们将从实践中学习新的 LTS 分支在独当一面之前需要滞后稳定分支多少版本。

发布流程

我们已经讨论了各种版本以及它们允许的修改,但我们还没有深入讨论这些版本的发布流程。在这节里,我将详细描述这些细节,例如从master上的新特性到次要版本的发布,再到为次要版本发布补丁。在这节里,“bug”一词不仅指代传统意义上的错误代码,也同时指代性能问题(运行效率不可接受的低代码)。在 Julia 语言中,性能至关重要,我们经常将性能问题视为不可绕过的 bug。以下是一连串围绕x.y.0次要版本展开的各个阶段和关键事件:

一眼望去,你就能发现这是一条漫长的征途。尤其是稳定化阶段,它的耗时忽长忽短,从几周到几个月不等,难以预测。高质量的愿望和按时发布的期待相互冲突,就像一根两头尖的针。一方面,我们不想在调试完成之前就匆忙发布,以免因为粗制滥造而让人失望。另一方面,我们不想因为在调试上花费过多的时间而导致无法按时发布次要版本——尽管我们都知道软件开发,尤其是复杂的程序语言开发,跳票是家常便饭的事。

为了解决这个矛盾,我们想出了一个好主意。如果我们同时进行一个版本的稳定化和下一个版本的开发,我们就有望按时完成版本迭代。每个次要版本的开发阶段占用固定的四个月时间,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)的意义了若指掌。这些预发布版本有什么意义?我起初对此也懵懵懂懂,直到我开始自己发布软件版本。这些预发布版本其实是一种**沟通**,一种与所有依赖你软件的用户进行的沟通。它们向你的用户发出信号:“亲,来试试看这个。”每个预发布版本向各种用户请求不同的反馈:

所以,下次当你看到一个预发布版本,不要错过尝试它的机会!让我们知道它是否为你正常工作。如果你这样做的话,最终版本就会给你带来平滑、高质量的使用体验。

版本维护

关于 bug 修复,一个(次要)版本的生命并不随着其打上x.y.0标签而结束。后面一系列叫做x.y.z的补丁版本正在翘首以待呢。这又是怎么回事?所有活跃分支都需要修复 bug,但 bug 修复通常进行于最新的分支,随后才反向移植到之前的活跃分支。例如,master上有个 bug,这个 bug 会以**pull 请求**(pull request,PR)的方式被修复。同时,这个 bug 每波及一个活跃分支,该 PR 就会被贴上相应的backport x.y的**GitHub 标签**(label)。当前的活跃分支为masterrelease-1.3(不稳定),release-1.2(稳定),和release-1.0(LTS),这个 PR 会被贴上相应的backport 1.3backport 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 开发人员的门槛。

译者:郑文杰