Repository: hyperlane-xyz/hyperlane-monorepo
Pull request #6331: feat: integrate zksync on CLI
Author: login: mshojaei-txfusion
Created: 2025-05-23T12:28:09Z | Merged: 2025-05-28T09:40:20Z

### Description
This PR adds ZKSync compatibility validation for core contract deployments:

Adds pre-deployment check to validate ISM compatibility with chain's technical stack
Integrates isIsmCompatible utility from SDK
Adds assertion handling for incompatible ISM configurations
https://github.com/txfusion/hyperlane-monorepo/pull/25

### Related issues
https://linear.app/hyperlane-xyz/issue/ENG-1155

Previously was: https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/5216

### Backward compatibility
Yes - This change adds additional validation but does not modify existing deployment behavior for supported chains.

Testing
Passed build


Diff:
diff --git a/.changeset/empty-dingos-ring.md b/.changeset/empty-dingos-ring.md
new file mode 100644
index 00000000000..9525a109b3e
--- /dev/null
+++ b/.changeset/empty-dingos-ring.md
@@ -0,0 +1,5 @@
+---
+'@hyperlane-xyz/cli': minor
+---
+
+Add support for zksync based networks
diff --git a/typescript/cli/src/config/ism.ts b/typescript/cli/src/config/ism.ts
index f5ddcaab1e7..01d9b4d67b4 100644
--- a/typescript/cli/src/config/ism.ts
+++ b/typescript/cli/src/config/ism.ts
@@ -10,6 +10,7 @@ import {
   MultisigIsmConfig,
   MultisigIsmConfigSchema,
   TrustedRelayerIsmConfig,
+  isStaticIsm,
 } from '@hyperlane-xyz/sdk';
 
 import { CommandContext } from '../context/types.js';
@@ -89,6 +90,7 @@ const ISM_TYPE_DESCRIPTIONS: Record<string, string> = {
 
 export async function createAdvancedIsmConfig(
   context: CommandContext,
+  excludeStaticIsms: boolean = false,
 ): Promise<IsmConfig> {
   logBlue('Creating a new advanced ISM config');
   logBoldUnderlinedRed('WARNING: USE AT YOUR RISK.');
@@ -98,12 +100,12 @@ export async function createAdvancedIsmConfig(
 
   const moduleType = await select({
     message: 'Select ISM type',
-    choices: Object.entries(ISM_TYPE_DESCRIPTIONS).map(
-      ([value, description]) => ({
+    choices: Object.entries(ISM_TYPE_DESCRIPTIONS)
+      .filter(([value]) => !excludeStaticIsms || !isStaticIsm(value as IsmType))
+      .map(([value, description]) => ({
         value,
         description,
-      }),
-    ),
+      })),
     pageSize: 10,
   });
 
diff --git a/typescript/cli/src/config/warp.ts b/typescript/cli/src/config/warp.ts
index 6b41ab524de..2dba6533c60 100644
--- a/typescript/cli/src/config/warp.ts
+++ b/typescript/cli/src/config/warp.ts
@@ -3,6 +3,7 @@ import { stringify as yamlStringify } from 'yaml';
 
 import {
   ChainMap,
+  ChainTechnicalStack,
   DeployedOwnableConfig,
   HypERC20Deployer,
   IsmConfig,
@@ -155,6 +156,10 @@ export async function createWarpRouteDeployConfig({
     const proxyAdmin: DeployedOwnableConfig | undefined =
       await setProxyAdminConfig(context, chain);
 
+    const excludeStaticIsms =
+      context.multiProvider.getChainMetadata(chain).technicalStack ===
+      ChainTechnicalStack.ZkSync;
+
     /**
      * The logic from the cli is as follows:
      *  --yes flag is provided: set ism to undefined (default ISM config)
@@ -167,13 +172,19 @@ export async function createWarpRouteDeployConfig({
     if (context.skipConfirmation) {
       interchainSecurityModule = undefined;
     } else if (advanced) {
-      interchainSecurityModule = await createAdvancedIsmConfig(context);
+      interchainSecurityModule = await createAdvancedIsmConfig(
+        context,
+        excludeStaticIsms,
+      );
     } else if (
       await confirm({
         message: 'Do you want to use a trusted ISM for warp route?',
       })
     ) {
-      interchainSecurityModule = createDefaultWarpIsmConfig(owner);
+      interchainSecurityModule = createDefaultWarpIsmConfig(
+        owner,
+        excludeStaticIsms,
+      );
     }
 
     const type = await select({
@@ -335,23 +346,36 @@ export async function readWarpCoreConfig(
 }
 
 /**
- * Creates a default configuration for an ISM with a TRUSTED_RELAYER and FALLBACK_ROUTING.
+ * Creates a default configuration for an ISM.
  *
- * Properties relayer and owner are both set as input owner.
+ * When excludeStaticIsms is false (default):
+ * - Creates an AGGREGATION ISM with TRUSTED_RELAYER and FALLBACK_ROUTING modules
+ * - Properties relayer and owner are both set as input owner
  *
- * @param owner - The address of the owner of the ISM.
- * @returns The default Aggregation ISM configuration.
+ * When excludeStaticIsms is true:
+ * - Creates only a TRUSTED_RELAYER ISM (as static ISMs like AGGREGATION are not supported)
+ * - Properties relayer is set as input owner
+ *
+ * @param owner - The address of the owner of the ISM
+ * @param excludeStaticIsms - Whether to exclude static ISM types (default: false)
+ * @returns The ISM configuration
  */
-function createDefaultWarpIsmConfig(owner: Address): IsmConfig {
+function createDefaultWarpIsmConfig(
+  owner: Address,
+  excludeStaticIsms: boolean = false,
+): IsmConfig {
+  const trustedRelayerModule: IsmConfig = {
+    type: IsmType.TRUSTED_RELAYER,
+    relayer: owner,
+  };
+
+  if (excludeStaticIsms) {
+    return trustedRelayerModule;
+  }
+
   return {
     type: IsmType.AGGREGATION,
-    modules: [
-      {
-        type: IsmType.TRUSTED_RELAYER,
-        relayer: owner,
-      },
-      createFallbackRoutingConfig(owner),
-    ],
+    modules: [trustedRelayerModule, createFallbackRoutingConfig(owner)],
     threshold: 1,
   };
 }
diff --git a/typescript/cli/src/deploy/core.ts b/typescript/cli/src/deploy/core.ts
index ad1dc0f5a0a..03e9c89f7da 100644
--- a/typescript/cli/src/deploy/core.ts
+++ b/typescript/cli/src/deploy/core.ts
@@ -27,6 +27,7 @@ import {
   prepareDeploy,
   runDeployPlanStep,
   runPreflightChecksForChains,
+  validateCoreIsmCompatibility,
 } from './utils.js';
 
 interface DeployParams {
@@ -66,6 +67,10 @@ export async function runCoreDeploy(params: DeployParams) {
       'Select chain to connect:',
     );
   }
+
+  // Validate ISM compatibility
+  validateCoreIsmCompatibility(chain, config, context);
+
   let apiKeys: ChainMap<string> = {};
   if (!skipConfirmation)
     apiKeys = await requestAndSaveApiKeys([chain], chainMetadata, registry);
diff --git a/typescript/cli/src/deploy/utils.ts b/typescript/cli/src/deploy/utils.ts
index 1f11f525962..e179643b66b 100644
--- a/typescript/cli/src/deploy/utils.ts
+++ b/typescript/cli/src/deploy/utils.ts
@@ -5,11 +5,15 @@ import {
   ChainMap,
   ChainMetadata,
   ChainName,
+  CoreConfig,
   IsmConfig,
+  IsmType,
   MultisigConfig,
+  WarpRouteDeployConfig,
   getLocalProvider,
+  isIsmCompatible,
 } from '@hyperlane-xyz/sdk';
-import { Address, ProtocolType } from '@hyperlane-xyz/utils';
+import { Address, ProtocolType, assert } from '@hyperlane-xyz/utils';
 
 import { parseIsmConfig } from '../config/ism.js';
 import { CommandContext, WriteCommandContext } from '../context/types.js';
@@ -191,3 +195,68 @@ function transformChainMetadataForDisplay(chainMetadata: ChainMetadata) {
     'Native Token: Decimals': chainMetadata.nativeToken?.decimals,
   };
 }
+
+function validateIsmCompatibility({
+  chain,
+  ismType,
+  context,
+}: {
+  chain: ChainName;
+  ismType?: IsmType;
+  context: WriteCommandContext;
+}) {
+  const { technicalStack: chainTechnicalStack } =
+    context.multiProvider.getChainMetadata(chain);
+
+  if (ismType) {
+    assert(
+      isIsmCompatible({
+        ismType,
+        chainTechnicalStack,
+      }),
+      `Selected ISM of type ${ismType} is not compatible with the selected Chain Technical Stack of ${chainTechnicalStack} for chain ${chain}!`,
+    );
+  }
+}
+
+/**
+ * Validates that the ISM configuration is compatible with the chain's technical stack.
+ * Throws an error if an incompatible ISM type is configured.
+ */
+export function validateCoreIsmCompatibility(
+  chain: ChainName,
+  config: CoreConfig,
+  context: WriteCommandContext,
+) {
+  if (typeof config.defaultIsm !== 'string') {
+    validateIsmCompatibility({
+      chain,
+      ismType: config.defaultIsm?.type,
+      context,
+    });
+  }
+}
+
+/**
+ * Validates that the ISM configurations are compatible with each chain's technical stack.
+ * Throws an error if an incompatible ISM type is configured for a chain.
+ */
+export function validateWarpIsmCompatibility(
+  warpRouteConfig: WarpRouteDeployConfig,
+  context: WriteCommandContext,
+) {
+  for (const chain of Object.keys(warpRouteConfig)) {
+    const config = warpRouteConfig[chain];
+
+    if (
+      config.interchainSecurityModule &&
+      typeof config.interchainSecurityModule !== 'string'
+    ) {
+      validateIsmCompatibility({
+        chain,
+        ismType: config.interchainSecurityModule.type,
+        context,
+      });
+    }
+  }
+}
diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts
index fb75adab64e..cde4174928f 100644
--- a/typescript/cli/src/deploy/warp.ts
+++ b/typescript/cli/src/deploy/warp.ts
@@ -71,6 +71,7 @@ import {
   completeDeploy,
   prepareDeploy,
   runPreflightChecksForChains,
+  validateWarpIsmCompatibility,
 } from './utils.js';
 
 interface DeployParams {
@@ -92,6 +93,10 @@ export async function runWarpRouteDeploy({
   warpDeployConfig: WarpRouteDeployConfigMailboxRequired;
 }) {
   const { skipConfirmation, chainMetadata, registry } = context;
+
+  // Validate ISM compatibility for all chains
+  validateWarpIsmCompatibility(warpDeployConfig, context);
+
   const chains = Object.keys(warpDeployConfig);
 
   let apiKeys: ChainMap<string> = {};
@@ -104,7 +109,6 @@ export async function runWarpRouteDeploy({
   };
 
   await runDeployPlanStep(deploymentParams);
-
   // Some of the below functions throw if passed non-EVM chains
   const ethereumChains = chains.filter(
     (chain) => chainMetadata[chain].protocol === ProtocolType.Ethereum,


Reviews:
user: login: paulbalaji
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-26T15:32:53Z
user: login: xeno097
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T15:36:27Z
user: login: xeno097
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T15:37:02Z
user: login: paulbalaji
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-26T15:54:35Z
user: login: paulbalaji
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-26T16:16:42Z
user: login: xeno097
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T22:12:36Z

Review comments:
diff_hunk: @@ -143,6 +156,34 @@ async function runDeployPlanStep({ context, warpDeployConfig }: DeployParams) {
   if (!isConfirmed) throw new Error('Deployment cancelled');
 }
 
+/**
+ * Validates that the ISM configurations are compatible with each chain's technical stack.
+ * Throws an error if an incompatible ISM type is configured for a chain.
+ */
+function validateIsmCompatibility(
+  warpRouteConfig: WarpRouteDeployConfig,
+  context: WriteCommandContext,
+) {
+  for (const chain of Object.keys(warpRouteConfig)) {
+    const config = warpRouteConfig[chain];
+    const { technicalStack: chainTechnicalStack } =
+      context.multiProvider.getChainMetadata(chain);
+
+    if (
+      config.interchainSecurityModule &&
+      typeof config.interchainSecurityModule !== 'string'
+    ) {
+      assert(
+        isIsmCompatible({
+          chainTechnicalStack,
+          ismType: config.interchainSecurityModule.type,
+        }),
+        `Selected ISM of type ${config.interchainSecurityModule.type} is not compatible with the selected Chain Technical Stack of ${chainTechnicalStack} for chain ${chain}!`,
+      );
+    }
+  }
+}
+
 async function executeDeploy(
path: typescript/cli/src/deploy/warp.ts
user: login: paulbalaji
body: i think this is all before the warp module existed - needs to be migrated to warp module nomenclature
created_at: 2025-05-26T15:32:53Z
author_association: COLLABORATOR
diff_hunk: @@ -143,6 +156,34 @@ async function runDeployPlanStep({ context, warpDeployConfig }: DeployParams) {
   if (!isConfirmed) throw new Error('Deployment cancelled');
 }
 
+/**
+ * Validates that the ISM configurations are compatible with each chain's technical stack.
+ * Throws an error if an incompatible ISM type is configured for a chain.
+ */
+function validateIsmCompatibility(
+  warpRouteConfig: WarpRouteDeployConfig,
+  context: WriteCommandContext,
+) {
+  for (const chain of Object.keys(warpRouteConfig)) {
+    const config = warpRouteConfig[chain];
+    const { technicalStack: chainTechnicalStack } =
+      context.multiProvider.getChainMetadata(chain);
+
+    if (
+      config.interchainSecurityModule &&
+      typeof config.interchainSecurityModule !== 'string'
+    ) {
+      assert(
+        isIsmCompatible({
+          chainTechnicalStack,
+          ismType: config.interchainSecurityModule.type,
+        }),
+        `Selected ISM of type ${config.interchainSecurityModule.type} is not compatible with the selected Chain Technical Stack of ${chainTechnicalStack} for chain ${chain}!`,
+      );
+    }
+  }
path: typescript/cli/src/deploy/warp.ts
user: login: xeno097
body: Isn't this quite similar to [this](https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/b6d03cc18301843dddb6d4d5561a23f57fe8c582/typescript/cli/src/deploy/core.ts#L163-L180) with the only difference that here we iterate over each chain?
created_at: 2025-05-26T15:35:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -143,6 +156,34 @@ async function runDeployPlanStep({ context, warpDeployConfig }: DeployParams) {
   if (!isConfirmed) throw new Error('Deployment cancelled');
 }
 
+/**
+ * Validates that the ISM configurations are compatible with each chain's technical stack.
+ * Throws an error if an incompatible ISM type is configured for a chain.
+ */
+function validateIsmCompatibility(
+  warpRouteConfig: WarpRouteDeployConfig,
+  context: WriteCommandContext,
+) {
+  for (const chain of Object.keys(warpRouteConfig)) {
+    const config = warpRouteConfig[chain];
+    const { technicalStack: chainTechnicalStack } =
+      context.multiProvider.getChainMetadata(chain);
+
+    if (
+      config.interchainSecurityModule &&
+      typeof config.interchainSecurityModule !== 'string'
+    ) {
+      assert(
+        isIsmCompatible({
+          chainTechnicalStack,
+          ismType: config.interchainSecurityModule.type,
+        }),
+        `Selected ISM of type ${config.interchainSecurityModule.type} is not compatible with the selected Chain Technical Stack of ${chainTechnicalStack} for chain ${chain}!`,
+      );
+    }
+  }
+}
+
 async function executeDeploy(
path: typescript/cli/src/deploy/warp.ts
user: login: xeno097
body: yes it is a relic from the past
created_at: 2025-05-26T15:37:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -143,6 +156,34 @@ async function runDeployPlanStep({ context, warpDeployConfig }: DeployParams) {
   if (!isConfirmed) throw new Error('Deployment cancelled');
 }
 
+/**
+ * Validates that the ISM configurations are compatible with each chain's technical stack.
+ * Throws an error if an incompatible ISM type is configured for a chain.
+ */
+function validateIsmCompatibility(
+  warpRouteConfig: WarpRouteDeployConfig,
+  context: WriteCommandContext,
+) {
+  for (const chain of Object.keys(warpRouteConfig)) {
+    const config = warpRouteConfig[chain];
+    const { technicalStack: chainTechnicalStack } =
+      context.multiProvider.getChainMetadata(chain);
+
+    if (
+      config.interchainSecurityModule &&
+      typeof config.interchainSecurityModule !== 'string'
+    ) {
+      assert(
+        isIsmCompatible({
+          chainTechnicalStack,
+          ismType: config.interchainSecurityModule.type,
+        }),
+        `Selected ISM of type ${config.interchainSecurityModule.type} is not compatible with the selected Chain Technical Stack of ${chainTechnicalStack} for chain ${chain}!`,
+      );
+    }
+  }
path: typescript/cli/src/deploy/warp.ts
user: login: paulbalaji
body: yeah exactly - will be refactoring as part of the move to modules once the SDK pr is merged
created_at: 2025-05-26T15:54:35Z
author_association: COLLABORATOR
diff_hunk: @@ -143,6 +156,34 @@ async function runDeployPlanStep({ context, warpDeployConfig }: DeployParams) {
   if (!isConfirmed) throw new Error('Deployment cancelled');
 }
 
+/**
+ * Validates that the ISM configurations are compatible with each chain's technical stack.
+ * Throws an error if an incompatible ISM type is configured for a chain.
+ */
+function validateIsmCompatibility(
+  warpRouteConfig: WarpRouteDeployConfig,
+  context: WriteCommandContext,
+) {
+  for (const chain of Object.keys(warpRouteConfig)) {
+    const config = warpRouteConfig[chain];
+    const { technicalStack: chainTechnicalStack } =
+      context.multiProvider.getChainMetadata(chain);
+
+    if (
+      config.interchainSecurityModule &&
+      typeof config.interchainSecurityModule !== 'string'
+    ) {
+      assert(
+        isIsmCompatible({
+          chainTechnicalStack,
+          ismType: config.interchainSecurityModule.type,
+        }),
+        `Selected ISM of type ${config.interchainSecurityModule.type} is not compatible with the selected Chain Technical Stack of ${chainTechnicalStack} for chain ${chain}!`,
+      );
+    }
+  }
path: typescript/cli/src/deploy/warp.ts
user: login: paulbalaji
body: so theyre similar but not exact - still refactored so it reuses more logic 
created_at: 2025-05-26T16:16:41Z
author_association: COLLABORATOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-05-23T12:28:13Z
body: ###  🦋  Changeset detected

Latest commit: c864a60df6f59af628910d430317eac2b0784b44

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 13 packages</summary>

  | Name                         | Type  |
| ---------------------------- | ----- |
| @hyperlane-xyz/cli           | Minor |
| @hyperlane-xyz/ccip-server   | Minor |
| @hyperlane-xyz/cosmos-sdk    | Minor |
| @hyperlane-xyz/cosmos-types  | Minor |
| @hyperlane-xyz/github-proxy  | Minor |
| @hyperlane-xyz/helloworld    | Minor |
| @hyperlane-xyz/infra         | Minor |
| @hyperlane-xyz/sdk           | Minor |
| @hyperlane-xyz/tsconfig      | Minor |
| @hyperlane-xyz/utils         | Minor |
| @hyperlane-xyz/widgets       | Minor |
| @hyperlane-xyz/starknet-core | Minor |
| @hyperlane-xyz/core          | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/hyperlane-xyz/hyperlane-monorepo/new/feat/integrate-zksync-on-cli?filename=.changeset/polite-zoos-explode.md&value=---%0A%22%40hyperlane-xyz%2Fcli%22%3A%20patch%0A---%0A%0Afeat%3A%20integrate%20zksync%20on%20CLI%0A)


author_association: NONE
user: login: mergify[bot]
created_at: 2025-05-23T12:28:42Z
body: ⚠️ The sha of the head commit of this PR conflicts with #5216. Mergify cannot evaluate rules on this PR. ⚠️
author_association: NONE
user: login: codecov[bot]
created_at: 2025-05-23T12:29:52Z
body: ## [Codecov](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/pull/6331?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 0.00%. Comparing base [(`36218a4`)](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/commit/36218a4c6f8ae2e97315e7390887eed1a7727be8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) to head [(`ecc9518`)](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/commit/ecc95188d5c2af0e02ab9d9f64114748a2faaf0e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz).
> Report is 20 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@          Coverage Diff          @@
##            main   #6331   +/-   ##
=====================================
  Coverage   0.00%   0.00%           
=====================================
  Files          1       1           
  Lines         14      14           
=====================================
  Misses        14      14           
```

| [Components](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/pull/6331/components?src=pr&el=components&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) | Coverage Δ | |
|---|---|---|
| [core](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/pull/6331/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) | `∅ <ø> (∅)` | |
| [hooks](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/pull/6331/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) | `∅ <ø> (∅)` | |
| [isms](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/pull/6331/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) | `∅ <ø> (∅)` | |
| [token](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/pull/6331/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) | `∅ <ø> (∅)` | |
| [middlewares](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/pull/6331/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) | `∅ <ø> (∅)` | |

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE