为什么大型monorepos变慢了Rush.js + Sparo对此做了什么

Rush.js、PNPM工作空间和Sparo的稀疏校验是如何攻击拖大monorepos后腿的三件事:Git、安装和构建

一个大的monorepo会在三个独立失败的地方变慢:Git、依赖项安装和构建。解决一个问题对其他问题没有任何帮助。

稀疏校验使407个项目的测试报告克隆速度提高了一倍,减少了一半.git,切git status从76毫秒到13毫秒

稀疏签出根本没有加速安装,因为Rush不能从缺少项目文件夹的签出安装。

选择标志将叶文件重建从407个项目削减到4个。对一个广泛使用的包进行更改仍然会重新构建所有东西,因为图表就是这么说的。

大多数假设您使用npm、pnpm或yarn。如果你没有,这篇文章的前半部分对你不适用。

问题是

想象一下新员工的第一天。他们跑了git clone然后等10到40分钟,看着物体计数器攀升超过100万。

然后安装运行。一个包含数万个条目的锁文件被解析,并且node_modules超过了10亿字节。到午饭时,他们还没有打开编辑器。

第二天,他们更换了一个包装中的一行。CI重建整个存储库来检查它。

那一天产生了三种不同的成本,每一种成本的比例都不同:

饭桶:历史的字节数,以及工作树中的文件数。而不是你编辑的那些文件。你可以从克隆时间,群体大小,git status,以及checkout.

安装:第三方包的数量,以及包管理器必须解析和写入的依赖图的大小。你看到了吗node_modules尺寸和安装墙壁时间。

建设:项目的数量,以及是否重建未更改的项目。你可以在"它重建了一切"和CI minutes中看到它。

这就是为什么一个解决方案无法涵盖所有三个方面。稀疏签出不会使安装更快。构建缓存不会使git status更快。

Three monorepo bottlenecks: Git, install, and build/test, with the tool that addresses each

这是基于回购协议

下面的一切都是对照一个小教程回购检查的。这是一个玩具酒店预订应用程序,有一个NestJS API和一个TanStack Start前端。领域逻辑是故意变得单薄的。回购的存在是为了展示工具。

回购有七个项目,都是手工注册的rush.json。两个是应用程序(@hotel/api和@hotel/web).五个是包(@hotel/ui, @hotel/shared-types, @hotel/api-client, @hotel/eslint-config,以及@hotel/typescript-config).

运行时边缘是web → ui, web → api-client, web → shared-types, api-client → shared-types,以及api → shared-types。每个项目还将两个配置包都列为开发依赖项。因此,仅开发的边缘比运行时的边缘多两倍,这在我们根据变更的内容选择项目时很重要。

工具链是固定的:拉什5.181.0,PNPM 11.5.1,和范围内的节点>=20.9.0 <25.0.0.

回购是120个1.1 MB的跟踪文件.git目录。在这样的规模下,稀疏的收银台什么也买不到,所以这种回购无法告诉你其中任何一种方式是否能大规模运作。这就是为什么后来的数字来自合成放大。

Sparo:让Git交付更少

Sparo是两个Git特性的小型包装器。要理解包装器,您需要知道它包装什么。

部分克隆:下载更少的对象

一个正常的git clone下载每个提交、每个目录列表和每个文件的每个版本。其中大部分你永远也不会读到。

git clone --filter=blob:none向服务器请求子集。您仍然可以获得所有的提交和所有的目录列表。您跳过的是文件内容。Git稍后按需从标记为立约人.

这种推迟是有代价的。Git自己的文档说动态获取"往往很慢,因为一次只能获取一个对象"。Agit checkout在一个批处理中获取它需要的所有东西,所以结帐是好的。从历史中读取任意文件的脚本则不是。

稀疏签出:编写较少的文件

部分克隆会减少您下载的内容。稀疏签出减少了写入磁盘的内容。您给Git一个目录列表,Git将所有其他内容都排除在工作树之外。

该列表有两种格式。锥形模式是默认的,它接受目录:您命名的目录下的所有内容,加上顶层的文件。非锥形模式采用gitignore风格的模式。不推荐使用它,它需要O(N×M)个模式匹配,并且它不适用于稀疏索引。

您可以进一步加快生成树的速度。这稀疏索引 (index.sparse)将整个目录存储为单个条目,而不是每个文件一个条目。这缩小了索引并加快了速度git status和git add。Git只在你通过的时候写它--sparse-index.

这文件系统监控程序 (core.fsmonitor)让Git找到修改过的文件,而不用检查磁盘上的每个文件。它可以在Windows和macOS上运行。Linux支持在2026年6月到达Git 2.55.0。

关于稀疏检出的两个警告。它删除已经在磁盘上的文件,如果要删除的文件有未转移的更改,它将拒绝继续。解决这些问题,然后运行git sparse-checkout reapply.

配置文件:命名您想要的集

手写稀疏目录列表比听起来更糟糕。随着回购的增长,列表会发生变化,每个开发商都会维护自己的列表。Sparo用提交的文件替换列表。

sparo clone不同于git clone以四种记录的方式。它只获取默认分支。它支持blobless部分克隆。它检查了骨骼文件夹。它配置锥形模式。它还为您应用了一些Git设置,包括fsmonitor、未跟踪的缓存,feature.manyFiles, fetch.prune,以及pull.rebase.

A 轮廓住在common/sparo-profiles/.json并且被提交给Git,所以它是用它所应用的分支来版本化的。它有四个字段:KaTeX parse error: Expected '}', got 'EOF' at end of input: ...选择器和一个参数。 { "schema": "/json-schemas/sparo/v1/sparo-profile.schema.json",

"selections": { "selector": "--to", "argument": "@hotel/web" }

}

选择器是拉什的项目选择器。因此--to, --from,以及--impacted-by都在这里工作,你可以列举几个。多重选择结合在一起。excludeFolders搜索includeFolders击败选择。

因为选择器是Rush的,所以上面的概要文件具体实现了rush build --to @hotel/web会构建:应用程序加上它所依赖的一切。

全新克隆的工作流程是两个命令:sparo clone ,那么sparo checkout --profile web。两个相关的命令值得了解。sparo checkout --to和--from添加到已记录的配置文件中,而不是替换它们,以及--no-profile清除录音。

一部分永远不会被剥离。Sparo称之为骨骼,它包括整个common/目录。这很重要,因为common/保存Rush的配置、脚本和自动安装程序。没有一个概要文件可以让您签出无法安装或构建的文件。

关于这次回购的一个警告。两者都是手写的,他们的$schema指向Microsoft托管的URL。我在2026-10-16检查了那个网址,它返回了HTTP 404。Sparo自己发布的模式/sparo/schemas/sparo-profile.schema.json退了200。Microsoft URL不是旧位置;它不存在,斯派洛的医生也从未提及。奔跑sparo init-profile --profile web从您安装的Sparo版本生成模板。

Which projects each Sparo profile materializes, and which stay out of the working tree

一棵稀疏的树每天看起来是什么样子

我在一个由sparo clone应用了配置文件。这使得这一机制不再是理论上的,而是可检验的。

git ls-files | wc -l # 120

git ls-files | while read -r f; do -e "$f" && echo x; done | wc -l # 118

git status --porcelain | wc -l # 0

git config --get remote.origin.partialCloneFilter # blob:none

git config --get remote.origin.promisor # true

Git跟踪120个文件。只有118个在磁盘上。git status调用树clean,因为两个丢失的文件被标记为skip-worktree。

这两个文件是docs/BANG_OPERATOR.md和.github/workflows/ci.yml。为了阅读我必须运行的工作流git show HEAD:.github/workflows/ci.yml,因为它不在此克隆的磁盘上。稀疏设置位于.git/config.worktree而不是.git/config,因为git sparse-checkout set将repo升级到特定于工作树的配置。

这是计划的陷阱。选择的配置文件--to @hotel/web排除.github/,因此您的配置项配置将从工作树中消失。遍历树的所有东西看到的回购都比Git小。git grep返回较少的结果,并且git commit -a不会将锥外文件记录为已删除。

如果某个工具需要一个目录,请将其命名为:

{

"selections": { "selector": "--to", "argument": "@hotel/web" },

"includeFolders": ".github"

}

实践中还出现了三个问题。克隆是blobless,所以读取Git没有获取的内容需要一次网络往返。repo范围的lint或后台ide索引将触发这种情况。Sparo拒绝签出省略了包含本地编辑的文件夹的配置文件,所以先隐藏或提交。和sparo-ci checkout使用一个无树木的在CI中克隆而不是在blobless中克隆。任何假定目录列表存在的东西在跑步者和你的笔记本电脑上会有不同的表现。

稀疏校验斗安装

这是我没想到的结果,而且影响了你应该怎么规划。

关于方法的一个警告:我用手工构建了这三个案例git sparse-checkout,不与sparo checkout --profile。这将拉什和PNPM的需求与Sparo的配置区分开来。请将下面的发现视为Rush安装的一个属性,而不是Sparo自己检查的一个度量。

拉什和PNPM需要每个注册项目的package.json在磁盘上。因此,忽略项目文件夹的配置文件无法安装。我在407项目测试夹具上连续三次失败:

仅命名应用程序和包文件夹的锥形集合被省略common/,以及rush update0.29秒内死亡MODULE_NOT_FOUND为common/scripts/install-run-rush.js。锥形模式包括根文件和您命名的目录,除此之外别无其他。

加common/还不够。拉什仍然需要每个项目的package.json,所以相同的命令失败了Could not find package.json for @hotel/eslint-config.

具体化所有408的非锥形集合package.json文件加common/确实安装了,磁盘上有513个文件,而不是3,328个。但是rush build --to @hotel/ui然后失败了TS6053: File '@hotel/typescript-config/react.json' not found,因为共享配置包也丢失了。

因此,一个可使用的集合比"仅仅是我正在开发的应用程序"要窄。保留每package.json, common/和配置包,并且只稀疏化源目录。这仍然需要你从3328个文件到513个。

这个食谱有一个难题:它需要非锥形的图案。锥形集只能命名目录,但是您必须保存的文件位于每个包的根目录。非锥形模式已被弃用,速度较慢。在不需要安装的时候,圆锥模式还是比较好的选择。

安装尺寸的杠杆不是检验。这是拉什的过滤安装,我将在下一节中介绍。在夹具中,稀疏校验改变了安装时间和安装尺寸。

拉什:图表、安装、选择

拉什构建了依赖图,并将安装任务委托给PNPM。四块就够了。

The seven projects and their runtime and dev-only dependency edges

项目登记册

rush.json手动列出每个项目。没有全球发现和目录扫描。

从rush.json,删节为七个条目中的两个:

{

"rushVersion": "5.181.0",

"pnpmVersion": "11.5.1",

"nodeSupportedVersionRange": ">=20.9.0 <25.0.0",

"projects": [

{

"packageName": "@hotel/shared-types",

"projectFolder": "packages/shared-types",

"shouldPublish": false

},

{

"packageName": "@hotel/ui",

"projectFolder": "packages/ui",

"shouldPublish": false

}

]

}

所有七个条目都省略了三个可选字段:tags, versionPolicyName,和一个真理shouldPublish。每一个遗漏都让你损失一个特征。没有标签,您无法选择项目tag:。没有版本策略,一切都不会一成不变。随着shouldPublish: false拉什跟踪没有变化的文件。

手动列出项目会让你在每个项目上花费一行,而在其他任何地方都可以买到快捷方式。每个命令读取一个小文件,而不是遍历树package.json。添加一个项目就变成了文件中一行明显权威的diff。

安装:一个锁文件,一种安装更少的方法

Rush在工作区模式下将安装委托给PNPM。

{

"useWorkspaces": true,

"minimumReleaseAgeMinutes": 1440,

}

拉什产生common/temp/pnpm-workspace.yaml列出所有当地项目,然后在整个回购过程中运行一次PNPM。你得到一个锁文件common/config/rush/pnpm-lock.yaml,一个内容寻址存储,以及到每个项目的node_modules.

rush install执行安装,但不会修改任何文件。如果锁定文件过期,它将失败。rush update是解析和重写的命令。使用update工作时和install在CI中。运行的CI作业update将接受不再与清单匹配的锁定文件,这是您希望CI拒绝的状态。

要安装得更少,请传递与传递给相同的项目选择器rush build:

rush install --to @hotel/web

Rush仍然生成完整的工作空间。然后就过去了--filter到PNPM,所以只有选中的项目和它们的依赖闭包会被安装。有两个限制。这些标志仅在以下情况下有效useWorkspaces是真的。和rush update拒绝它们,所以这缩小了安装范围,而不是分辨率。

这有助于了解磁盘上的最终内容。在此repo中完全安装后,common/temp容量为1.6 GB。PNPM存储占707 MB,node_modules和Rush自己下载的工具install-run对于336 MB。这rush-recycler高速缓存增加了164 MB。大约有5gb是Rush自己管理的。

供应链年龄检查

PNPM可以拒绝最近发布的软件包。它的设置是minimumReleaseAge以分钟为单位,在PNPM 10.16.0中添加,从PNPM 11默认为1440。Rush在其自己的键下显示相同的值,minimumReleaseAgeMinutes。该检查适用于可传递的依赖关系,而不仅仅是您指定的依赖关系。

之所以要把数字写下来,是因为豁免去了哪里。PNPM把他们关在里面minimumReleaseAgeExclude,里面pnpm-workspace.yaml。在紧急回购中,该文件是在common/temp,它被gitignored和dockerignored。因此,在一台笔记本电脑上添加的豁免只存在于该笔记本电脑上。当一个新的克隆、配置项或docker compose build锁定文件验证失败。

当您需要比window更早的版本时,安装会失败,并显示ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION。固定一个较旧的版本,或者添加一个有意的豁免。禁用一个补丁的策略是第三种选择,但也是错误的选择。

批量、全局和分阶段命令

common/config/rush/command-line.json这里定义了六个自定义命令。三个是bulk,这意味着它们按照依赖关系顺序为每个选定的项目运行一次:lint, typecheck,以及seed。三个是global,这意味着它们为回购运行一次:dev, format,以及format:check.

dev必须是全球性的。批量命令会缓冲每个项目的输出,直到该项目完成,而两个监视服务器永远不会完成。全局命令运行一个小脚本,该脚本在其自己的进程组中生成每个服务器。发信号给这个团体会把整个团体拖垮rushx链条。仅发信号通知领导者离开保持其端口的服务器。

拉什还支持阶段性命令,本回购不使用。而不是一个build每个项目一个脚本,项目声明如下操作_phase:build和_phase:test,以及它们的输入和输出rush-project.json。阶段使操作级缓存成为可能,因为缓存键是为每个操作计算的。然后,项目可以缓存它的构建,而不是它的测试。如果您移动到构建缓存,这就是您编辑的文件。

子空间是这个repo关闭的另一个扩展特性。子空间是一组拥有自己的锁文件和安装的项目。这就是几千个项目的monorepo如何按产品组划分解决工作,而不是在每个安装上解决一个巨大的图形。在某些配置中,交叉子空间依赖是注入的,而不是符号链接的,因此启用子空间是一个深思熟虑的步骤。

选择:仅构建更改的内容

这是拉什保持CI诚实的一半。传递一个项目选择器,拉什建立一个子集。

rush build --to @hotel/web # @hotel/web and everything it depends on

rush build --from @hotel/shared-types # shared-types, its dependents, and their dependencies

rush build --only @hotel/ui # exactly this project, dependencies ignored

rush build --from git:origin/main # everything this branch changed, closed over

如果在这些基础上构建自动化,有四个属性很重要。

选择器组合成一个联盟。同一个标志可以重复,如在--only a --only b,没有标志会从选择中移除项目。要缩小集合,请缩小输入。

三个标志被记录为危险的: --only, --impacted-by,以及--impacted-by-except。每个人都可以选择一个没有依赖项的项目,因此如果那些依赖项从未构建过,构建就会失败。它们适合在构建阶段之后运行的CI阶段。他们是冷结账的错误选择。--from是安全的形式,因为它引入了它所选择的所有内容的依赖关系。

--from在Rush 5.38.0中改变了含义。今天,它意味着项目、它的依赖项以及整个集合的依赖项。在5.38.0之前,它的行为如下--impacted-by并且没有添加依赖项,所以旧的文档描述了旧的行为。这git:选择器更新,在Rush 5.57.0中添加。

没有--from-except。仅仅--to-except和--impacted-by-except存在。--from-except显示在生成的配置项配置和从未运行的博客文章中。

选择器可以是项目名称,.对于当前项目,git:, subspace:,或者tag:。对于Git表单,记录的形状是单个引用,例如git:origin/main。拉什解决它,并回落到git merge-base -- HEAD 当引用不是完整的提交哈希时。我没有找到支持三点范围的文档,例如git:origin/main...HEAD,所以我会计算基本提交并传递散列。

增量构建及其下的假设

rush build是增量的,不能以其他方式制作。它散列每个项目的输入,并跳过自上次成功构建以来输入没有改变的任何项目。那个州生活在/.rush/temp/package-deps_build.json。这就是为什么这次回购make clean删除这些文件:状态跟踪源哈希,而不是输出是否仍然存在。

禁用缓存后,Rush使用产出保存。如果输入没有改变,Rush认为磁盘上的输出仍然是正确的。删除apps/api/dist这个假设现在是错误的。rush build报告最新的所有内容,不发出任何内容,失败稍后会在运行时作为丢失的文件出现。

这两个构建命令的不同之处在于它能让人看出来。

rush build从缓存中读取并写入缓存。

rush rebuild清理和重建每个项目。确实如此不从缓存中读取,它做给它写信。

所以CI作业正在运行rebuild在填充其他作业读取的缓存时,没有获得缓存优势。如果您采用缓存,请将CI移动到rush build。您可以使用以下命令关闭写入RUSH_BUILD_CACHE_WRITE_ALLOWED=0.

缓存本身仍然是实验性的,在这个回购中是关闭的。这就是为什么没有build-cache目录存在于下common/temp.

拉什和PNPM没有在全球安装。mise.toml为开发人员锁定工具链。当mise不可用时,如在docker文件中,common/scripts/install-run-rush.js读rushVersion从rush.json并运行该版本。

版本控制和发布

这里的每个项目都设定了shouldPublish: false,以及version-policies.json为空,因此该回购没有common/changes/目录,不发布任何内容。在发行的回购中,rush change写入每次更改的文件,并rush change --verify当一个公共包在没有CI的情况下更改时,CI失败。rush version --bump然后应用凸起,并rush publish写入变更日志。

这些数字看起来像什么

我做了一个夹具并测量了它,因为估计会比什么都不说更糟糕。

固定物是这个回购的工作树加上400个生成的包。每个生成的包都有一个package.json,一个tsconfig.json和六个源文件。结果是407个注册的项目和3,328个跟踪的文件,提交到一个1.7 MB、5,785个对象和1.32 MiB包的本地裸远程。该机器是一台运行Node 24.22.0、Rush 5.181.0和PNPM 11.5.1的8核arm64笔记本电脑。

一个设置细节让我白跑了一趟。一个简单的遥控器需要uploadpack.allowFilter=true。没有它,git clone --filter=blob:none印刷品warning: filtering not recognized by server, ignoring悄悄地制造出一个完整的克隆体。下面的每一个blobless数字都来自那个设置之后。

基线是平原git clone加上一个完整的rush rebuild. 优化是git clone --filter=blob:none --sparse使用锥形模式稀疏集,加上基于选择的构件。计时是指定运行计数的最接近等级的百分位数。

在你阅读这些数字之前,有两点需要注意。克隆人跑过来file://所以他们测量Git的对象和树工作而不是网络传输。在真实的网络中,稀疏克隆会比这里看起来更好,因为它跳过的字节是开销较大的部分。这些测量使用三种不同的树,所以每张表都用它来自的树来命名。

检验,在应用于blobless克隆的锥形模式稀疏集上测量:

公制的 基线 优化

克隆和稀疏集(p50,n=10) 284毫秒 160毫秒

.git在磁盘上 1824 KiB 920号

磁盘上的文件 3,328 85

git status(p50 / p95,n=50) 76 / 80毫秒 13 / 14毫秒

安装并构建。安装行将完整克隆与支持安装的稀疏树进行比较,稀疏树包含513个文件和每个package.json。两个构建行都在完整克隆中运行。

公制的 基线 优化

寒冷rush update(p50,n=5) 46.5秒 41.9秒

common/temp安装后 1.20吉卜 1.20吉卜

在叶文件更改后构建 24.60年代,407个项目 1.04秒,4个项目

在中心文件更改后构建 24.60年代,407个项目 24.74 s,401个项目

那些表格里有四样东西值得读两遍。

Blobless克隆本身比没用还糟糕

--filter=blob:none仅用1972 KiB在341毫秒内克隆.git,两者都比普通克隆人差。退房HEAD无论如何,具体化每一个斑点,并且延迟的降落在第二个,松散的包裹中。胜利来自停止那些文件被写的稀疏集合。

秩序也很重要。在blobless克隆后应用稀疏集花费了561 ms,留下了最大的.git在KiB街1988号。克隆使用--sparse第一次,然后设置锥花了160毫秒和一半的.git尺寸。

这git status数字是你感觉到的

76毫秒到13毫秒Git停止检查3,242个不再在磁盘上的文件。这就是你没注意到的命令和你注意到的命令之间的区别。

安装程序不会移动

稀疏校验减少了97%的工作树,并且没有改变安装时间。

读点差而不是中位数。完整的克隆给出了39.6、41.8、46.5、49.3和52.9秒。可安装的稀疏树给出了36.6秒、40.5秒、41.9秒和60.1秒,加上一次在注册表上330.6秒停止的运行。五次稀疏运行中有两次击败了每次全克隆运行,稀疏分布是更广泛的一次。这就是当指标由网络和注册延迟决定时"没有影响"的样子:两个分布重叠,并且两个顺序都不稳定。common/temp一模一样,1.20磅。

安装成本是依赖图的解析和具体化,无论您签出多少项目文件夹,该图都是相同的。上一节中的三个失败解释了为什么你不能稀疏化你的出路。

选择取决于图形,而不是旗帜

一个没有依赖项的文件在1.04 s内重建了4个项目,是完整的24.60 s的4%,一个其他400个依赖的包中的文件在24.74 s内重建了401个项目,是完整的重建加簿记。

选择不是魔术。它是你的依赖图的一个函数,一个广泛使用的共享包改变起来总是很昂贵的。fixture连接该hub包下游的所有400个生成的包,因此第二行是最差的情况,而不是被操纵的情况。

缓存在这个fixture中是关闭的,在所有30个构建和安装日志中的grep没有发现缓存消息。唯一被报道的跳跃来自Rush的增量变更检测。

CI和CD

此回购提交的工作流

repo提交一个工作流,它的排序编码了两个教训。从.https://www.iissbbs.com /workflows/ci.yml:

name: CI

on:

push:

branches: "main"

pull_request:

branches: "main"

jobs:

build:

runs-on: ubuntu-latest

steps:

  • uses: actions/checkout@v4

with:

fetch-depth: 2

  • uses: actions/setup-node@v4

with:

node-version: 22

  • run: node common/scripts/install-run-rush.js install

  • run: node common/scripts/install-run-rush.js format:check

  • run: node common/scripts/install-run-rush.js lint

  • run: node common/scripts/install-run-rush.js typecheck

  • run: node common/scripts/install-run-rush.js rebuild --verbose

  • run: git diff --exit-code -- apps/web/src/routeTree.gen.ts

第一个教训是typecheck之前运行build。期间,Vite不进行类型检查build,所以rush build可以在有类型错误的web应用程序上成功。类型检查必须是它自己的通行证。

第一课之后是第二课。因为typecheck在任何东西建成之前运行,apps/web/src/routeTree.gen.ts必须承诺。TanStack Start的Vite插件生成该文件src/router.tsx导入它,但是在typecheck时还没有生成它。最后一步是在构建过程中重新运行生成器并对文件进行diffs,因此drift会导致任务失败。

两个较小的选择是有意的。CI使用rebuild而不是build,因为它需要一个干净的完整版本。和--production不存在,因为它不是内置的Rush参数。rush.json不声明它,Rush 5用Unrecognized arguments: --production.

这项工作安装所有的东西,构建所有的东西,所以它与回购成线性关系。这是这种规模的正确默认值。下一部分是它在大的时候应该采取的形状。没有一个是在这个存储库中实现的。

How a developer machine, CI, and a shared remote build cache fit together

大型回购公司需要的工作流程

在大型回购中,结账是最先收缩的。

下面是一个设计,而不是我运行的管道。我测试了它所依赖的部分:选择器、过滤安装和缓存行为。我没有端到端地运行这个工作流,所以把它当作需要验证的东西,而不是一个处方。

最重要的规则是使用每步一个选择器。如果签出实现了一组不同于后面步骤所选择的项目,那么某个步骤最终会针对一个不在磁盘上的项目运行。

调用文件ci-affected.yml:

  • uses: actions/checkout@v4
    with:
    fetch-depth: 0
  • run: git fetch origin main
  • run: npx sparo-ci checkout --from git:origin/main
  • run: node common/scripts/install-run-rush.js install --from git:origin/main
  • run: node common/scripts/install-run-rush.js format:check
  • run: node common/scripts/install-run-rush.js lint --from git:origin/main
  • run: node common/scripts/install-run-rush.js typecheck --from git:origin/main
  • run: node common/scripts/install-run-rush.js build --from git:origin/main
  • run: git diff --exit-code -- apps/web/src/routeTree.gen.ts
    有些选择是经过深思熟虑的。它使用--from而不是--impacted-by,因为--impacted-by选择不包含依赖项的依赖项,并在cold worker上失败。它保持typecheck以前build。它使用build而不是rebuild,因为只有build读取缓存。它使用过滤安装,这是一个地方安装时间下降。

该片段的四个部分我还没有核实,并会先检查一个真正的回购。

fetch-depth: 0反对稀疏结帐。选择需要合并基础,这需要基础本地提交,所以浅层克隆破坏了它。克隆完整的历史是最直接的解决办法。--filter=blob:none使历史变得便宜,或者你可以只获取目标分支。

format:check无法限定范围。这是一个全局命令,所以它没有选择器,在稀疏树上它只看到被具体化的文件。它悄悄地检查比以前少。

仅当web应用程序位于所选集合中时,路由树漂移检查才有意义。在一个不接触的变化上apps/web,该步骤比较没有重新生成的文件。

安装步骤可能会彻底失败。这是上一节的约束:Rush需要每个注册项目的package.json在磁盘上。如果签出没有保留它们,那么无论如何安排其余部分,安装都会失败。

The pull request pipeline, with the build-cache branch made explicit

影响选择出错的四种方式

浅层克隆破坏了合并基础。这个回购fetch-depth: 2对于其路由树差异来说是足够,但对于选择来说是不够的。拉什解决了git:选择器,并退回到git merge-base -- HEAD 当引用不是完整的提交哈希时。这需要本地的基本提交。在浅层克隆上,缺少合并基础,或者它是浅层边界而不是真实边界。无论哪种方式,所选的集合都会无声无息地变得太小。

过时的缓存返回错误的输出。随着buildCacheEnabled: true,缓存恢复取代了输出保留,因此项目的输出文件夹总是被恢复所取代。缓存键包括项目的源文件,以及它的工作空间依赖项的源。它还介绍了外部npm依赖项的版本,包括间接依赖项和Rush命令行参数。它不包括环境变量或Node.js版本,除非您选择通过operationSettings\[\].dependsOnEnvVars和dependsOnNodeVersion。没有文件表明拉什或PNPM自己的版本会使条目无效。一个建筑process.env.SOMETHING因此将充满信心地恢复错误输出。拉什的医生指出rush-audit-cache-plugin用于查找您未申报的输入。

缓存读取和写入以错误的方式进行。 isCacheWriteAllowed在云提供商块中默认为false。记录的模式在开发人员的机器上是只读的,并且RUSH_BUILD_CACHE_WRITE_ALLOWED=1论CI。如果把它倒过来,开发人员的机器就会重写条目,而CI读不到任何有用的内容。

片状测试阻碍合并。二十分之一的测试失败比慢速测试更糟糕,因为它教人们重新运行直到绿灯。选择受影响的项目以加快速度,但是不要让合并决定依赖于您不信任的套件。

不要复制此回购的节点固定

这个repo运行三个不同的节点版本,并侥幸成功。mise.toml面向开发人员的pins节点24。上面的工作流使用节点22。两个docker文件都使用node:22-slim.

它之所以有效是因为rush.json强制一个范围而不是一个精确的版本,并且三者都在范围内。这也是因为拉什和PNPM被精确地固定,所以解析图形的工具在任何地方都是一样的。

我不会复制它。范围是一个公差,而不是一个引脚,漂移的故障模式是在一台机器上而不是在另一台机器上再现的构建。选择一个节点版本,并在所有三个地方使用它。

部署

部署从同一个问题开始。七个项目中的两个产生可运行的工件,并且都需要共享工作空间来构建。

docker文件是单阶段的,并且刻意注重实效。每个都是node:22-slim,复制整个存储库,使用install-run引导安装,并构建一个选择。从apps/api/Dockerfile:

FROM node:22-slim

WORKDIR /repo

COPY . .

RUN node common/scripts/install-run-rush.js install

RUN node common/scripts/install-run-rush.js build --to @hotel/api

CMD "node", "dist/main.js"

web变体有一个值得复制到任何Vite应用程序的细节。VITE_API_URL是一个版本ARG而不是运行时环境变量,因为Vite是内联的import.meta.env.*在构建时。服务器端呈现需要不同于浏览器的URL。它在容器内部运行,因此它必须通过其组合服务名而不是localhost.

单级选择以图像大小换取可靠性。拉什和PNPM通过符号链接将包链接到common/temp并且跨构建阶段复制该布局的删减子集是脆弱的。

rush deploy就是改进,这里不实现了。它复制选定的项目及其常规依赖项、npm包和本地项目。默认情况下,Dev依赖项被忽略。它的配置位于common/config/rush/deploy.json,这是您使用创建的rush init-deploy --project 并提交。输出到common/deploy/除非你通过--target-folder,以及--create-archive产生一个拉链。

需要注意的是linkCreation,它接受default, script,或者none。随着script你得到了一个create-links.js在服务器上运行,并且node_modules/.bin二进制文件还没有生成。

每个应用程序的管道与CI的选择机制不同。在以下情况下触发API管道--from表示API的闭包发生了变化,并且web管道对web闭包进行了相同的测试。

Merge-to-main deployment, with migration and API/web ordering

排序是部署与受影响的选择不同的地方。迁移在API部署之前运行,因为新代码需要新的模式。API在web应用程序之前部署,因为运行的前端可能会在它之前或之后调用一个版本的API。在API契约允许的范围内,尽可能缩短偏差窗口。

拉什和斯帕罗与其他竞争对手相比如何

Rush + Sparo Nx / Turborepo 巴泽尔/巴克2 标量,树苗/ EdenFS

工作单位 中的一个项目rush.json 工作区项目 A BUILD文件目标 回购中的一条路径

贮藏 每个项目的散列 任务哈希,远程缓存 内容寻址、沙盒化 没有人

Git缩放 稀疏检验和blobless克隆 未解决 未解决 重点是

迁移成本 注册软件包,一个锁文件 一个配置文件,保持你的布局 最高:构建文件,密封工具 低为标量;树苗的新客户

Nx和Turborepo优化了您已经安装的任何设备上的任务执行。Turborepo明确地不管理依赖关系。Bazel和Buck2以对每一次构建建模为代价,购买了隔离和可再现性。Buck2自己的就绪语言列表有9种,包括Haskell和OCaml。

Scalar、Sapling和EdenFS攻击Git的I/O而不是您的构建图,Scalar在上游Git中运行。这一组与前三组中的任何一组相结合,因为稀疏签出与您如何缓存构建是正交的。

什么时候不用这个

如果你的回购持有几十个包和一个团队,那就跳过所有这些。

安装成本是真实的。您维护一个注册表,同意一个锁文件策略,保持配置文件最新,并了解缓存失效。在这个尺寸之下,工具是一个比问题更大的维护表面。

如果你不是npm系列的包管理器,那么Rush是错误的选择,因为它的价值在于拥有npm锁文件和安装策略。

跳过构建缓存,直到您的项目声明了准确的输入和输出。恢复错误输出的缓存比没有缓存更糟糕,因为它很快而且是错误的。如果您的构建触及了项目文件夹之外的状态,或者读取了尚未在中声明的环境变量,也可以跳过它rush-project.json.

如果整个repo已经在一秒钟内签出,或者如果您的工具假设一个完整的树并且您不能更改它们,则跳过稀疏签出。这docs/和.github/上面的经验就是这个样子,它适用于搜索工具和CI脚本,也适用于ide。

最后,不要凭直觉接受这些。如果您不能陈述您当前的克隆时间、安装时间和CI p50,您就无法判断某个更改是否有帮助。

如何衡量自己的回购

使用四个命令建立基线:

du -sh .git # total Git footprint on disk

git count-objects -vH # object count and pack size

git ls-files | wc -l # index size: what Git knows about

time git status # the command you run a hundred times a day

count-objects -vH印刷品in-pack和size-pack,也就是当你去博客时会移动的数字。比较git ls-files | wc -l与实际磁盘上的文件进行比较,以查看配置文件删除了哪些内容。

然后衡量这些命令不能告诉你的两件事。安装时冷时热。统计您最常进行更改的项目。每个计时运行五次,并报告p50和p95,而不是平均值。克隆和安装时间是重尾的,所以平均时间是由哪个运行慢决定的。

本教程没有涵盖的内容

docker文件是单阶段的,并且rush deploy是后续而非实施。构建缓存是关闭的,所以这里没有使用缓存键。没有Bazel或Buck2深度,该表行总结了每个项目自己的文档。

这两个Sparo配置文件是手写的$schemaURL未解析,也未通过验证sparo init-profile。CI安装和重建一切。未配置子空间和阶段命令。并且没有真正的测试套件:每一个test脚本是echo "no tests"。因此,片状试验的失效模式是从设计方面描述的,而不是观察到的。

相关推荐
will_net1 小时前
Vue IIS部署 api 访问当前站点 代理跳转真实API 问题
前端·javascript·vue.js
weixin_449173651 小时前
react 的useEffect用法
前端·javascript·react.js
坊钰1 小时前
【LangGraph框架入门级】12. 人机交互:interrupt 中断与 Command 恢复
开发语言·数据库·python·人机交互·ai编程·langgraph
开开心心就好1 小时前
打印机卡住打不了?一键清理卡死打印队列
服务器·开发语言·算法·pdf·ocr·idea·visual studio code
David猪大卫1 小时前
【C++修炼】STL容器—模板进阶:非类型模板参数、模板特化、模板分离编译
c语言·开发语言·c++·经验分享·笔记·学习·面试
牛战士coding1 小时前
Java 对象写入 Redis 后类型变了?Jackson 多态序列化问题复盘
java·开发语言·redis
Chasing__Dreams1 小时前
python杂识--24--Nuitak打包exe
开发语言·python
yuniko-n1 小时前
【JUC】读写锁
java·开发语言
niucloud-admin1 小时前
NIUCLOUD LITE AI开发文档--基础开发规范
开发语言·php