原文:Matt Mitchell
翻译:Edi Wang
随着 .NET Core 3.0 Preview 6 的推出,我们认为简要了解一下我们基础设施系统的历史以及过去一年左右所做的重大改进会很有用。
如果您对构建基础结构感兴趣,或者想要了解我们如何构建与 .NET Core 一样大的产品,那么此帖子将很有趣。它不描述应在下一个应用程序中使用的新功能或示例代码。如果您喜欢这些类型的帖子,请告诉我们。我们有几个类似计划,但希望知道此类信息是否对你有帮助。
一点历史
.NET Core 项目始于 3 多年前,与传统的微软项目相比,这是一个重大转变。
我们早期的基础设施决策是围绕必要性和权宜之计做出的。我们使用 Jenkins 进行 GitHub PR 和 CI 验证,因为它支持跨平台 OSS 开发。我们的官方版本位于 Azure DevOps(当时称为 VSTS)和 TeamCity(由ASP.NET核心使用),其中存在签名和其他关键运输基础结构。
我们使用手动更新包依赖项版本和有点自动化的 GitHub PRs 的组合将存储库集成在一起。团队独立构建了包装、布局、本地化和所有其他工具所需的工具,这些在大型开发项目中出现的任务。
虽然并不理想,但从某种意义上说,这在早期就足够有效了。随着项目从 .NET Core 1.0 和 1.1 发展到 2.0 及之后,我们希望投资一个更加集成的开发栈、更快的发布节奏和更简单的服务。我们希望生成一个新的带有最新运行时的 SDK,每天发布多次。我们希望在不降低独立存仓库的开发速度的情况下进行所有这些工作。
.NET Core 面临的许多基础结构挑战源于仓库结构的隔离、分布式性质。虽然多年来它变化很大,但该产品由 20-30 个独立 Git 仓库(ASP.NET Core 直到最近还拥有更多)组成。一方面,有许多独立的开发孤岛往往使这些孤岛的开发非常高效:开发人员可以在库中快速迭代,而不必担心技术栈的其余部分。另一方面,它使整个项目的创新和集成效率降低得多。一些示例:
译者注:[栈] 的原文为 Stack,不是指栈数据结构,而是描述组成整个.NET Core的各种组件,它们一起,是一个栈。
在所有这些情况下,在许多层面上都有失败的机会,进一步减缓了这一进程。随着 .NET Core 3.0 规划的认真开始,很明显,如果不对我们的基础结构进行重大更改,我们就无法创建我们想要的范围的产品发布。
三管齐下的方法
我们开发了一个三管齐下的方法来减轻我们的痛苦:
Arcade
在 .NET Core 3.0 之前,有 3-5 种不同的工具实现分散在不同的仓库中,具体取决于您计数的方式。
虽然在这个世界上,每个团队可以自定义他们的工具,并只构建他们需要的,但它确实有一些显著的缺点:
开发人员在仓库之间奔波的效率较低
示例:当开发人员从 dotnet/corefx 跑到 dotnet/core-sdk 时,存储库的"语言"是不同的。她键入什么来编译和测试?日志放在何处?如果她需要向回购中添加新项目,这是如何做到的?
每个必需的功能都被开发 N 次
示例:.NET Core 产生成吨的 NuGet 包。虽然有一些变化(例如,使用 dotnet/core-setup 生成的 Microsoft.NETCore.App 共享运行时包,与 Microsoft.AspNet.WebApi.Client 等"普通"软件包的构建方式不同),但生成它们的步骤相当类似。
遗憾的是,由于仓库的布局、项目结构等存在分歧,因此这些打包任务需要实现的方式不同。存储库如何定义应生成哪些包、这些包中的内容、其元数据等。如果没有共享工具,团队通常更容易实现另一个打包任务,而不是重用另一个打包任务。这当然对资源造成压力。
通过 Arcade,我们努力将所有仓库放在一个通用布局、仓库"语言"和任务集(如果可能的话)。这并非没有陷阱。任何类型的共享工具最终都解决了一些"金发(Goldilocks)"问题。如果共享工具过于规范,则任何重大规模的项目所需的自定义类型将变得困难,并且更新该工具变得非常困难。
使用新更新很容易破坏仓库。BuildTools 因此遭受损失。使用它的仓库与它紧密耦合,以至于它不仅不能用于其他仓库,而且在 BuildTools 中的任何更改通常以意想不到的方式使使用者崩溃。如果共享工具的规范性不够,则存储库在工具的使用上往往会出现偏差,而推出更新通常需要在每个单独的存储库中进行大量工作。在这一点上,为什么我们还需要共享工具?
Arcade 实际上尝试同时使用这两种方法。它将通用仓库"语言"定义为一组脚本(请参阅 eng/common)、通用仓库布局以及作为 MSBuild SDK 推出的通用生成目标集。选择完全采用 Arcade 的仓库具有可预测的行为,这使得更改易于跨仓库推出。不希望这样做的仓库可以从各种提供基本功能(如签名和打包)的 MSBuild 任务包中进行选择,这些功能在所有存仓库看起来都相同。当我们对这些任务进行更改时,我们会尽力避免重大更改。
让我们来看看 Arcade 提供的主要功能,以及它们如何集成到我们更大的基础架构中。
常规编译任务包
这些是 MSBuild 任务的基本层,可以独立使用,也可以作为 Arcade SDK 的一部分使用。他们是"付费才能玩"("Arcade"因此得名)。它们提供了大多数 .NET Core 仓库中所需的一组通用功能:
签名:
Microsoft.DotNet.SignTool
发布编译产物(跨仓库订阅源):
Microsoft.DotNet.Build.Tasks.Feed
打包:
Microsoft.DotNet.Build.Tasks.Packaging
常见的仓库目标和行为
这些是作为称为"Arcade SDK"的 MSBuild SDK 的一部分提供的。通过利用它,仓库选择加入默认的 Arcade 编译行为、项目和项目布局等。
通用仓库"语言"
一组使用依赖项流在所有 Arcade 存储库之间同步的通用脚本文件(稍后将介绍更多)。这些脚本文件引入了采用 Arcade 的仓库的通用"语言"。对于开发人员来说,在这些存储库之间移动变得更加无缝。此外,由于这些脚本在存储库之间同步,因此对 Arcade 存储库中的原始副本进行新更改可以快速将新功能或行为引入完全采用共享工具的存储库。
共享 Azure DevOps 作业和步骤模板
虽然定义公共存储库"语言"的脚本主要针对与人交互,但 Arcade 还有一组 Azure DevOps 作业和步骤模板,允许 Arcade 存储库与 Azure DevOps CI 系统进行接口。与常规编译任务包一样,步骤模板构成了一个基础层,几乎每个仓库都可以使用(例如,发送生成遥测)。作业模板形成更完整的单元,使存储库能够减少对 CI 流程细节的担心。
迁移到 Azure DevOps
如上所述,更大的团队在 2.2 版本中使用了 CI 系统的组合:
许多区别只是为了必要性。Azure DevOps 不支持公共 GitHub PR/CI 验证,因此ASP.NET Core 转向 AppVeyor 和 Travis 来填补空白,而 .NET Core 则投资 Jenkins。经典 Azure DevOps 对构建业务流程没有很多支持,因此ASP.NET Core 团队转向 TeamCity,而 .NET Core 团队在 Azure DevOps 上构建了名为 PipeBuild 的工具来提供帮助。所有这些分歧都非常昂贵,即使在一些不明显的方式:
当 Azure DevOps 开始推出基于 YAML 的构建管道,并在 .NET Core 3.0 开始启动时对公共 GitHub 项目的支持,我们认识到我们具有独特的机会。有了这种新的支持,我们可以将所有现有的工作流从单独的系统移动到现代 Azure DevOps 中,还可以对如何处理正式的 CI 和 PR 工作流进行一些更改。我们从以下工作大致概要出发:
到目前为止,所有主 .NET Core 3.0 仓库都在 Azure DevOps 上,用于其公共 PR 和官方 CI。一个很好的例子管道是 dotnet/arcade 自己本身的官方编译/PR管道。
译者注:Arcade 自己的编译管道 https://github.com/dotnet/arcade/blob/master/azure-pipelines.yml
(文章翻译未完待续)