本文原作者JebBarabanov是开源项目angular-builders,jest-marbles,drummer,kite-surfer,gamer和writer的管理者。以下全文翻译李霄。
01什么是开源
广义定义—开源是信息可以公开获得,允许复制和修改的概念。
开源模式有什么好处呢?可想而知的一个主因是开放有益于收获合作者。
Togetherwe’restrong.(Zarya,2016)
02创立开源软件的动机
虽然寻求合作者是选择开源模式的主因,但大部分人并不单单为这一个原因,通常还有其他原因,一些常见的动机和情景总结如下:
你想解决的问题目前市面上没有免费的解决方案(软件)
你想成为一个创始人
你想要成为一个开源软件项目的创始人,这样你就可以在你的简历上添上华丽的一笔。你想要实现自我的成功(毕竟我们都是人)。如果这是驱使你启动一个开源项目的主要原因,那么我可以向你保证,当你读完这篇指南你就会从新考虑了,为了这个原因不值得这么做。
你想比别人更好的解决一个问题
你遇到了一个问题,在市面上有一个开源软件能解决这个问题,但是你认为它不够好或并不完全是你想要的。
你想通过创立开源软件的方式解决一个问题
你遇到了一个问题,目前市面上没有软件可以解决这个问题,于是你开启了一个开源软件项目,希望这样可以找到解决方案。在我看来,不一定。
你应当先解决这个问题,确保你写的软件有用,然后回到第1个原因。
用一句话总结这一部分就是:确认你的目的和你的期望是一致的。
03如何启动一个开源软件项目
我们再强调一次:
这无关你的抱负
你不指望从中获利
你是真心想帮助那些和你遇到同样问题的人
如果你对以上回答都是“是”的话,那么以下是可以帮助你做对这件事的一个清单:
再三确认市面上并没有相似的方案。如果有的话,也许你的方案可以成为已有开源软件的一个完美PR.
为即将发生的事做好准备
你投入到开源软件的每一分钟都将减少你对家人,业余爱好和健康方面投入的一分钟。你可以做的更好的最好途径就是开始委派任务,当你有足够多的合作者的时候,你就可以“外包”一部分你的责任给那些你信任的人。
代码分离
进入正题,你现在已经写了一个解决某特定问题的软件,你也认为其他人能从中获益。你的软件代码此时还是你的代码库的一部分,如果你不是想开放你代码库的全部内容,那么你首先应该把你要开源的那部分代码从你自己的代码库中分离出来并放到另外一个目录下。
普适化你的代码
你要确保你新目录下的代码是普遍适用的而不是只能解决某一个特定的问题。需要的时候,做一个抽象层。
让事情简单些
通用性虽好,且追求且谨慎。
“过度优化”和“过于通用”是两个软件工程里众所周知的问题,你应当在这两个极端之间寻求一个平衡点----你的代码应该在解决你的一个特定问题之外还能解决其它的类似问题,但却不是全世界的所有问题。说得形象一点,如果从1到100,只解决你自己的特定问题是1,能解决世界上的全部问题是100,那么你应该从2开始。
尽量多用你自己开发的软件
记住,你是你的开源软件的第一个用户。
不要吃官司
如果你要从你所在公司的代码库中分离出一部分代码,一定要咨询你的上峰,有必要时咨询法务部门。确保公司支持你的倡议,也要确保你想要开源的代码不属于你公司的知识产权。这个咨询的过程也会帮你确定哪种开源许可证更适用于你的项目。
发表你的代码
开源软件,顾名思义,就是要开放你的软件源码。
公开源码的途径有不少,但我们这个攻略里默认你用的是GitHub.
首先,在GitHub上创建一个仓库(repository).
克隆这个仓库
把你的开源代码拷贝到这个目录下(暂时还不要删除原先那个源代码的目录)。
提交及推送(Commit&push)--大功告成,现在该软件是个开源软件了。
创建安装包
现在你的软件已经开源了,但还没有用户。现在还没人指导它的存在。并且,你的软件现在是以源代码的形式呈现在网上,用户只有把这些代码拷贝+粘贴到你的代码库才能使用。这并不是很方便,对吧?
为了更合理地分发你的软件,你需要:
把你的源代码创建成一个安装包。
把这个安装包发布在一个公开的安装包注册平台(选择什么安装包注册平台,取决于你所在行业的生态。比如,Java软件通常选择MavenCentralRepository,如果是Javascript的话,你应当选择NpmPackageRegistry)。
这正是你在你的新仓库里加入一个构建链,确定你的软件名字等等这些事情的时候。我在这里就不展开叙述了,因为这取决于你所在的生态,你所用的工具和编程语言。
如果你是一个自己做一切的人,命名软件项目,加入构建链和生成安装包对你来说都是小事一桩的话,那么恭喜你!但如果你是一个比较习惯于只写代码,但从未处理过定义(defining),配置(configure),构件(artifact),等这类事情,那么前方有一个全新的世界等你来学习。如果你是后一类人,那么你得开始学习了。这不会是很速成的事情,但相信我,你会学会的。
在你的软件大红大紫以前,确保它能正常运行。再强调一次,你是你自己开源软件的第一个用户。
总而言之,当你做完这一切—把安装包发布了,这个时候你才能说你的软件是一个开源软件了。这个时候,你就可以大声的告诉大家,”嗨,这个软件可以解决你的问题,去下载和使用吧!“
如何处理后续的开发
当你在你开始使用你软件的安装包时,工作流程就会有所不同。之前,你的软件代码是你的代码库的一部分,如果有什么修改,你马上就能用上。但现在,你的软件相当于一个外来的安装包,和任何你使用的第三方软件无异。
所以,以下是一些能帮你规避提交问题版本的几点建议:
确保你的新版本被充分的测试了,包括单元测试,和端到端测试。关于测试的重要性,我无需再强调了吧。
在你的本地环境里,打包和安装你的软件,确保每个环节都按期待进行,接下来你就可以发布这个新版本了。
先给想要的用户分发beta版本,而不是一次性就把新版本发布给全世界。举一个例子,在npmpackageregistry平台上,disttags(分发标签)就是用来做这个事的。
默认的标签是latest。也就是说当你运行npminstallmypackage的时候,程序默认运行npminstallmypackage@latest.
当你把你的版本标上不同的标签,比如beta的时候,用户就需要专门强调他或她要安装的是beta版本:npminstallmypackage@beta.
04如何写文档
这一部分内容基于一个基础,这就是:你已经有一个开源软件了,它已经在GitHub上了。
为什么要写文档,文档包括哪些?
一个没有文档的开源软件是没前途的。没有前途是因为没有人会仔细研究你的代码来搞清这个软件应该怎么用,甚至连你的软件是干什么用的都不知道。
你的文档应该包含两样最基本的东西–用途和操作方法。这些是文档的必要构成部分。
如何写项目说明(description)
说明是人们进入到你的GitHub仓库看到的第一则信息,所以一则好的说明应当简明扼要地描述软件的用途。
提供以下三个例子给你作为参考:
Angularbuilders(这是原文作者的项目)的说明:Angularbuildfacadeextensions(Jestandcustomwebpackconfiguration)
你可以在仓库的About部分修改说明。
如何写自述文件README.md
README.md是一个在你项目的根目录里,用标记语法(markdownsyntax)书写的包含用户所需的全部信息的文本。
README.md文本应当包括软件用途更细节的描述以及详细操作方法。操作方法应当包含公共应用编程接口(publicAPI)的每一个部分,而且最好是结合实例。
以下是写好的API接口文档的几点建议:
简洁:你的API和实例写的越简单,用户就能越轻松地理解它是干什么的以及怎么用。
排版清晰:每一个API函数都用同样的模板和可视化排版。通过这种方式,你也在形成自己与用户传递API信息的特定语言风格。
用户视角——确保你是从用户视角来写API描述的。假装你不知道该软件的内在构造而你所有的信息都来自这个文档。
及时更新——随着你的项目的迭代,API可能也跟着有改变。确保你的自述文件反映最新的API和例子。
自述文件可以(但不一定)包含以下几个元素:
Linktoacontributionguide
Listofcontributors
Linktoachangelog
Latestversion
License
Buildstatus
Downloadscounter
Linktoachatforfastfeedback
在这个链接里你可以看到一个比较好的自述文件的例子:aws-amplify/amplify-js:AdeclarativeJavaScriptlibraryforapplicationdevelopmentusingcloudservices.(github.com)
善用徽章(badge)
使用徽章是一种很好的曝光项目重要信息的视觉呈现方式,项目重要信息包括:构建状态(build),发行版(release),许可证(license)等等。有不少徽章的选择,但我推荐你使用shields.io徽章,它们的徽章几乎涵盖了所有需求。
在自述文件里加入徽章非常简单:
打开Shields.io:Qualitymetadatabadgesforopensourceprojects;
选择相应的类别;
点击你想要添加到自述文件里的徽章;
填写必要信息(适用的话);
从下拉菜单里选择“CopyMarkdown”;
把markdown粘贴到你的自述文件里。
徽章通常是放在自述文件的最开头,在具体说明之前。以下是一个例子:
一定要有测试例子
API文档虽然重要,但还是API的实际代码更重要。
文档不够,测试来凑。其实很多时候具有说明性的测试比文档能更好地解释你的代码是如何运行的。
总结一下,在这一部分里,我们主要是讲了如何写说明和自述文件,但其实还有一些其它的文档。比如说,随着你的项目不断地发展壮大,它的议题(issues)也会不断增多,这些议题会成为文档的有机组成部分。不过,一个涵盖了API接口的自述文件是最基本的。
05如何发布一个开源项目
我们已经讨论了如何更好的启动一个项目以及如何写好文档,那么我们现在来聊聊如何吸引用户,和如何更好的吸引和正确地管理贡献者。
这一部分内容是基于一个前提,这就是:你已经有一个开源软件了,它已经在GitHub上了,它的文档很完善,用户可以轻松地通过安装包注册表来“消费”这个软件。
如何向世界宣传你的项目?
我说不上哪种方式更好,但我个人偏好通过发布者发博文,因为这种方式能确保博文的曝光,而不只是一个”可以被投放“的模糊名词。
写好一篇博文的几点建议:
提供详细的分步骤指南,从安装开始,一直到应用实例。
创建一些示例项目,然后在你的博文里提供这些示例项目的链接。比起一篇博文,很多开发者更乐于见到一些应用实例。
除了增加曝光度之外,你还想要吸引贡献者。
如何让你的项目对贡献者有吸引力?
但并不是每次都能从一个团队开始,如果你一开始是一个人,那你就需要吸引贡献者。以我的经验来看,贡献者一般来自两类人:
在寻找一个项目做出贡献,以发挥影响力的人(这类人很少,但还是存在的)。
使用了你的软件然后发现了bug或者缺失的功能的用户。
提供待办清单
这个清单可以包含一些bug,一些计划中的功能,等等。这个清单会让第#1类贡献者清晰明了的看到他\她可以做的事情然后从中选择合适的来提供拉取请求。这个清单可以是一个单独的文本,或者你可以(或应该)用GitHub上的议题标签。
提供贡献者手册
基础版的贡献者手册包括仓库结构解释,分步骤的构建、运行和测试的操作说明。进阶版的手册也可以涵盖体系结构(architecture),设计决策,行为守则等等。
认可贡献者
把贡献者的名字放在开源项目的首页会成为贡献者的动力之一。
把贡献者的用户名放在首页还不够,我建议你使用AllContributor(另外一个有用的开源项目)。这个工具不仅能帮你做一个漂亮的区域来展示所有贡献者的头像和徽章,还会通过创建拉取请求的方式自动地把新的贡献者加到这个区域里。
总结以下,这一部分,我们讨论了一些关于如何增加项目曝光度和给潜在贡献者提供创建拉取请求和议题的动力的事情。但这些还不足以维系贡献者,或者确保他们从头到尾完成一件他们开始的事务。
06如何管理议题和拉取请求
这一部分,我们来聊聊贡献者–开源项目的圣杯(不可缺少、非常重要的)。
什么是对开源项目的贡献?
一个对开源项目的贡献是任何除了项目持有者以外的人做出的修改。在实践中,贡献一般有两种形式:
议题(issues)
GitHub对议题的说法:
您可以在仓库中使用议题收集用户反馈,报告软件漏洞,并且组织要完成的任务。议题不只是一个报告软件漏洞的地方。
简而言之,议题是一条需要对之采取某种行动的信息。
拉取请求(PR)
GitHub对拉取请求的说法:
拉取请求可让您在GitHub上向他人告知您已经推送到仓库中分支的更改。在拉取请求打开后,您可以与协作者讨论并审查潜在更改,在更改合并到基本分支之前添加跟进提交。
简而言之,拉取请求是一则对项目的实际修改。
如何处理议题和拉取请求?
以我自己为例子
我个人能给你最好的建议就是使用一种工作方式。这个方式就是说,当你开始写一个新的特征的时候你应该创建一个拉取请求,然后当它满足你的要求的时候尽快合并它。如果你发现这个拉取请求中有错或者没有涵盖必要的功能,你应该创建一个相应的议题。这样的工作方式不仅把工作整理得井井有条,更给贡献者们提供一个参考,让他们能相应地调整议题和拉取请求。
还有,如果你提出很高的标准(比如要求每个拉取请求都要有合适的文档,测试,等),你也应该以相同的标准要求自己。你不能提出一些连你自己不会遵循的标准。有时,你应该对贡献者比对自己宽容些,尤其是在项目初创阶段。这也引出我要说的下一点。
感恩所有工作
和他人合作完全是建立在相互尊重之上的。你应该尊重贡献者们。耐心地回答问题(哪怕问题很简单),礼貌地提出有建设性的批评。
记住:尊重贡献者是很关键的。
如果有人创建议题(哪怕没有经过深思熟虑,没有仔细调研,是偶发性不可复制的问题),感谢他们。他们已经多花了功夫,把凳子前移打了一段觉得会对你有用的文字。谢谢他们,如果必要,再礼貌地问问其它细节。
如果有人创建了一个拉取请求,但达不到你设定的高标准,谢谢他们。礼貌地提出修改意见,给他们提供一个你自己的拉取请求以及贡献者手册的链接作为参考。
有建设性和正面的对话会成为贡献者创建贡献的额外动力。
质量还是数量
事情总有个取舍(除非你拥有的是一个像Angular和React那样的大型开源项目)。(这里有两个极端:)如果你决定不放宽标准,哪怕一点点也不放宽,那么你很可能最终得自己来实现所有工作。或者,如果你决定为贡献者降低标准,那么你指定标准这个事就变成徒劳的了,因为标准没能被遵从。
我学到每个贡献者都需要不同的方式,这取决于每个人的特点以及他们对贡献的兴趣。你应当考虑议题的紧急性,贡献者的经验,你的代码的复杂性,需要修复或添加的功能的复杂度还有贡献者的动机,等等这些因素。
通常来说,如果是相对紧急的议题,我会礼貌地请求修改,如果等上几天没有动静,我就自己做。对于相对不太重要(锦上添花)的修复或新特征,我通常就会完全把它们留给开源社区。
慢慢地,议题和拉取请求的数量就会变得越来越多,对那么多的议题和拉取请求进行跟踪,排优先顺序,分门别类就会变成一件心有余而力不足的事情。这就意味着标签将起到举足轻重的作用。
善用标签
GitHub标签是整理议题和拉取请求的利器。它不仅可以让你根据标签来搜索和筛选,我发现最有用的是它可以把一个项目的进度状态图像化地呈现出来。
当你进入“议题”页面,如果看到大部分的议题都被帖上了bug标签的话,就意味着你不能再不停地推新功能了,而是应该停下来专注把这些bug修好。相对应的另一种情况就是,大部分的议题被贴上了enhancement(改进)或者requiredfeature(需要新功能)的标签。
Priority是另一类有用的标签,它可以告诉管理者和贡献者该优先专注在什么议题上。
除此之外,标签也能帮助到贡献者。比如,当潜在贡献者进入到“议题”页面,他们可以通过help-wanted和pr-welcome这样的标签明了的看出哪些议题需要社区的帮助。
除了使用单一责任的标签(比如bug和enhancement),我还建议你使用描述范围和程度的标签,比如:
priority:low,priority:high
required:investigation,required:tests,required:docs
packages:package1,packages:package2etc.
以下用我的一个项目中的议题页截图作为例子:
使用议题和拉取请求的模板
我强烈建议你多花几分钟来定义issues和PR模板。
你能通过模板定义和规范贡献者在仓库里创建议题和拉取请求时应该包含的信息。
下面是一个议题模板的典型例子:
使用GitHub应用和GitHubActions
有很多不少GitHub应用和actions可以帮你管理issues和PR,以下这个列表是我个人觉得有用的:
Stalebot
WIP
Autoapproval
PRlabeler
及时回复
这是我遇到的一个例子:
刚开始的回复比较快,用了两天;
讨论也算有成效;
但这个PR现在还没关闭,PR里指出的错误和缺失没有任何更新。
就这样,我换到了另外一个软件。
角色转换,情况也是一样的,如果你两个多星期才回复用户创建的PR,那么你就会失去这些用户了(用户是潜在的贡献者)。
如何给议题排优先顺序?
这里我给你提供几个能帮你排优先顺序的方法。
首先,怎么确定哪个议题最重要呢?最重要的议题就是用户们最想要的,无论这是一个新功能,一个bug修护还是别的要求。有的时候用户们会在提issues的时候就表达他们的关切程度,但大部分时候他们是不会的。这种情况下,我教你一个可以知道用户们对那些议题感兴趣的途径:
每一个项目都会有一个“Insights”按键,其之下有一个“traffic”区域。
打开traffic你就可以看到哪些页面是用户访问最多的。见下图:
被访问最多的议题很可能就是用户需求最大的。在知道了需求最大的议题之后,你就需要强调这个议题,这通常有两种方法:
置顶议题:在每个仓库中,你可以置顶最多三个议题,被置顶的议题会出现在议题页面的最顶端,很显眼,很难被错过,如下图:
添加标签:我在前页讲过善用标签的话题了,这就是一个最合适使用help-wanted和priority:high的时候了,这样的标签可以让贡献者们看到这个议题很重要而且很欢迎社区的帮助。
总而言之,保持你的项目条理工整是很重要的。。条理工整不仅使得管理更加高效,更会提升整个项目给人的印象。议题和拉取请求也是你开源项目不可分割的一部分,不要低估它们的价值。
07如何实现软件开发自动化
一个自然地管理贡献(也就是议题和拉取请求)的方法是自动化管理,这也是OSS(开源软件)管理的一个重要方面。
为何选择自动化管理?
最坏情况:完全没有自动化
你可以看到,在没有自动化的情况下,你一个人要做全部的事情,光修一个bug你就需要做很多的工作,并且,每修一个bug你都要做一遍这个流程里的所有工作。
再让我们看看另一种情况。
最好情况:完全自动化
在这种情况里,你只需要做你必须做的部分,检查代码,以及(时不时地)同意拉取请求,其它部分都是自动化的。
什么是持续集成(CI)?
一个非常基本的CI运行将包括构建和单元测试,但它不仅限于这两个。它还可能包括各种静态代码分析工具、linter等。这是你可以定义标准的地方。
为何选择端到端测试(E2E)?
E2E测试不仅应涵盖代码的正确性,还应涵盖部署流程、包的完整性等。我是在不小心发布了一个不包含任何代码的包的新版本时,才意识到这一点的。我发布那个不包含任何代码的包的时候,竟然构建通过了,单元测试和E2E测试都是绿色的(构建,单元测试和E2E测试是在一次从测试项目里连接构建输出目录时安装的)。哪里失败了?是在打包阶段。
为了实现这一点,我有如下建议:
在CI运行期间,启动一个本地包注册表。每种语言/生态系统都有一些选项,例如对于Java或Scala项目,有Nexus仓库,对于JavaScript,有Verdaccio(这是我在@angular-builders项目中使用的)
令开启一个项目来使用你的包(这个项目可以在同一个仓库中)。这个项目中的测试应该测试你的包的功能。
将此项目配置为使用本地包注册表。
构建包后,将其发布到本地包注册表(在CI系统中启动)。
在你的测试项目中安装最新版本的软件包(你在上一步中刚刚发布的)
运行测试。
以上这些步骤不仅能测试包完整性和可靠性,而且还会在持续部署方面为你节省一些工作。
持续集成系统是如何工作的?
有很多为开源项而设计的CI系统免费计划,其中包括TravisCI、CircleCI、AppVeyor、GithubActions等。这些,这些计划基本上的功能都差不多,它们在虚拟机上检测你的代码,运行您定义的脚本(通常运行构建和测试),然后向GitHub报告成功或失败。
所有这些系统都在GitHub上用于CI的应用程序,并且所有这些系统的集成流程都非常相似:
在平台上注册。
在您的GitHub帐户中安装相应的应用程序。
配置对选定仓库的访问权限。
创建定义构建矩阵、所需构建链和CI脚本的配置文件(如travis.yaml)。
推送给master
这将使您的CI在每个PR上运行并向GitHub报告状态——但这还不够。您真正想要的是在一个PR通过所有检查之前不让它被合并到主分支。这是通过定义分支保护规则来完成的。为了定义这些,您应该转到仓库“Setting”中的“Branches”部分,然后按“Addrule”按钮:
然后选中“Requirestatuscheckstopassbeforemerging”复选框。
如你所见,相应的GithubApps复选框已经出现在这里,所以剩下的唯一事情就是启用它们。
确切的构建脚本实际上取决于您的生态系统、您的项目所用的语言、您使用的框架等等。因此,我们不会在这里介绍它——你需要自己查看CI系统的文档才能了解细节。不过,你先现在对CI是什么以及它如何自动执行PR应该有了很好的了解,所以让我们继续下一部分的内容。
什么是持续部署(CD)?
持续部署(CD)是一个软件发布过程,它使用自动化测试来验证对代码库的更改是否正确且稳定,以便在一个生产环境中快速、自主地部署。
在我们的例子中,生产环境是一个包在注册表中公开可用的环境。这是一个不可逆的状态,因为一旦你发布了一个包,你就无法撤回了,因为它已经公开了(也就是很可能已经被用户使用了)。
持续部署有多种策略,这实际上取决于项目及其复杂性。但在我看来,发布应该只从主分支发布,因为这样能简化工作流程。只从主分支发布的CD策略步骤如下:
每个PR代表一个错误修复或一个新功能。
代码在到达主分支之前已经经过测试(包括E2E)。
主分支是一个受保护的分支,只要你不合并失败的PR,主分支就会保持稳定。
每个PR合并到主分支的事件都会触发masterCI运行,最终发布一个新版本。
我们需要做以下一些事情来自动化软件包的发布:
基于提交消息(commitmessage)的自动版本升级。
基于提交消息(commitmessage)的自动CHANGELOG更新。
自动将包发布到公共包仓库。
在Github上自动发布。
语义发布(semantic-release)是如何工作的?
语义发布使整个包发布的工作流程自动化,包括:确定下一个版本号、生成发行说明以及发布安装包。这消除了人的主观情绪和版本号之间的直接联系,严格遵循语义版本控制规范。
关于集成,有很好的文档,我们就不在这里赘述了。不过我要提几点:
在开始使用SemanticRelease之前,确保你已经了解语义版本控制规范和常规的提交格式。
为了使语义发布良好运行,你应该强制执行某些提交消息的格式。为此,你可以将commitlint作为huskyprecommithook运行。当有人创建本地提交时,它会强制执行常规提交,但它不能对直接从GitHubWebUI执行的提交做任何事情(当人们想要快速修复他们的PR时经常这么做)。因此我建议你使用commitlintGithubAction做备份。
如何使项目保持最新?
如果你的项目没有外部依赖——你就可以跳过这一部分了。但是,大多数项目通常依赖于其他包,而其他包往往会发生变化。
使您的项目与其依赖项保持同步很重要,但也很耗时。对我们来说幸运的是,有不少帮助我们解决这个问题的工具,例如Greenkeeper、Renovate和Dependabot。
它们的工作原理几乎相同,所以我将引用Dependabot的“它是如何工作的”部分作为说明:
Dependabot查找有没有任何更新
Dependabot会提取您的依赖文件并查找任何过时或不安全的需求。
Dependabot开启拉取请求
如果您的任何依赖项已过期,Dependabot会创建一个单独的拉取请求以更新每个依赖项。
交由你审核并合并
你检查测试否通过,阅读包含的变更日志和发行说明,然后信心满满地点击合并。
总结一下,如果你有一个完全自动化的CI/CD周期,如果现在你的开源项目仓库中出现了一个新问题,您可以在几分钟内提供错误修复。事实上,你可以用你的手机GitHub应用,修复一两条错误的代码,然后提交代码。其余的都是自动完成的,你的用户会立即获得一个新版本。我已经多次这样,快速而轻松地为我的客户提修订过的新版本。
08版本管理
什么是软件版本控制?
我们先来看看维基百科对软件版本控制的定义:
软件升级版本控制是为计算机软件的某个独特状态分配的相对应的唯一版本名称或唯一版本号的过程。我们通常使用两种不同的软件版本控制方案来追踪现代的计算机软件的版本变更:
可能在一天内多次递增的内部版本号,例如修订控制号
通常更改频率要低得多的发布版本,例如语义版本控制[1]或项目代号。
事实上,有多种方法可以给你的软件产品版本一个唯一的标识。最广为人知的方法是给它一个名字。
地球上的绝大多数人,即使是那些间接接触到技术的人,可能都听说过AndroidIceCreamSandwich和Marshmallow或MacOSLeopard、itsfrozencousinSnowLeopard和BigSur。
译者李霄2012年拍摄于加州MountainView谷歌总部
程序员可能听说过Eclipse及其天体Luna、Mars和Photon。这些都是软件产品的主要版本的名字。
给软件版本取名字固然对市场营销有好处,但名字有时候也会制造困惑。事实上,谷歌已经放弃使用candies这个安卓版本的名字,因为多年来从用户那里听到的反馈是,并不是全球社区中的每个人都能直观地理解这些名称。
理所当然,我们还没有进化到足以从动物物种推断版本号,即使雪豹比豹酷得多。天体和糖果是更容易掌握的概念,但前提是你按字母顺序命名它们(如Android和Eclipse这个例子)。但有一件事是肯定的——没有比数字能更好确定演替顺序的方式。因此,如果你将软件产品的第一个版本命名为“产品1”,将第二个版本命名为“产品2”,那么第二个版本是最新的版本这件事就非常直观了,不是吗?
但是,与不涉及API的独立软件产品不同,其他软件(如大多数开源软件产品使用的软件需要更好的版本控制,而不仅仅是数字序列。例如,如果我们仅使用简单的数字序列进行版本控制,那么用户怎么能区分bug修复和破坏现有API的更改呢?答案是:语义版本控制。
什么是语义版本控制?
语义版本(也称为SemVer)是一种广泛采用的版本方案,它使用以下格式的3个数字序列来表示一个版本:MAJOR.MINOR.PATCH。
规则很简单——以版本号MAJOR.MINOR.PATCH为例:
Major:进行不兼容的API更改时的版本
Minor:以向后兼容的方式添加功能时的版本
PATC:以向后兼容的方式进行错误修复时的版本
预发布和构建元数据的附加标签可以加在MAJOR.MINOR.PATCH格式的后缀扩展里。
语义版本控制提供了一种把软件产品的更改用清晰简洁的方式来传达给用户的方法。
但更重要的是,语义版本控制被各种允许用户依赖特定范围的版本而不是特定版本的包管理器和构建工具(如NPM和Maven)广泛采用。例如,指定版本范围^2.2.1而不是明确地规定版本2.2.1将让用户接受任何向后兼容的错误修复或在版本2.2.1之上发布的新功能。这就相当于,构建工具和包管理器听从于用户和包所有者之间的合同——一种由SemVer定义的合同。
这意味着这一切都是你的责任——你要定义什么是重大变化以及什么是微小变化。如果你不小心将破坏性更改作为错误修复(补丁版本)发布,它将会破坏依赖于版本范围的构建。破坏性构建是一件可怕的事情,因此我建议您使用带有预定义消息格式的语义发布以及强制规定提交格式的工具。你可以参考语义版本控制官网[1]获取更多信息。
我们已经谈到什么是破坏性变更了,现在我们来谈谈如何引入它们。
什么是破坏性变更(breakingchanges)?
破坏性更改是对公共API的更改,这种更改以不兼容的方式删除、重命名或更改您与用户的合同。理想情况下,你应当在代码中保持向后兼容性,并且永远不引入任何破坏性更改。然而现实是残酷的。
软件和代码都在不断地迭代,用户的需求会发生变化,API也会发生变化。你作为一名开发人员在不断成长,你的软件作为一件产品也是同样在成长。因此,特别是作为一个没有报酬的开源开发人员,你不可能自己一个人一直维护着项目中的所有祖传代码。而且有时,你需要摆脱它们。
问题是怎么做?
与往常一样,你需要权衡利弊。权衡利弊之后,你会更好地了解此更改或其他更改如何影响用户。你不必不惜一切代价保持向后兼容,也不必在每个旧版本中实现所有新功能。但这当然是您应该考虑的事情。
自动迁移
我们可以把破坏性变更分为两种类型:非确定性的和确定性的。
非确定性重大变更是指你无法预测迁移工作的结果的变更,例如当你完全删除API的某个部分时。在这种情况下,是用户来自行决定是用其它第三方库替换它,自己实现它,还是完全忽略这个版本。
确定性更改是在给定代码X和用户输入I的情况下,允许你将其转换为代码Y的更改。例如,更改函数名称或导入语句。在这种情况下,你可以编写一个自动化程序来更改用户的代码库并将其调整为新的API。有了这种自动化,你就不必关心向后兼容性和详细的迁移指南。这是一种让用户以零努力升级其代码的方法,这在软件更新中至关重要。
如果你还是决定去做,有一些工具可以帮助你。最广为人知且与语言无关的是Facebook的Codemode:
Codemode是一个工具/库,可帮助你进行大规模代码库重构,这些重构可以部分自动化,但仍需要人工监督和偶尔干预。
还有更复杂的工具使用AST,可用于执行比findandreplace更复杂的任务.例如,另一个名为JSCodeShift的Facebook库(特定于JS/TS)。又例如,代码迁移——一个工具(同样是JS/TS特定的),它允许你相对容易地编写引导迁移,并为用户提供很好的基于CLI的提示。
一些大型开源软件项目甚至有自己的解决方案。比如Angularschematic,一种支持复杂逻辑的基于模板的代码生成器。
向后推送(back-porting)
另一种常见的做法是将重要更改向后推送到以前的版本。例如,在主要版本(具有重大更改)发行之后发现了一个严重错误,而这个错误也适用于以前的版本。
在这种情况下,您不能期望用户因为一个错误而执行繁琐的迁移。另一方面,检查旧版本,在其上实施修复,并将其作为旧版本的小问题发布可能很麻烦。
解决方案是:每个主要版本都有一个受保护的分支。
每次你计划发布一个主要版本时,你都应该从主分支创建一个名为c.x.x的分支,其中c是当前的主要版本号。然后你把所有此类分支都设置为受保护的(就像主分支一样),这样你就不会意外破坏它们。然后,无论何时你必须从较新的主要版本向后移植功能或错误修复,你要么在此分支上重新实现它(如果可能的话),要么就从主分支中挑选提交。
此外,值得一提的策略是给下一个新的主要版本也预留一个单独的分支(这个分支的作用和上面提到的为老版本预留分支的作用是相反的)。
结语
我希望你喜欢这个指南,并且读完以后对拥有开源项目的意义有了比较好的理解。
参考资料
【1】SemanticVersioning2.0.0|SemanticVersioning(semver.org)