From 6516cc1d5089f533be26b5092b70e13c444f7585 Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@arkins.net>
Date: Tue, 2 Jun 2020 14:41:03 +0200
Subject: [PATCH] feat(lerna): use docker exec (#6407)

Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
---
 .../__snapshots__/lerna.spec.ts.snap          | 78 ++++++++++++++++
 .../npm/post-update}/lerna.spec.ts            | 41 ++++++++-
 lib/manager/npm/post-update/lerna.ts          | 91 ++++++++++++-------
 lib/manager/npm/post-update/yarn.ts           |  7 +-
 4 files changed, 174 insertions(+), 43 deletions(-)
 rename lib/{workers/branch/lock-files => manager/npm/post-update}/__snapshots__/lerna.spec.ts.snap (67%)
 rename lib/{workers/branch/lock-files => manager/npm/post-update}/lerna.spec.ts (66%)

diff --git a/lib/workers/branch/lock-files/__snapshots__/lerna.spec.ts.snap b/lib/manager/npm/post-update/__snapshots__/lerna.spec.ts.snap
similarity index 67%
rename from lib/workers/branch/lock-files/__snapshots__/lerna.spec.ts.snap
rename to lib/manager/npm/post-update/__snapshots__/lerna.spec.ts.snap
index 4c64806349..f93b73cf05 100644
--- a/lib/workers/branch/lock-files/__snapshots__/lerna.spec.ts.snap
+++ b/lib/manager/npm/post-update/__snapshots__/lerna.spec.ts.snap
@@ -1,5 +1,44 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`generateLockFiles() allows scripts for trust level high 1`] = `
+Array [
+  Object {
+    "cmd": "npm install --package-lock-only --no-audit",
+    "options": Object {
+      "cwd": "some-dir",
+      "encoding": "utf-8",
+      "env": Object {
+        "HOME": "/home/user",
+        "HTTPS_PROXY": "https://example.com",
+        "HTTP_PROXY": "http://example.com",
+        "LANG": "en_US.UTF-8",
+        "LC_ALL": "en_US",
+        "NO_PROXY": "localhost",
+        "PATH": "/tmp/path",
+      },
+      "timeout": 900000,
+    },
+  },
+  Object {
+    "cmd": "npx lerna@latest bootstrap --no-ci -- --package-lock-only --no-audit",
+    "options": Object {
+      "cwd": "some-dir",
+      "encoding": "utf-8",
+      "env": Object {
+        "HOME": "/home/user",
+        "HTTPS_PROXY": "https://example.com",
+        "HTTP_PROXY": "http://example.com",
+        "LANG": "en_US.UTF-8",
+        "LC_ALL": "en_US",
+        "NO_PROXY": "localhost",
+        "PATH": "/tmp/path",
+      },
+      "timeout": 900000,
+    },
+  },
+]
+`;
+
 exports[`generateLockFiles() defaults to latest 1`] = `
 Array [
   Object {
@@ -117,6 +156,45 @@ Array [
 ]
 `;
 
+exports[`generateLockFiles() maps dot files 1`] = `
+Array [
+  Object {
+    "cmd": "npm install --package-lock-only --no-audit",
+    "options": Object {
+      "cwd": "some-dir",
+      "encoding": "utf-8",
+      "env": Object {
+        "HOME": "/home/user",
+        "HTTPS_PROXY": "https://example.com",
+        "HTTP_PROXY": "http://example.com",
+        "LANG": "en_US.UTF-8",
+        "LC_ALL": "en_US",
+        "NO_PROXY": "localhost",
+        "PATH": "/tmp/path",
+      },
+      "timeout": 900000,
+    },
+  },
+  Object {
+    "cmd": "npx lerna@latest bootstrap --no-ci -- --package-lock-only --no-audit",
+    "options": Object {
+      "cwd": "some-dir",
+      "encoding": "utf-8",
+      "env": Object {
+        "HOME": "/home/user",
+        "HTTPS_PROXY": "https://example.com",
+        "HTTP_PROXY": "http://example.com",
+        "LANG": "en_US.UTF-8",
+        "LC_ALL": "en_US",
+        "NO_PROXY": "localhost",
+        "PATH": "/tmp/path",
+      },
+      "timeout": 900000,
+    },
+  },
+]
+`;
+
 exports[`generateLockFiles() performs full npm install 1`] = `
 Array [
   Object {
diff --git a/lib/workers/branch/lock-files/lerna.spec.ts b/lib/manager/npm/post-update/lerna.spec.ts
similarity index 66%
rename from lib/workers/branch/lock-files/lerna.spec.ts
rename to lib/manager/npm/post-update/lerna.spec.ts
index 4bd78fb957..9438d7ae4a 100644
--- a/lib/workers/branch/lock-files/lerna.spec.ts
+++ b/lib/manager/npm/post-update/lerna.spec.ts
@@ -1,12 +1,13 @@
 import { exec as _exec } from 'child_process';
 import { envMock, mockExecAll } from '../../../../test/execUtil';
 import { mocked } from '../../../../test/util';
-import * as _lernaHelper from '../../../manager/npm/post-update/lerna';
 import { platform as _platform } from '../../../platform';
 import * as _env from '../../../util/exec/env';
+import * as _lernaHelper from './lerna';
 
 jest.mock('child_process');
 jest.mock('../../../util/exec/env');
+jest.mock('../../../manager/npm/post-update/node-version');
 
 const exec: jest.Mock<typeof _exec> = _exec as any;
 const env = mocked(_env);
@@ -20,7 +21,16 @@ describe('generateLockFiles()', () => {
     env.getChildProcessEnv.mockReturnValue(envMock.basic);
   });
   it('returns if no lernaClient', async () => {
-    const res = await lernaHelper.generateLockFiles(undefined, 'some-dir', {});
+    const res = await lernaHelper.generateLockFiles(
+      undefined,
+      'some-dir',
+      {},
+      {}
+    );
+    expect(res.error).toBe(false);
+  });
+  it('returns if invalid lernaClient', async () => {
+    const res = await lernaHelper.generateLockFiles('foo', 'some-dir', {}, {});
     expect(res.error).toBe(false);
   });
   it('generates package-lock.json files', async () => {
@@ -60,14 +70,35 @@ describe('generateLockFiles()', () => {
       JSON.stringify({ devDependencies: { lerna: '2.0.0' } })
     );
     const execSnapshots = mockExecAll(exec);
-    const res = await lernaHelper.generateLockFiles('yarn', 'some-dir', {});
-    expect(res.error).toBe(false);
+    const res = await lernaHelper.generateLockFiles('yarn', 'some-dir', {}, {});
     expect(execSnapshots).toMatchSnapshot();
+    expect(res.error).toBe(false);
   });
   it('defaults to latest', async () => {
     platform.getFile.mockReturnValueOnce(undefined);
     const execSnapshots = mockExecAll(exec);
-    const res = await lernaHelper.generateLockFiles('npm', 'some-dir', {});
+    const res = await lernaHelper.generateLockFiles('npm', 'some-dir', {}, {});
+    expect(res.error).toBe(false);
+    expect(execSnapshots).toMatchSnapshot();
+  });
+  it('maps dot files', async () => {
+    platform.getFile.mockReturnValueOnce(undefined);
+    const execSnapshots = mockExecAll(exec);
+    const res = await lernaHelper.generateLockFiles(
+      'npm',
+      'some-dir',
+      { dockerMapDotfiles: true },
+      {}
+    );
+    expect(res.error).toBe(false);
+    expect(execSnapshots).toMatchSnapshot();
+  });
+  it('allows scripts for trust level high', async () => {
+    platform.getFile.mockReturnValueOnce(undefined);
+    const execSnapshots = mockExecAll(exec);
+    global.trustLevel = 'high';
+    const res = await lernaHelper.generateLockFiles('npm', 'some-dir', {}, {});
+    delete global.trustLevel;
     expect(res.error).toBe(false);
     expect(execSnapshots).toMatchSnapshot();
   });
diff --git a/lib/manager/npm/post-update/lerna.ts b/lib/manager/npm/post-update/lerna.ts
index ef5cd9bd4a..d449ac198f 100644
--- a/lib/manager/npm/post-update/lerna.ts
+++ b/lib/manager/npm/post-update/lerna.ts
@@ -1,8 +1,12 @@
+import semver from 'semver';
 import { quote } from 'shlex';
+import { join } from 'upath';
 import { logger } from '../../../logger';
 import { platform } from '../../../platform';
-import { exec } from '../../../util/exec';
+import { ExecOptions, exec } from '../../../util/exec';
 import { PostUpdateConfig } from '../../common';
+import { getNodeConstraint } from './node-version';
+import { optimizeCommand } from './yarn';
 
 export interface GenerateLockFileResult {
   error?: boolean;
@@ -13,7 +17,7 @@ export async function generateLockFiles(
   lernaClient: string,
   cwd: string,
   config: PostUpdateConfig,
-  env?: NodeJS.ProcessEnv,
+  env: NodeJS.ProcessEnv,
   skipInstalls?: boolean
 ): Promise<GenerateLockFileResult> {
   if (!lernaClient) {
@@ -21,11 +25,51 @@ export async function generateLockFiles(
     return { error: false };
   }
   logger.debug(`Spawning lerna with ${lernaClient} to create lock files`);
-  const cmd: string[] = [];
-  // const envVars = ['NPM_CONFIG_CACHE', 'npm_config_store'];
+  const preCommands = [];
+  const cmd = [];
+  let cmdOptions = '';
   try {
+    if (lernaClient === 'yarn') {
+      preCommands.push('npm i -g yarn');
+      if (skipInstalls !== false) {
+        preCommands.push(optimizeCommand);
+      }
+      cmdOptions = '--ignore-scripts --ignore-engines --ignore-platform';
+    } else if (lernaClient === 'npm') {
+      if (skipInstalls === false) {
+        cmdOptions = '--ignore-scripts  --no-audit';
+      } else {
+        cmdOptions = '--package-lock-only --no-audit';
+      }
+    } else {
+      logger.warn({ lernaClient }, 'Unknown lernaClient');
+      return { error: false };
+    }
+    if (global.trustLevel === 'high' && config.ignoreScripts !== false) {
+      cmdOptions = cmdOptions.replace('--ignore-scripts ', '');
+    }
+    const tagConstraint = await getNodeConstraint(config.packageFile);
+    const execOptions: ExecOptions = {
+      cwd,
+      extraEnv: {
+        NPM_CONFIG_CACHE: env.NPM_CONFIG_CACHE,
+        npm_config_store: env.npm_config_store,
+      },
+      docker: {
+        image: 'renovate/node',
+        tagScheme: 'npm',
+        tagConstraint,
+        preCommands,
+      },
+    };
+    if (config.dockerMapDotfiles) {
+      const homeDir =
+        process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
+      const homeNpmrc = join(homeDir, '.npmrc');
+      execOptions.docker.volumes = [[homeNpmrc, '/home/ubuntu/.npmrc']];
+    }
+    cmd.push(`${lernaClient} install ${cmdOptions}`);
     let lernaVersion: string;
-    // const volumes: VolumeOption[] = [];
     try {
       const pJson = JSON.parse(await platform.getFile('package.json'));
       lernaVersion =
@@ -34,37 +78,14 @@ export async function generateLockFiles(
     } catch (err) {
       logger.warn('Could not detect lerna version in package.json');
     }
-    lernaVersion = lernaVersion || 'latest';
-    logger.debug('Using lerna version ' + lernaVersion);
-    let params: string;
-    if (lernaClient === 'npm') {
-      if (skipInstalls === false) {
-        params = '--ignore-scripts  --no-audit';
-      } else {
-        params = '--package-lock-only --no-audit';
-      }
-    } else {
-      params = '--ignore-scripts --ignore-engines --ignore-platform';
+    if (!lernaVersion || !semver.validRange(lernaVersion)) {
+      lernaVersion = 'latest';
     }
-
-    // // istanbul ignore if
-    // if (config.dockerMapDotfiles) {
-    //   const homeDir =
-    //     process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
-    //   const homeNpmrc = join(homeDir, '.npmrc');
-    //   volumes.push([homeNpmrc, `/home/ubuntu/.npmrc`]);
-    // }
-    cmd.push(`${lernaClient} install ${params}`);
-    cmd.push(`npx lerna@${quote(lernaVersion)} bootstrap --no-ci -- ${params}`);
-    await exec(cmd, {
-      cwd,
-      env,
-      // docker: {
-      //   image: `renovate/${lernaClient}`,
-      //   volumes,
-      //   envVars,
-      // },
-    });
+    logger.debug('Using lerna version ' + lernaVersion);
+    cmd.push(
+      `npx lerna@${quote(lernaVersion)} bootstrap --no-ci -- ${cmdOptions}`
+    );
+    await exec(cmd, execOptions);
   } catch (err) /* istanbul ignore next */ {
     logger.debug(
       {
diff --git a/lib/manager/npm/post-update/yarn.ts b/lib/manager/npm/post-update/yarn.ts
index 8a5c71a0ea..44f19d48b1 100644
--- a/lib/manager/npm/post-update/yarn.ts
+++ b/lib/manager/npm/post-update/yarn.ts
@@ -31,6 +31,9 @@ export async function hasYarnOfflineMirror(cwd: string): Promise<boolean> {
   return false;
 }
 
+export const optimizeCommand =
+  "sed -i 's/ steps,/ steps.slice(0,1),/' /home/ubuntu/.npm-global/lib/node_modules/yarn/lib/cli.js";
+
 export async function generateLockFile(
   cwd: string,
   env: NodeJS.ProcessEnv,
@@ -47,9 +50,7 @@ export async function generateLockFile(
     ) {
       logger.debug('Updating yarn.lock only - skipping node_modules');
       // The following change causes Yarn 1.x to exit gracefully after updating the lock file but without installing node_modules
-      preCommands.push(
-        "sed -i 's/ steps,/ steps.slice(0,1),/' /home/ubuntu/.npm-global/lib/node_modules/yarn/lib/cli.js"
-      );
+      preCommands.push(optimizeCommand);
     }
     const commands = [];
     let cmdOptions = '--network-timeout 100000';
-- 
GitLab