From 03c8ef7344bc2b022c1790967d152d8b60a5d975 Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@arkins.net>
Date: Tue, 8 Aug 2023 16:29:32 +0200
Subject: [PATCH] =?UTF-8?q?fix(lerna):=20don=E2=80=99t=20use=20packages=20?=
 =?UTF-8?q?if=20v7=20or=20higher=20(#23756)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 docs/development/adding-a-package-manager.md  |   2 +-
 .../__snapshots__/monorepo.spec.ts.snap       | 197 ------------------
 .../manager/npm/extract/monorepo.spec.ts      |  68 +++++-
 lib/modules/manager/npm/extract/monorepo.ts   |  23 +-
 readme.md                                     |   2 +-
 5 files changed, 87 insertions(+), 205 deletions(-)
 delete mode 100644 lib/modules/manager/npm/extract/__snapshots__/monorepo.spec.ts.snap

diff --git a/docs/development/adding-a-package-manager.md b/docs/development/adding-a-package-manager.md
index 5d3fefbfc4..790c257057 100644
--- a/docs/development/adding-a-package-manager.md
+++ b/docs/development/adding-a-package-manager.md
@@ -63,7 +63,7 @@ Normally a package manager parses or extracts all package files in _parallel_, a
 If the package manager you're adding works in _serial_, use this function instead.
 
 For example the npm and Yarn package manager must process the `package.json` and `package-lock` or `yarn.lock` files together.
-This allows features like Lerna and Workspaces to work.
+This allows features like Workspaces to work.
 This means that for npm or Yarn we need to iterate through all package files after the initial parsing.
 
 As another example, in order for Gradle to extract dependencies Renovate must first call a command via a child process.
diff --git a/lib/modules/manager/npm/extract/__snapshots__/monorepo.spec.ts.snap b/lib/modules/manager/npm/extract/__snapshots__/monorepo.spec.ts.snap
deleted file mode 100644
index 70e2789482..0000000000
--- a/lib/modules/manager/npm/extract/__snapshots__/monorepo.spec.ts.snap
+++ /dev/null
@@ -1,197 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`modules/manager/npm/extract/monorepo .extractPackageFile() updates internal packages 1`] = `
-[
-  {
-    "deps": [
-      {
-        "depName": "@org/a",
-        "isInternal": true,
-      },
-      {
-        "depName": "@org/b",
-        "isInternal": true,
-      },
-      {
-        "depName": "@org/c",
-      },
-      {
-        "depName": "foo",
-      },
-    ],
-    "managerData": {
-      "lernaJsonFile": "lerna.json",
-      "lernaPackages": [
-        "packages/*",
-      ],
-    },
-    "packageFile": "package.json",
-  },
-  {
-    "deps": [
-      {
-        "depName": "@org/b",
-        "isInternal": true,
-      },
-      {
-        "depName": "@org/c",
-      },
-      {
-        "depName": "bar",
-      },
-    ],
-    "managerData": {
-      "hasPackageManager": undefined,
-      "lernaClient": undefined,
-      "lernaJsonFile": "lerna.json",
-      "npmLock": undefined,
-      "packageJsonName": "@org/a",
-      "workspacesPackages": undefined,
-      "yarnLock": undefined,
-      "yarnZeroInstall": undefined,
-    },
-    "npmrc": undefined,
-    "packageFile": "packages/a/package.json",
-    "skipInstalls": undefined,
-  },
-  {
-    "managerData": {
-      "hasPackageManager": undefined,
-      "lernaClient": undefined,
-      "lernaJsonFile": "lerna.json",
-      "npmLock": undefined,
-      "packageJsonName": "@org/b",
-      "workspacesPackages": undefined,
-      "yarnLock": undefined,
-      "yarnZeroInstall": undefined,
-    },
-    "npmrc": undefined,
-    "packageFile": "packages/b/package.json",
-    "skipInstalls": undefined,
-  },
-]
-`;
-
-exports[`modules/manager/npm/extract/monorepo .extractPackageFile() uses lerna package settings 1`] = `
-[
-  {
-    "deps": [
-      {
-        "depName": "@org/a",
-        "isInternal": true,
-      },
-      {
-        "depName": "@org/b",
-        "isInternal": true,
-      },
-      {
-        "depName": "@org/c",
-      },
-      {
-        "depName": "foo",
-      },
-    ],
-    "managerData": {
-      "lernaJsonFile": "lerna.json",
-      "lernaPackages": [
-        "packages/*",
-      ],
-    },
-    "packageFile": "package.json",
-  },
-  {
-    "deps": [
-      {
-        "depName": "@org/b",
-        "isInternal": true,
-      },
-      {
-        "depName": "@org/c",
-      },
-      {
-        "depName": "bar",
-      },
-    ],
-    "managerData": {
-      "hasPackageManager": undefined,
-      "lernaClient": undefined,
-      "lernaJsonFile": "lerna.json",
-      "npmLock": undefined,
-      "packageJsonName": "@org/a",
-      "workspacesPackages": undefined,
-      "yarnLock": undefined,
-      "yarnZeroInstall": undefined,
-    },
-    "npmrc": undefined,
-    "packageFile": "packages/a/package.json",
-    "skipInstalls": undefined,
-  },
-  {
-    "managerData": {
-      "hasPackageManager": undefined,
-      "lernaClient": undefined,
-      "lernaJsonFile": "lerna.json",
-      "npmLock": undefined,
-      "packageJsonName": "@org/b",
-      "workspacesPackages": undefined,
-      "yarnLock": undefined,
-      "yarnZeroInstall": undefined,
-    },
-    "npmrc": undefined,
-    "packageFile": "packages/b/package.json",
-    "skipInstalls": undefined,
-  },
-]
-`;
-
-exports[`modules/manager/npm/extract/monorepo .extractPackageFile() uses yarn workspaces package settings with lerna 1`] = `
-[
-  {
-    "managerData": {
-      "lernaClient": "yarn",
-      "lernaJsonFile": "lerna.json",
-      "lernaPackages": [
-        "oldpackages/*",
-      ],
-      "workspacesPackages": [
-        "packages/*",
-      ],
-    },
-    "packageFile": "package.json",
-  },
-  {
-    "managerData": {
-      "hasPackageManager": undefined,
-      "lernaClient": "yarn",
-      "lernaJsonFile": "lerna.json",
-      "npmLock": undefined,
-      "packageJsonName": "@org/a",
-      "workspacesPackages": [
-        "packages/*",
-      ],
-      "yarnLock": undefined,
-      "yarnZeroInstall": undefined,
-    },
-    "npmrc": undefined,
-    "packageFile": "packages/a/package.json",
-    "skipInstalls": undefined,
-  },
-  {
-    "managerData": {
-      "hasPackageManager": undefined,
-      "lernaClient": "yarn",
-      "lernaJsonFile": "lerna.json",
-      "npmLock": undefined,
-      "packageJsonName": "@org/b",
-      "workspacesPackages": [
-        "packages/*",
-      ],
-      "yarnLock": undefined,
-      "yarnZeroInstall": undefined,
-    },
-    "npmrc": undefined,
-    "packageFile": "packages/b/package.json",
-    "skipInstalls": undefined,
-  },
-]
-`;
diff --git a/lib/modules/manager/npm/extract/monorepo.spec.ts b/lib/modules/manager/npm/extract/monorepo.spec.ts
index 974906f52c..ce5c9e3e46 100644
--- a/lib/modules/manager/npm/extract/monorepo.spec.ts
+++ b/lib/modules/manager/npm/extract/monorepo.spec.ts
@@ -35,7 +35,8 @@ describe('modules/manager/npm/extract/monorepo', () => {
               depName: '@org/c',
             },
             {
-              depName: 'foo',
+              depName: 'lerna',
+              currentValue: '^6.0.0',
             },
           ],
         },
@@ -60,7 +61,6 @@ describe('modules/manager/npm/extract/monorepo', () => {
         },
       ];
       await detectMonorepos(packageFiles);
-      expect(packageFiles).toMatchSnapshot();
       expect(packageFiles[1].managerData?.lernaJsonFile).toBe('lerna.json');
       expect(
         packageFiles.some((packageFile) =>
@@ -69,6 +69,59 @@ describe('modules/manager/npm/extract/monorepo', () => {
       ).toBeTrue();
     });
 
+    it('skips lerna package settings if v7 or later', async () => {
+      const packageFiles: Partial<PackageFile>[] = [
+        {
+          packageFile: 'package.json',
+          managerData: {
+            lernaJsonFile: 'lerna.json',
+            lernaPackages: ['packages/*'],
+          },
+          deps: [
+            {
+              depName: '@org/a',
+            },
+            {
+              depName: '@org/b',
+            },
+            {
+              depName: '@org/c',
+            },
+            {
+              depName: 'lerna',
+              currentValue: '^7.0.0',
+            },
+          ],
+        },
+        {
+          packageFile: 'packages/a/package.json',
+          managerData: { packageJsonName: '@org/a' },
+          deps: [
+            {
+              depName: '@org/b',
+            },
+            {
+              depName: '@org/c',
+            },
+            {
+              depName: 'bar',
+            },
+          ],
+        },
+        {
+          packageFile: 'packages/b/package.json',
+          managerData: { packageJsonName: '@org/b' },
+        },
+      ];
+      await detectMonorepos(packageFiles);
+      expect(packageFiles[1].managerData?.lernaJsonFile).toBeUndefined();
+      expect(
+        packageFiles.some((packageFile) =>
+          packageFile.deps?.some((dep) => dep.isInternal)
+        )
+      ).toBeFalse();
+    });
+
     it('updates internal packages', async () => {
       const packageFiles: Partial<PackageFile>[] = [
         {
@@ -88,7 +141,8 @@ describe('modules/manager/npm/extract/monorepo', () => {
               depName: '@org/c',
             },
             {
-              depName: 'foo',
+              depName: 'lerna',
+              currentValue: '6.1.0',
             },
           ],
         },
@@ -113,7 +167,6 @@ describe('modules/manager/npm/extract/monorepo', () => {
         },
       ];
       await detectMonorepos(packageFiles);
-      expect(packageFiles).toMatchSnapshot();
       expect(packageFiles[1].managerData?.lernaJsonFile).toBe('lerna.json');
       expect(
         packageFiles.some((packageFile) =>
@@ -132,6 +185,12 @@ describe('modules/manager/npm/extract/monorepo', () => {
             lernaPackages: ['oldpackages/*'],
             workspacesPackages: ['packages/*'],
           },
+          deps: [
+            {
+              depName: 'lerna',
+              currentValue: '^6.0.0',
+            },
+          ],
         },
         {
           packageFile: 'packages/a/package.json',
@@ -143,7 +202,6 @@ describe('modules/manager/npm/extract/monorepo', () => {
         },
       ];
       await detectMonorepos(packageFiles);
-      expect(packageFiles).toMatchSnapshot();
       expect(packageFiles[1].managerData?.lernaJsonFile).toBe('lerna.json');
     });
 
diff --git a/lib/modules/manager/npm/extract/monorepo.ts b/lib/modules/manager/npm/extract/monorepo.ts
index d296c28af8..479bf6b13f 100644
--- a/lib/modules/manager/npm/extract/monorepo.ts
+++ b/lib/modules/manager/npm/extract/monorepo.ts
@@ -1,4 +1,5 @@
 import is from '@sindresorhus/is';
+import semver from 'semver';
 import { logger } from '../../../../logger';
 import { getParentDir, getSiblingFileName } from '../../../../util/fs';
 import type { PackageFile } from '../../types';
@@ -10,7 +11,27 @@ export async function detectMonorepos(
   packageFiles: Partial<PackageFile<NpmManagerData>>[]
 ): Promise<void> {
   await detectPnpmWorkspaces(packageFiles);
-  logger.debug('Detecting Lerna and Yarn Workspaces');
+  logger.debug('Detecting workspaces');
+  // ignore lerna if using v7 or later by deleting all metadata
+  for (const p of packageFiles) {
+    if (p.managerData?.lernaJsonFile) {
+      const lernaConstraint = p.deps?.find(
+        (dep) => dep.depName === 'lerna'
+      )?.currentValue;
+      if (
+        !lernaConstraint ||
+        !semver.validRange(lernaConstraint) ||
+        semver.intersects(lernaConstraint, '>=7.0.0')
+      ) {
+        logger.debug('Deleting lerna metadata as v7 or later is in use');
+        delete p.managerData.lernaJsonFile;
+        delete p.managerData.lernaPackages;
+        delete p.managerData.lernaClient;
+      } else {
+        logger.debug('Detected lerna <7');
+      }
+    }
+  }
   for (const p of packageFiles) {
     const { packageFile, npmrc, managerData = {}, skipInstalls } = p;
     const {
diff --git a/readme.md b/readme.md
index e444b33be1..1ceaf9c124 100644
--- a/readme.md
+++ b/readme.md
@@ -20,7 +20,7 @@ Multi-platform and multi-language.
   - each week
   - each month
 - Relevant package files are discovered automatically
-- Supports monorepo architectures like Lerna or Yarn workspaces with no extra configuration
+- Supports monorepo architectures with workspaces with no extra configuration
 - Bot behavior is customizable via configuration files (config as code)
 - Use ESLint-like shared config presets for ease of use and simplifying configuration (JSON format only)
 - Lock files are supported and updated in the same commit, including immediately resolving conflicts whenever PRs are merged
-- 
GitLab