乐趣区

关于monorepo:基于-Rush-的-Monorepo-多包发布实践

前言

五月份分享了 利用级 Monorepo 优化计划,次要论述了之前 monorepo(Yarn + Lerna)存在的问题以及解决方案,但在该分享里,并没有波及到 pacakge 公布相干的内容(在那段期间次要是以利用 app 开发为主),偶有 pacakge 开发也是依赖关系较为简单的场景(单包开发 / 公布),应用 npm publish 就能搞定。

随着后续的倒退(次要是团队内另一个仓库的迁入),package 开发场景占了相当重的比例(仓库代码行数达到了百万级,项目数超过 100),但多包公布体验并不是很好,次要集中在以下 3 个方面:

  1. 公布形式与 Lerna 差别较大,而 Rush 相干命令文档较为简陋(太简陋了,参数试了很多遍),无奈迅速上手;
  2. 公布流程不够标准,根本靠手敲命令行;
  3. 短少规范的开发工作流。

本次分享便是为了解决以上问题,在理论中摸索出 Monorepo 多包公布场景的较佳实际。

Workspace protocol (workspace:)

在进行探讨之前,先要理解 Workspace protocol (workspace:),这里以 pnpm 为例,下述例子摘自 Workspace | pnpm

默认状况下,如果工作区中可用的包版本与申明的范畴相匹配,pnpm 将从工作区链接包。比方 monorepo 中存在 foo@1.0.0,而 monorepo 内另一个我的项目 bar 依赖 "foo: ^1.0.0",那么 bar 就会应用工作区中的 foo,假使 bar 依赖 "foo: 2.0.0",那么 pnpm 将会从远端下载 foo@2.0.0 供 bar 应用,这就引入了一些不确定性。

应用 workspace 协定时,pnpm 将回绝解析为本地工作区包以外的任何内容。因而,如果设置 "foo": "workspace:2.0.0",这次装置将失败,因为工作区中不存在 "foo@2.0.0"

多包公布

根底操作

相较于传统单仓库单包一个一个的去手动进行公布行为,monorepo 的劣势之一就是能够不便地进行多包公布。

rush change

在 Rush monorepo 中,rush change 是发包流程的终点,其产物 <branchname>-<timestamp>.json(后文用 changefile.json 代替)会被 rush version 以及 rush publish 生产。

changefile.json 生成流程如下:

  1. 检测以后分支与指标分支(通常是 master)的差别,筛选出存在变更的我的项目(基于 git diff 命令);
  2. 针对筛选进去的每一个我的项目通过交互式命令行询问一些信息(如版本更新策略以及更新的内容简要形容);
  3. 基于上述信息在 common/changes 目录下生成对应 package 的 changefile.json。

留神:截图中的变更类型(type 字段)为 none,不是 major/minor/patch 中的任何一种,none 意味着 “ 将这些变更滚入下一个补丁(patch)、主要变更(minor)或次要变更(major)”,因而实践上,如果一个我的项目只存在类型为 “none “ 的变更文件,它既不会耗费文件,也不会晋升版本。

type: none 的个性使得咱们能够将 已开发结束但不须要追随下一次公布周期 的 package 提前合入 master,直到该 pacakge 呈现 type 不为 none 的 changefile.json。

rush version 与 rush publish

rush versionrush publish --apply 则会基于生成的 changefile.json 进行版本号的更新(即 bump version,遵循 semver 标准,被公布 package 的下层 package 的版本号可能会被更新,下一大节会详细描述)。

rush publish --publish 则会基于 changefile.json 进行对应 package 的公布。

Rush 的公布流程与另一个风行的 Monorepo 场景发包工具 Changesets 基本一致,遇到单纯的 PNPM Monorepo 兴许能够基于 Changesets 复用本文计划 🥳。

  • 🦋 A way to manage your versioning and changelogs with a focus on monorepos
  • Changesets: 风行的 Monorepo 场景发包工具

级联公布

后面有提到在更新版本号时,除了更新以后须要被公布的 package 的版本号,也可能更新其下层 package 的版本号,这取决于下层 package 在 package.json 中如何援用以后 package 的。

如下所示,@modern-js/plugin-tailwindcss(下层 package)通过 "workspace:^1.0.0" 的模式引入 @modern-js/utils(底层 package)。

package.json(@modern-js/plugin-tailwindcss)

{
  "name": "@modern-js/plugin-tailwindcss",
  "version": "1.0.0",
  "dependencies": {"@modern-js/utils": "workspace:^1.0.0"}
}

package.json(@modern-js/utils)

{
  "name": "@modern-js/utils",
  "version": "1.0.0"
}
  • @modern-js/utils 更新至 1.0.1,Rush 在更新版本号时不会更新 @modern-js/plugin-tailwindcss 的版本号。因为 ^1.0.0 兼容 1.0.1,从语义的角度登程,@modern-js/plugin-tailwindcss 不须要更新版本号,间接装置 @modern-js/plugin-tailwindcss@1.0.0 是能够获取到 @modern-js/utils@1.0.1
  • @modern-js/utils 更新至 2.0.0,Rush 在更新版本号时会更新 @modern-js/plugin-tailwindcss 的版本号至 1.0.1。因为 ^1.0.0 不兼容 2.0.0,更新 @modern-js/plugin-tailwindcss 版本至 1.0.1 才可援用到最新的 @modern-js/utils@2.0.0,此时 @modern-js/plugin-tailwindcss 的 package.json 内容如下:
{
  "name": "@modern-js/plugin-tailwindcss",
  "version": "1.0.1",
  "dependencies": {
   // 援用版本号也产生了变动 
    "@modern-js/utils": "workspace:^2.0.0"
  }
}

更新了版本号,还须要公布至 npm。此时须要 rush publish 减少 --include-all 参数,配置该参数后 rush publish 查看到仓库中存在 shouldPublish: true 的 package 的版本新于 npm 版本时,会将该 package 公布。

这样就实现了基于语义化的级联公布。

意料之外的公布

最开始基于 Rush 革新我的项目时,monorepo 内的我的项目之间始终应用 "workspace: *" 相互援用,即应用 monorepo 内最新版本。

这就导致了两个问题:

  1. app 上线时,可能会带上开发过程中的 package 上线(基于 Trunk Based Development 的开发分支模型,master 为 trunk 分支)
  2. 发包时会带上意料之外的公布,因为应用了 "workspace: *",所以底层包更新,下层包必然公布(为了保障 * 的语义)

所以,monorepo 内我的项目之间的援用须要遵循以下标准。

援用标准

  1. 判断是否须要应用 workspace: 援用 monorepo 内的最新版本
  2. 如果须要应用 workspace:,那么请应用 "workspace: ^x.x.x" 代替 "workspace: *",防止无意义的公布(当然也要思考理论依赖关系,如果 packageA 与 packageB 始终须要一起公布,就该当应用 "workspace: *"

随着 monorepo 内项目数日益增长,我的项目之间若全副应用 workspace: 援用,那么一个 package 更新时,须要其所有外部接入方被动进行回归测试

通过 CI 进步 master 分支准入规范当然没问题,然而业务场景往往过于简单,还是须要测试同学的染指,除非团队成员代码品质以及测试品质极高。

或者应用 feature flag 机制进行管制,但人工控制往往老本较大,须要成熟的基建计划配合。

而对于业务关联并不严密的我的项目,他们仅仅是在物理层面存在于同一个 monorepo 内罢了,并不需要关注对方最新版本,并不需要应用 workspace:

举个例子:把 babel/react/modernjs 等套件放到同一个 monorepo 治理,各套件外部应用 workspace: 享受 monorepo 工程劣势荒诞不经,我的项目之间依赖则间接应用 npm 稳固版本更适合。

当然开源我的项目的业务边界都很显著,而具体到咱们的业务仓库(一个团队一个仓库),外面可能放着许多八竿子打不着的模块,这个时候应用 workspace: 便是自讨苦吃了。

那么如何判断是否须要应用 workspace:

举个例子,假如我是包 bar 的 owner,当初要援用一个包 foo,须要通过以下判断:

foo 更新,bar 肯定会更新并进行测试且迭代上线节奏统一,那么应用 workspace:,否则一律应用 npm 远端版本。

工作流

须要留神的是:

  1. 开发阶段性能点阶段性合入 trunk 分支(master 分支)时,生成的是 type: none 的 changefile.json,这是为了防止其余 package 公布时带上处于开发过程中的包
  2. 因为须要生成 type: major/minor/patch 的 changefile.json 在测试分支进行测试包公布,所以测试阶段则不进行合入,待验收结束后合入进行正式版本公布。

流水线详解

测试版本

  1. 基于 changefile.json 获取本次须要公布的 package
  2. 按需装置指标 package 的依赖

    • rush install -t package1 -t package2
  3. 按需构建指标 package

    • rush build -t package1 -t package2
  4. rush publish 读取 changefile.json 进行版本号更新

    • rush publish –prerelease-name [canary.x] –apply
  5. rush publish 公布版本号变更的 package

    • rush publish –publish –tag canary –include-all –set-access-level public
  6. 通过机器人将公布信息同步至相干告诉群

正式版本

  1. 基于 changefile.json 获取本次须要公布的 package
  2. 按需装置指标 package 的依赖

    • rush install -t package1 -t package2
  3. 按需构建指标 package

    • rush build -t package1 -t package2
  4. 拉取一个指标分支用于承载公布流程中产生的 commits(能够将该分支了解为 release 分支)
  5. rush version 在上一步拉取的指标分支上生产 changefile.json 更新版本号并生成 CHANGELOG.md

    • rush version –bump –target-branch [source-branch] –ignore-git-hooks
  6. 在指标分支上执行 rush update 更新 lockfile,防止 package.json 与 lockfile 不统一
  7. rush publish 公布 package 至 npm

    • rush publish –apply –publish –include-all –target-branch [source-branch] –add-commit-details –set-access-level public
  8. 生成一个将指标分支合并至 master 分支的 Merge Request

    1. Deleting change files and updating change logs for package updates.
    2. Applying package updates.
    3. rush update.
  9. 通过机器人将公布信息同步至相干告诉群(蕴含 Merge Request 信息,须要及时合入)

公布减速

能够看到,公布流程中的前三个步骤都是统一的:

  1. 基于 changefile.json 获取本次须要公布的 package
  2. 按需装置指标 package 的依赖

    • rush install -t package1 -t package2
  3. 按需构建指标 package

    • rush build -t package1 -t package2

但这套计划刚刚落地时,应用的是「全量装置 monorepo 依赖并全量构建 packages」这种简略粗犷的形式。

monorepo 须要解决的是规模性问题:我的项目越来越大,依赖装置越来越慢,构建越来越慢,跑测试用例越来越慢。

「按需」就成为了关键词,pnpm 作为包管理器曾经十分优良,甚至能够按需装置依赖,但对于大型 monorepo 须要的能力还是有所欠缺的,所以咱们引入了 Rush 解决 monorepo 下的工程化问题。

所以指标很明确:在 monorepo 规模越来越大的状况下,整个我的项目的复杂度始终维持在一个稳固的水准。—— 利用级 Monorepo 优化计划

在优化之前,公布一次靠近 12min,哪怕只有公布一个包,并且这个包里只有一句 console.log("hello world"),而且随着我的项目的增多,12min 可能只是终点。所以「按需」又回到了咱们的眼帘。

Rush 在公布流程中会扭转须要公布的我的项目的版本号,只有将这个过程提前,事后获取扭转了版本号的我的项目,就能失去 install 与 build 命令的指标参数。

于是通过翻阅 @microsoft/rush-librush version 相干源码,失去了以下代码:

function getVersionUpdatedPackages(params: {
  rushConfiguration: RushConfiguration;
  prereleaseName?: string;
}) {const { prereleaseName, rushConfiguration} = params;
  const changeManager: ChangeManager = new ChangeManager(rushConfiguration);

  if (prereleaseName) {const prereleaseToken = new PrereleaseToken(prereleaseName);
    changeManager.load(rushConfiguration.changesFolder, prereleaseToken);
  } else {changeManager.load(rushConfiguration.changesFolder);
  }
  // 扭转 package.json 版本号(内存中,理论文件不做改变)changeManager.apply(false);
  return rushConfiguration.projects.reduce((accu, project) => {
    const packagePath: string = path.join(
      project.projectFolder,
      FileConstants.PackageJson,
    );
    // 理论 package.json 的版本号
    const oldVersion = (JsonFile.load(packagePath) as IPackageJson).version;
    // 内存中 package.json 的版本号
    const newVersion = project.packageJsonEditor.version;
    // 不统一则为咱们的指标我的项目
    if (oldVersion !== newVersion) {accu.push({ name: project.packageName, oldVersion, newVersion});
    }
    return accu;
  }, [] as UpdatedPackage[]);
}

辅助命令

rush change-extra

源自接入方 lockfile 造成的困扰。该命令能够为未变更的 package 生成 changefile.json,使其能够被公布。

rush change 命令默认会比对以后分支与 master 分支的差别,找出产生变更的我的项目,通过交互式命令行让开发者生成对应的 changefile.json 文件。

后面「级联公布」中有提到,Rush 能够依据 semver 标准更新相干包的版本并进行公布,在 "workspace: ^x.x.x" 的援用形式下,除非底层包进行 major 更新,否则下层包是不会更新公布的。

问题就在于此,下层包没有被公布,底层包又被接入方的 lockfile 锁住了,咱们(被迫)须要一种计划可能公布实际上不须要公布的包(这里是 @jupiter/block-tools),这就是 rush change-extra 诞生的起因。

更须要一种可能深度更新指定依赖的形式,但目前没有找到包管理器维度的解决方案

结语

本文通过 Rush 根本的发包操作动手,介绍了在理论开发过程中会遇到的一些问题并给出了整体落地的计划,同时基于「按需」的思路优化线上公布速度。

退出移动版