Julia的版本发布流程

从事软件开发的行家里手们对版本发布流程与节奏如此了若指掌,以至于他们将其精髓内化(internalize)并以为人人都懂得这些“浅显的道理”。 可是事实恰好相反,外行一眼望去如同雾里看花。 所以为了整个Julia社区,乃至于其它编程语言社区,我觉得有必要将Julia的开发过程白纸黑字地写下来。 在本文中,我将阐述:

  • 各种不同的版本
  • 各种版本中允许和不允许的改动
  • 版本发布流程的各阶段
  • 根据风险承受力决定使用哪种版本
  • 发布流程中的各阶段与标志性事件

这些文字材料是从discourse论坛和Slack协作交流群中摘录而来。 所有资料都是现成的,我只是将其归纳在一处。 如果大家觉得这篇文章颇有益处,我们会考虑将其变成一份官方文档。 宏观上来说,Julia遵循SemVer标准制定的“语义化版本”。 但SemVer在微观上提供了许多自由度,供使用者自行解释。 这篇文章正是为填补这些微观细节所作。

补丁版本(Patch releases)

  • SemVer的版本号格式为主版本号.次版本号.修订号。 Julia的补丁版本增加版本号的最后位,即修订号。 比方说,从1.2.31.2.4标志着补丁版本的发布。

  • 依据SemVer,补丁版本只能包含bug修复,低风险的性能改进,和文档更新。 当然,对于什么才是bug修复,不同的人有不同的见解。 造成这一分歧的原因是有些人误将bug当feature并写出建构在其上的代码。 总体来说,我们发布补丁时会慎之又慎,并且用PkgEval1来确保尽可能少的既有代码遇到兼容性问题。 有理由相信,用户们可以高枕无忧地更新到最新的补丁版本。

  • 我们认为,如果不是为了修复某个bug,补丁版本也应当避免修改内部代码。 尽管通常来说在任何版本中,做出公开应用程式介面(API)以外的修改都是无可厚非的,我们仍谨慎地避免这一行为,以将不兼容的风险降到最低。

  • 一般来说,补丁版本大约每月发布一次,并建立在当前的几个活跃(active)版本分支(branch)上(稍后详述)。 如果当月凑不齐足够的bug修复,该月也可能被跳过。

  • 大约在发布补丁版本的五天前,我们会在反向移植(backport)分支上运行PkgEval。 如果一切顺利,我们会将其归并(merge)并冻结(freeze)这些版本分支,并在discourse上宣布可以开始测试了。 如果在这接下来的五天里,一切风平浪静,这些版本分支会被贴上新的版本标签(tag)。

次要版本(Minor releases)

  • 次要版本增加版本号的中间位,即次版本号。 比方说,从1.2.31.3.0标志着次要版本的发布。

  • 次要版本包含bug修复,新特性(feature),和一些“小改动”。 这些小改动理论上可能造成不兼容,但事实上很少引起不兼容。 更何况,我们通过PkgEval完全避免了不兼容的发生。

  • 次要版本也会大量地重构(refactor)内部代码。 之前提到,我们规定补丁版本只允许在修复bug的前提下小范围地重构内部代码,次要版本便顺理成章地成为我们大范围重构的工作场所。 如果你的程序依赖于我们的内部代码而不是公开的应用程式介面的话,你这下可能会遇到不兼容问题。 事实上,你之所以在补丁版本侥幸活了下来,是因为我们在补丁版本执行了比SemVer更严格的标准。 任何出于某种需要而依赖我们内部代码的用户,在升级次要版本时都应该格外小心。

  • 次要版本每四月发布一次,也就是每年发布三次。 每四个月,我们在discourse上宣布当前开发版本将在两周后冻结。 在冻结当天,我们为次要版本建立release-1.3分支2。 该分支会被贴上版本标签,并且不允许额外添加新增特性。

主要版本(Major releases)

  • 主要版本增加版本号的第一位,即主版本号。比方说,2.0.0标志着主要版本的发布。

  • 依据SemVer,主要版本可以大刀阔斧地改动。 不过,现实中,我们很清楚我们将如何塑造Julia的代码,并不会做出面目全非的改变。 大部分用户级别代码会在Julia 2.0版本3中完整地保留下来。 无理取闹地将一切规则打破并不是我们想看到的。

  • 主要版本的职责在于修正明显的API设计缺陷,人人都会因为能摆脱这种差劲的,扑朔迷离的API而拍手称快。 主要版本也允许修改底层代码,这会造成某些第三方库(package)的不兼容,但这是从根本上改善语言所必须付出的代价。

长期支持(Long term support)

一些用户乐于时刻更新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.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,我也可以帮忙解决。”

  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次要版本展开的各阶段与标志性事件:

  • 开发(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.60.7版本5发布过beta版本(两者之前都已有alpha版本)。

  • 候选版本说道:“这下总算快完成了,请测试并告诉我们是否有bug。如果不这样做,我们发布的版本可能会含有影响你的应用的bug。”候选版本,只要不含阻碍发布的bug,随时都会成为下一个正式版本。

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

版本维护

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

结论

但愿你读完这篇关于Julia版本发布流程和政策的综述后有所启迪。 我们最想看到的是你们当中的某些人读完之后参与到Julia的事业中,同时也希望通过揭秘Julia的发布流程,我们降低了成为Julia开发人员的门槛。


  1. PkgEval工具能运行所有Julia第三方库(package)的测试套件。它确保了我们不会不经意间造成不兼容问题。一旦发现不兼容,我们一方面会检查我们的版本是否违背了SemVer,另一方面(无论不兼容的责任方是谁)向该第三方库发送pull请求(pull request,PR)。 

  2. 译者注:release-1.3是此文写作时的版本分支。 

  3. 译者注:2.0为此文写作时的未来主要版本。 

  4. 译者注:根据前文,破坏兼容性的更新在开发2.0版本时才会出现。 

  5. 0.7即为包含弃用(deprecation)的1.0版本。 

  6. 译者注:前一个高影响力的工作是帮助调用PkgEval。