作者丨Matt Mitchell
译者丨平川
随着.NET Core 3.0 预览版 6 的推出,我们认为有必要简要回顾一下根本举动步伐系统的历史,以及在过去一年旁边韶光里所做的重大改进。如果你对构建根本举动步伐感兴趣,或者希望理解如何构建像.NET Core 这样大的产品,那么这篇文章将非常有趣。

从 3 年前开始,.NET Core 项目就与传统的微软项目有很大的不同。
在 GitHub 上公开开拓由集成在一起的独立 Git 存储库组成,而不是一个弘大的存储库面向许多平台它的组件可以在多个“载体”中发布(例如 Roslyn 作为 Visual Studio 和 SDK 的组件发布)我们早期的根本举动步伐决策是环绕必要性和便利性做出的。我们利用 Jenkins 进行 GitHub PR 和 CI 验证,由于它支持跨平台的 OSS 开拓。我们的官方构建版本位于 Azure DevOps(当时称为 VSTS)和 TeamCity(由 ASP 利用)中,个中有署名和其他关键的交付根本举动步伐。我们搭配利用手动更新包依赖项版本和自动化 GitHub PR 的方法将存储库集成在一起。团队独立地构建了他们须要的工具来进行打包、布局、本地化,以及在大型开拓项目中涌现的所有其他常见任务。虽然不是很空想,但在某种程度上,这在早期已经运行得足够好了。随着项目从.NET Core 1.0 和 1.1 发展到 2.0 以及更高版本,我们希望投资于进一步整合的技能栈、更快的交付周期和更大略的做事。我们希望每天多次利用最新的运行时来天生一个新的 SDK。我们希望所有这些都不降落独立存储库的开拓速率。
.NET Core 面临的许多根本举动步伐方面的寻衅都源于存储库构造的隔离和分布式特性。只管多年来它变革很大,但该产品是由 20 到 30 个独立的 Git 存储库组成(ASP.NET Core 直到最近还比它多得多)。一方面,拥有许多独立的开拓竖井会使这些竖井中的开拓非常高效;开拓职员可以在库中快速迭代,而不用担心栈的其他部分。另一方面,它使得全体项目的创新和集成效率大大降落。下面是一些例子:
如果我们须要推出新的署名或打包特性,那么跨这么多利用不同工具的独立存储库进行署名或打包本钱将非常高。变更跨栈移动缓慢且代价高昂。存储库库中位于栈“底”的修复程序和和特性(例如 corefx 库)可能几天内在 SDK 中(栈“顶”)都看不到。如果我们在 dotnet/corefx 中做了一个修复,那么这个变动必须被构建,并且新版本将会进入任何引用它的上层组件中(例如 dotnet/core-setup 和 ASP.NET Core),它将在那里测试、提交和构建。然后,这些新组件须要将这些新输出供应给栈的更上层,以此类推,直到最上层。在所有这些情形下,都有可能在许多层面上涌现失落败,从而进一步减缓进程。随着.NET Core 3.0 操持的正式启动,很明显,如果不对根本举动步伐进行重大变动,我们就无法创建所需范围的版本。
三管齐下为了减轻痛楚,我们三管齐下:
共享工具(即 Arcade)——投资跨存储库的共享工具;系统整合(Azure DevOps)——弃用 Jenkins,转而采取 Azure DevOps 实现 GitHub CI。将我们的官方构建从传统 VSTS 时期的过程转成当代化的“配置即代码(config-as-code)”。自动化依赖流和创造(Maestro)——显式跟踪存储库之间的依赖关系,并以很快的节奏自动更新它们。Arcade
在.NET Core 3.0 之前,有 3 到 5 种不同的工具实现分散在不同的存储库中,这和你如何打算有关。
核心运行时存储库(dotnet/coreclr、dotnet/corefx 和 dotnet/core-setup)有 dotnet/buildtools;ASP.NET Core 存储库有 aspnet/KoreBuild;dotnet/symreader 等多个存储库利用 Repo Toolset;其他一些单独的存储库有单独的实现。虽然在这个天下上,每个团队都可以定制他们的工具,只构建他们须要的东西,但这确实有一些显著的缺陷:
开拓职员在存储库之间切换时效率更低
例如:当开拓职员从 dotnet/corefx 切换到 dotnet/core-sdk 时,存储库的“措辞”是不同的。她输入什么来构建和测试?日志放在哪里?如果她须要在存储库中添加一个新项目,该如何做?
须要的每个特性都要构建 N 次
例如:.NET Core 天生了大量的 NuGet 包。虽然有一些变革(例如,共享运行时包如出自 dotnet/core-setup 的 Microsoft.NETCore.App 就与 Microsoft.AspNet.WebApi.Client 等“普通”包的构建办法不同),但天生它们的步骤非常相似。遗憾的是,由于存储库在布局、项目构造等方面的差异,如何实现这些打包任务方面也产生了差异。存储库如何定义该当天生什么包、这些包中包含什么、它们的元数据等等。如果没有共享工具,团队常日更随意马虎实现另一个打包任务,而不是重用另一个。这当然会导致资源压力。
借助 Arcade,我们努力使所有的存储库采取公共的布局、存储库“措辞”和任务集(可能的话)。这并非没有陷阱。任何一种共享工具终极都会办理一些“刚刚好”问题。如果共享工具过于规范,那么在任何规模的项目中进行所需的定制都将变得非常困难,并且更新该工具也将变得非常困难。利用新的更新很随意马虎毁坏存储库。构建工具遭受了这种痛楚。利用它的存储库与它紧密耦合,以至于它不仅不能用于其他存储库,而且对构建工具进行任何变动常常会以意想不到的办法侵害用户。如果共享工具不足规范,那么存储库在利用工具时每每会涌现差异,并且推出更新常日须要在每个单独的存储库中做大量的事情。那么,为什么要共享工具呢?
实际上,Arcade 考试测验同时利用了这两种方法。它将公共存储库“措辞”定义为脚本集(请参阅 eng/common)、公共存储库布局和作为 MSBuild SDK 推出的公共构建目标集。选择完备采取 Arcade 的存储库具有可预测的行为,使得变动很随意马虎在存储库之间传播。不肯望这样做的存储库可以从供应基本功能(如署名和打包)的各种 MSBuild 任务包中进行选择,这些任务包在所有存储库中看起来都是一样的。当我们对这些任务进行变动时,我们会尽力避免毁坏性变动。
让我们来看看 Arcade 供应的紧张特性,以及它们如何集成到我们更大的根本举动步伐中。
公共构建任务包——这些是 MSBuild 任务的基本层,可以单独利用,也可以作为 Arcade SDK 的一部分。它们是“付费游戏”(因此得名 Arcade)。它们供应了一组在大多数.NET Core 存储库中都须要的公共功能:1、署名:Microsoft.DotNet.SignTool2、输出发布(到库间推送):Microsoft.DotNet.Build.Tasks.Feed3、打包:Microsoft.DotNet.Build.Tasks.Packaging公共存储库目标和行为——这些都是 MSBuild SDK 的一部分,称为“Arcade SDK”。借助它,存储库可以选择默认的 Arcade 构建行为、项目和工件布局等。公共存储库“措辞”——一组公共脚本文件,利用依赖流在所有 Arcade 存储库之间同步(稍后将详细先容)。这些脚本文件为采取 Arcade 的存储库引入了一种通用的“措辞”。对付开拓职员来说,在这些存储库之间切换变得更加平稳。此外,由于这些脚本是在存储库之间同步的,以是对 Arcade 存储库中的原始副本进行新的变动可以快速地将新特性或行为引入完备采取共享工具的存储库中。共享 Azure DevOps 作业和步骤模板——虽然定义公共存储库“措辞”的脚本紧张面向与人交互,Arcade 也有一组 Azure DevOps 作业和步骤模板,这些模板许可 Arcade 存储库与 Azure DevOps CI 系统交互。与常见的构建任务包一样,步骤模板形成了一个基本层,险些每个存储库都可以利用它(例如发送构建遥测)。作业模板形成更完全的单元,使存储库不必担心 CI 流程的细节。Azure DevOps
如上所述,较大的团队通过 2.2 版本利用了一个 CI 系统的组合:
用于 ASP.NET Core GitHub PR 的 AppVeyor 和 Travis;用于 ASP.NET 正式构建的 TeamCity;用于.NET Core GitHub PR 和滚动验证的 Jenkins;用于所有非 ASP.NET Core 正式构建的经典(非 YAML) Azure DevOps 事情流。许多差异仅仅是出于必要性。Azure DevOps 不支持公共 GitHub PR/CI 验证,以是 ASP.NET Core 转向 AppVeyor 和 Travis 来补充这个空缺,而.NET Core 则投资于 Jenkins。经典 Azure DevOps 对构建编排没有太多的支持,以是 ASP.NET Core 团队乞助于 TeamCity,而.NET Core 团队则在 Azure DevOps 之上构建了一个名为 PipeBuild 的工具来帮助战胜困难。所有这些差异都是非常昂贵的,纵然因此一些不明显的办法:
虽然 Jenkins 很灵巧,但掩护一个大型的(大约 6000 道 8000 个任务)、稳定的举动步伐是一项重大的任务。在经典 Azure DevOps 之上构建我们自己的业务流程须要很多折衷方案。实际上,签入管道作业描述不是人类可读的(它们只是手工创建的构建定义的 json 描述),机密管理难以应对,当我们试图处理构建需求中的巨大差异时,它们很快就被过度参数化了。当在不同的系统中定义了正式构建、夜间验证和 PR 验证过程时,共享逻辑就变得非常困难。开拓职员在进行流程变动时必须格外小心,由于毁坏性变动很常见。我们在一个分外的脚本文件中定义了 Jenkins PR 作业,TeamCity 有许多手动配置的作业,AppVeyor 和 Travis 利用他们自己的 yaml 格式,Azure DevOps 上有我们构建的不为人知的自定义系统。在 PR 中变动构建逻辑很随意马虎毁坏正式 CI 构建。为了缓解这种情形,我们在编写脚本时只管即便保持与正式 CI 和 PR 构建相同的逻辑,但随着韶光的推移,总会涌现差异。一些差异,比如在构建环境中,基本上是不可能完备肃清的。对事情流进行变动的实践千差万别,常常难以理解。开拓职员理解到,Jenkins 中用于更新 PR 逻辑的 netci.groovy 文件没有转换为用于正式 CI 构建的 PipeBuild json 文件。因此,系统知识常日只有少数团队成员节制,这在大型组织中并不理想。当 Azure DevOps 开始推出基于 YAML 的构建管道和对公共 GitHub 项目的支持时,随着.NET Core 3.0 的启动,我们意识到,我们拥有一个独特的机会。有了这种新的支持,我们可以将现在所有的事情流从单独的系统转移到当代的 Azure DevOps 中,并对我们处理正式 CI 和 PR 事情流的办法进行一些变动。我们的事情大致如下:
把我们所有的逻辑都保存在 GitHub 中的代码中。所有地方都利用 YAML 管道。分别有一个公共项目和一个私有项目。1、这个公共项目将通过 GitHub 存储库和 PR 运行所有的公共 CI,我们还会一如既往2、在与公共 GitHub 存储库相匹配的存储库中,将运行正式 CI 的私有项目会作为我们须要任何私有变动的地方3、只有私有项目会访问受限资源在正式 CI 和 PR 构建之间共享相同的 YAML。利用模板表达式来辨别公共项目和私有项目中行为一定不同的地方,或者只有在私有项目中可用的资源才会被访问。虽然这常常使全体 YAML 定义有点混乱,但这意味着:1、降落了过程变动时构建中断的可能性2、开拓职员只须要变动一组地方就可以更改正式 CI 和 PR 流程。为常见任务构建 Azure DevOps 模板,最小化样板文件 YAML 的重复,并利用依赖流轻松推出更新(例如遥测)。到目前为止,所有紧张的.NET Core 3.0 存储库都在 Azure DevOps 上进行公共 PR 和正式 CI。一个很好的例子是 dotnet/arcade 本身的正式构建 /PR 管道。
Maestro 和依赖流
.NET Core 3.0 根本架构的末了一块拼图便是我们所说的依赖流。这并不是.NET Core 独占的观点。除非它们是完备自包含的,否则大多数软件项目都包含对其他软件的某种版本化引用。在.NET Core 中,这些包常日表现为 NuGet 包。当我们须要库供应的新特性或修复时,我们通过更新项目中引用的版本号来获取这些新更新。当然,这些包也可能有对其他包的版本化引用,那些其他包可能有更多的引用,等等。这就形成了一张图。当每个存储库拉取其输入依赖项的新版本时,变动将在图中流动。
一个繁芜的图
大多数软件项目的紧张开拓生命周期(开拓职员常常从事的事情)常日涉及少量相互关联的存储库。输入依赖关系常日是稳定的,更新很少。当他们确实须要变动的时候,常日是手工操作。开拓职员评估输入包的可用版本,选择得当的版本,然后提交更新。但在.NET Core 中并非如此。组件须要独立,以不同的节奏交付,并具有高效的内循环开拓体验,这导致了大量具有大量相互依赖关系的存储库。相互依赖关系也形成了一个相称深的图:
Dotnet/core-sdk 存储库作为所有子组件的聚合点。我们供应了一个特定的 dotnet/core-sdk 构建,它描述了所有其他引用的组件。
我们还希望新的输出能够快速通过这个图,以便尽可能多地验证终极产品。例如,我们期望 ASP.NET Core 或.NET Core 运行时的最新片段尽可能多地在 SDK 中表现自己。实质上,这意味着定期快节奏地更新每个存储库中的依赖项。在一个足够大的图中,就像.NET Core 一样,这很快就变成了一个不可好手工完成的任务。这种规模的软件项目可能会通过以下几种方法来办理这个问题:
“自漂移(Auto-floating)”输入版本——在这个模型中,dotnet/core-sdk 可能会引用 Microsoft.NETCore.App,后者来自 dotnet/core-setup,许可 NuGet 漂移到最新的预发布版本。虽然这样做有效,但它也有一些很大的缺陷。构建成为不愿定的。签出旧的 git SHA,而构建不一定利用相同的输入或天生相同的输出。复现 Bug 变得很困难。在 dotnet/core-setup 中,缺点的提交会毁坏除 PR 和 CI 检讨之外的任何存储库的输出。构建编排成为一项紧张任务,由于构建中的不同机器可能在不同的韶光还原程序包,从而产生不同的输入。所有这些问题都是“可以办理的”,但是须要巨大的投资和不必要的根本举动步伐的繁芜性。“复合(Composed)”构建——在这个模型中,利用每个输入存储库中的最新 git SHA,按照依赖关系的顺序一次性独立构建全体图。构建的每个阶段的输出都被输入到下一个阶段。存储库的输入阶段会有效地覆盖其输入依赖项版本号。在成功构建的末端,输出将被发布,所有存储库将更新它们的输入依赖项,以匹配刚刚构建的内容。这是对自动漂移版本号的改进,由于单个存储库构建不会被其他存储库中的缺点签入自动毁坏,但是它仍旧有很大的缺陷。毁坏性变动险些不可能在存储库之间有效地流动,重现失落败仍旧存在问题,由于存储库中的源代码常常与实际构建不匹配(由于输入版本在源代码掌握之外被覆盖)。自动化依赖流——在此模型中,外部根本举动步伐用于以确定的、经由验证的办法在存储库之间自动更新依赖项。存储库显式地在源代码中声明它们的输入依赖项和关联版本,并“订阅”来自其他存储库的更新。当天生新的构建时,系统创造匹配的订阅,就更新任何已声明的输入依赖项,并利用变动打开 PR。此方法提高了可再现性、使毁坏性变动流动的能力,并许可存储库所有者掌握更新的实行办法。缺陷是,它可能比其他两种方法都要慢得多。变动只能从栈底流向栈顶,其速率与每个存储库中 PR 和正式 CI 韶光的总和相同。.NET Core 已经考试测验了所有 3 种方法。我们在 1.x 的早期漂移版本。在 2.0 中实现了一定程度的自动化依赖流,并为 2.1 和 2.2 构建了一个复合构建。在 3.0 中,我们决定大量投资于自动化依赖流,放弃其他方法。我们想在一些主要的方面改进我们以前的 2.0 根本举动步伐:
简化对产品中实际内容的跟踪——在任何给定的存储库中,常日都可以确定哪些版本的组件被用作输入,但险些总是很难确定这些组件是在哪里构建的,它们来自于什么 git SHA,它们的输入依赖关系是什么,等等。减少必须的人类交互——大多数依赖项更新都很普通。在通过验证时自动合并更新 PR,以加快流程。将依赖关系流信息与存储库状态分开——存储库该当只包含依赖关系图中节点确当前状态信息。它们不应该包含关于转换的信息,比如该当在什么时候进行更新,从什么来源获取,等等。基于“意图”而不是分支的流依赖关系——由于.NET Core 是由相称多的半自治团队组成的,这些团队具有不同的分支思想、不同的组件发布周期等等,以是不该用分支代替意图。团队该当根据这些输入的目的,而不是它们来自哪里,来定义将哪些新的依赖项拉入存储库。此外,这些输入的目的应由生产这些输入的小组宣告。“意图”该当推迟到构建时——为了提高灵巧性,避免在构建完成之前分配构建的意图,我们许可声明多个意图。在构建时,输出只是在某个 git SHA 上构建出的一堆输出片段。就像在 Azure DevOps 构建的输出上运行一个发布管道,实质上为输出分配了一个目的一样,在依赖流系统中为构建分配一个意图也开始了基于意图的流依赖关系的过程。考虑到这些目标,我们创建了一个名为 Maestro++ 的做事和一个名为“darc”的工具来处理依赖流。Maestro++ 处理数据和依赖项的自动移动,而 darc 为 Maestro++ 供应了一个人机界面,以及一个进入全体产品依赖项状态的窗口。依赖流基于 4 个基本观点:依赖信息、构建、通道和订阅。
构建、通道和订阅
依赖信息——在每个存储库的 eng/Version.Details 中,都有一个存储库的输入依赖项声明,以及关于这些输入依赖项的源信息。读取此文件,然后跟踪每个输入依赖项的 repository+sha 组合的通报关系,天生产品依赖关系图。构建——构建只是 Azure DevOps 构建上的 Maestro++ 视图。构建标识存储库 +sha、总体版本号、在构建中天生的完全资产集及其位置(例如 NuGet 包、zip 文件、安装程序等)。通道——通道表示意图。将通道看作一个跨存储库分支可能很有用。可以将构建分配给一个或多个通道,从而将意图分配给输出。通道可以与一个或多个发布管道干系联。将构建分配给通道将激活发布管道并导致发布。构建的资产位置是根据发布活动更新的。订阅——订阅表示转换。它将位于特定通道上的构建的输出映射到另一个存储库的分支上,并供应关于该当在何时进行这些转换的附加信息。这些观点的设计使得存储库所有者不须要栈或其他团队流程的全局知识就可以参与依赖流。他们只须要知道三件事:
他们所做的构建的意图(如果有的话),以便可以分配通道。他们的输入依赖关系以及它们是从什么存储库天生的。他们希望从哪些通道更新这些依赖关系。例如,假设我拥有 dotnet/core-setup 存储库。我知道,我的主分支是为日常的.NET Core 3.0 开拓天生了一些片段。我想分配新的构建到预先声明的“.NET Core 3.0 开拓”通道。我还知道,我有几个 dotnet/coreclr 和 dotnet/corefx 包输入。我不须要知道它们是如何产生的,或者来自哪个分支。所有我须要知道的是,我想每天从“.NET Core 3.0 开拓”通道获取最新的 dotnet/corefx 输入,并在“.NET Core 3.0 开拓”通道每次涌现最新的 dotnet/corefx 输入时获取它们。
首先,我加入了一个 eng/Version.Details 文件。然后,我利用“darc”工具来确保主分支上的每个新构建的存储库都默认分配给“.NET Core 3.0 开拓”通道。接下来,我设置了订阅,从.NET Core 3.0 开拓通道中获取用于构建 dotnet/corefx、dotnet/coreclr、dotnet/standard 等的输入。这些订阅有一个节奏和自动合并策略(例如每周或每次构建)。
当每个订阅的触发器被激活时,Maestro++ 将根据已声明且与新天生的输出有交互的依赖项更新 core-setup 存储库中的文件(eng/Version.Details.xml、eng/Versions.props 等)。它打开一个 PR,一旦配置的检讨得到知足,就自动合并 PR。
这将在主分支上天生一个新的 core-setup 构建。完成后,自动将构建分配给“.NET Core 3.0 开拓”通道。.NET Core 3.0 开拓通道有一个与之关联的发布管道,它将构建的输出构件(例如包和符号文件)推送到一组目标位置。由于这个通道是为日常的公共开拓构建而设计的,以是包和符号被推送到不同的公共位置。发布管道完成后,通道分配就完成了,在此事宜上激活的任何订阅都将被触发。随着更多组件添加进来,我们将构建一个完全的流图,它表示存储库之间的所有自动流。
.NET Core 3 开拓通道的流图,包括.NET Core 3 Dev 流的其他通道(例如,Arcade 的“.NET Tools Latest”)。
同等和不一致
对.NET Core 依赖关系图状态的可见性增强突出了一个存在的问题:当同一个组件的多个版本在图中的不同节点上被引用时会发生什么?.NET Core 依赖关系图中的每个节点都可以将依赖关系流到多个其他节点。比如 Microsoft.NETCore.App 依赖,产生于 dotnet/core-setup,流向 dotnet/toolset、dotnet/core-sdk、aspnet/extensions 和其他一些地方。由于拉取要求验证韶光的变革、对毁坏性变更的相应须要以及所需的订阅更新频率的不同,此依赖项的更新将在这些地方以不同的速率提交。随着这些存储库流向其他地方,并终极在 dotnet/core-sdk 下合并,Microsoft.NETCore.App 可能会有许多不同版本在全体图中被间接引用。这叫做不一致。当依赖关系图中的每个产品依赖关系只有一个版本被引用时,该图便是同等的。如果可能的话,我们总是努力生产出同等的产品。
非同等性会导致哪些问题?不一致性表示可能的缺点状态。举个例子,让我们看看 Microsoft.NETCore.App。这个包表示特定的 API 表面。虽然存储库依赖关系图中可能会引用 Microsoft.NETCore.App 的多个版本,但 SDK 只供应一个。这个运行时必须知足可在该运行时上实行的间接引用组件(例如 WinForms 和 WPF)的所有需求。如果运行时不知足这些需求(例如毁坏性 API 变更),可能就会发生故障。在不一致的图中,由于所有存储库都没有利用相同版本的 Microsoft.NETCore.App,有可能错过了一个毁坏性的变更。
这是否意味着不一致始终是一种缺点状态?不。例如,我们假设图中 Microsoft.NETCore.App 的不一致只代表一个非毁坏性 JIT Bug 修复 coreclr 中的一个变更。从技能上讲,微软没有必要在图中的每一点上获取新的 Microsoft.NETCore.App。只需针对新的运行时交付相同的组件就足够了。
如果不一致只是偶尔的问题,那么我们为什么还要努力才能推出同等的产品呢?由于很难确定什么时候不一致无关紧要。大略地将同等性作为所需状态进行交付,要比试图理解不一致的组件之间的任何语义差异对终极产品所产生的影响更随意马虎。这是可以做到的,但是从构建频率来说,它是韶光密集型的,并且随意马虎出错。逼迫将同等性作为默认状态更安全。
依赖流的好处
随着存储库图越来越大,所有这些自动化和跟踪都有许多明显的上风。它为我们办理日常生活中的实际问题供应了很多可能性。虽然我们刚刚开始探索这个领域,但系统已经可以开始回答一些有趣的问题,并处理以了局景:
在 dotnet/core-sdk 的 git SHA A 和 SHA B 之间发生了什么“真正”的变革?通过遍历 Version.Details.xml 文件构建完全的依赖关系图,我可以识别图中发生的非依赖关系变更的变革。修复程序须要多永劫光才能涌如今产品中?通过结合存储库流图和每个存储库的遥测,我们可以估计将修复从存储库 A 移动到存储库 B 须要多永劫光。这在发布的后期尤其有代价,由于它帮助我们在查看是否要进行特定的变动时,做出更准确的本钱 / 收益评估。例如:我们是否有足够的韶光来处理这个修复并完成我们的场景测试?Core-sdk 构建及其所有输入构建天生的所有资产在什么位置?在做事发布时,我们想要进行特定的修复,但又不想进行其他修复。通道可以被放置到模式中,模式许可特定的修复程序自动通过图,但其他修复程序会被壅塞或须要批准。未来展望随着.NET Core 3.0 逐步结束,我们正在探求新的领域来改进。虽然操持仍处于(非常)初期的阶段,但我们估量在以下几个关键领域进行投资:
缩短将修复转换为可交付的、同等的产品的韶光——依赖关系图中的跳数非常主要。这许可存储库在其处理过程中有很大的自治权,但是增加了端到真个“构建”韶光,由于每个跳转都须要提交和正式构建。我们希望大大减少端到真个韶光。改进我们的根本举动步伐遥测——如果我们能更好地跟踪我们失落败的地方、我们的资源利用情形、我们的依赖状态等等,我们就可以更好地确定我们须要在哪里投资,以交付更好的产品。在.NET Core 3.0 中,我们朝着这个方向采纳了一些步骤,但是我们还有很长的路要走。多年来,我们已经对根本举动步伐进行了相称大的改进。从 Jenkins 到 Azure DevOps,从手工依赖流到 Maestro++,从许多工具实现到一个工具实现,我们对.Net Core 3.0 所做的改变是一个巨大的进步。我们已经为开拓和交付比以往任何时候都更可靠、更令人愉快的产品做好了准备。
原文链接:
https://devblogs.microsoft.com/dotnet/the-evolving-infrastructure-of-net-core/