From d0ec341e15d5beafcf962599e0f88ee3b17460cf Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@arkins.net>
Date: Wed, 15 Aug 2018 17:13:07 +0200
Subject: [PATCH] feat: skipInstalls (#2390)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Adds new admin option “skipInstalls” that is applicable for npm-only for now (including lerna-npm). If set to false, Renovate will perform a full install of modules rather than `—package-lock-only`. This is necessary in some cases to work around bugs in npm.

Self-hosted bot users can set this option themselves on the bot’s config, but app users will require it to be enabled per-repository by the app admin.
---
 lib/config/definitions.js                     |  7 ++++++
 lib/manager/npm/post-update/index.js          |  6 +++--
 lib/manager/npm/post-update/lerna.js          | 17 +++++++++-----
 lib/manager/npm/post-update/npm.js            |  9 ++++++--
 test/workers/branch/lock-files/lerna.spec.js  | 22 ++++++++++++++++++-
 test/workers/branch/lock-files/npm.spec.js    | 22 ++++++++++++++++++-
 .../__snapshots__/flatten.spec.js.snap        |  4 ++++
 website/docs/self-hosted-configuration.md     |  4 ++++
 8 files changed, 80 insertions(+), 11 deletions(-)

diff --git a/lib/config/definitions.js b/lib/config/definitions.js
index c88fa4d196..eff277f3eb 100644
--- a/lib/config/definitions.js
+++ b/lib/config/definitions.js
@@ -237,6 +237,13 @@ const options = [
     description: 'Set to false to disable lock file updating',
     type: 'boolean',
   },
+  {
+    name: 'skipInstalls',
+    description:
+      'Skip installing modules/dependencies if lock file updating is possible alone',
+    type: 'boolean',
+    admin: true,
+  },
   {
     name: 'ignoreNpmrcFile',
     description: 'Whether to ignore any .npmrc file found in repository',
diff --git a/lib/manager/npm/post-update/index.js b/lib/manager/npm/post-update/index.js
index 856e45e160..9c17294e0e 100644
--- a/lib/manager/npm/post-update/index.js
+++ b/lib/manager/npm/post-update/index.js
@@ -306,7 +306,8 @@ async function getAdditionalFiles(config, packageFiles) {
     const res = await npm.generateLockFile(
       upath.join(config.tmpDir.path, lockFileDir),
       env,
-      fileName
+      fileName,
+      config.skipInstalls
     );
     if (res.error) {
       // istanbul ignore if
@@ -472,7 +473,8 @@ async function getAdditionalFiles(config, packageFiles) {
     const res = await lerna.generateLockFiles(
       lernaPackageFile.lernaClient,
       upath.join(config.tmpDir.path, lernaDir),
-      env
+      env,
+      config.skipInstalls
     );
     // istanbul ignore else
     if (res.error) {
diff --git a/lib/manager/npm/post-update/lerna.js b/lib/manager/npm/post-update/lerna.js
index 4b4da66dca..04a17b616a 100644
--- a/lib/manager/npm/post-update/lerna.js
+++ b/lib/manager/npm/post-update/lerna.js
@@ -4,7 +4,7 @@ module.exports = {
   generateLockFiles,
 };
 
-async function generateLockFiles(lernaClient, tmpDir, env) {
+async function generateLockFiles(lernaClient, tmpDir, env, skipInstalls) {
   if (!lernaClient) {
     logger.warn('No lernaClient specified - returning');
     return { error: false };
@@ -26,10 +26,17 @@ async function generateLockFiles(lernaClient, tmpDir, env) {
     }
     lernaVersion = lernaVersion || 'latest';
     logger.debug('Using lerna version ' + lernaVersion);
-    const params =
-      lernaClient === 'npm'
-        ? '--package-lock-only --no-audit'
-        : '--ignore-scripts --ignore-engines --ignore-platform --mutex network:31879';
+    let params;
+    if (lernaClient === 'npm') {
+      if (skipInstalls) {
+        params = '--package-lock-only --no-audit';
+      } else {
+        params = '--no-audit';
+      }
+    } else {
+      params =
+        '--ignore-scripts --ignore-engines --ignore-platform --mutex network:31879';
+    }
     cmd = `npm i -g -C ~/.npm/lerna@${lernaVersion} lerna@${lernaVersion} && ${lernaClient} install ${params} && ~/.npm/lerna@${lernaVersion}/bin/lerna bootstrap -- ${params}`;
     logger.debug({ cmd });
     // TODO: Switch to native util.promisify once using only node 8
diff --git a/lib/manager/npm/post-update/npm.js b/lib/manager/npm/post-update/npm.js
index 62f7d9f58e..6a90bd0c7f 100644
--- a/lib/manager/npm/post-update/npm.js
+++ b/lib/manager/npm/post-update/npm.js
@@ -7,7 +7,7 @@ module.exports = {
   generateLockFile,
 };
 
-async function generateLockFile(tmpDir, env, filename) {
+async function generateLockFile(tmpDir, env, filename, skipInstalls) {
   logger.debug(`Spawning npm install to create ${tmpDir}/${filename}`);
   let lockFile = null;
   let stdout;
@@ -52,7 +52,12 @@ async function generateLockFile(tmpDir, env, filename) {
         }
       }
     }
-    cmd = `${cmd} --version && ${cmd} install --package-lock-only --no-audit`;
+    cmd = `${cmd} --version && ${cmd} install`;
+    if (skipInstalls) {
+      cmd += ' --package-lock-only --no-audit';
+    } else {
+      cmd += ' --no-audit';
+    }
     logger.debug(`Using npm: ${cmd}`);
     // TODO: Switch to native util.promisify once using only node 8
     ({ stdout, stderr } = await exec(cmd, {
diff --git a/test/workers/branch/lock-files/lerna.spec.js b/test/workers/branch/lock-files/lerna.spec.js
index 0d00f5f89f..25f6700a25 100644
--- a/test/workers/branch/lock-files/lerna.spec.js
+++ b/test/workers/branch/lock-files/lerna.spec.js
@@ -13,7 +13,27 @@ describe('generateLockFiles()', () => {
       JSON.stringify({ dependencies: { lerna: '2.0.0' } })
     );
     exec.mockReturnValueOnce({});
-    const res = await lernaHelper.generateLockFiles('npm', 'some-dir', {});
+    const skipInstalls = true;
+    const res = await lernaHelper.generateLockFiles(
+      'npm',
+      'some-dir',
+      {},
+      skipInstalls
+    );
+    expect(res.error).toBe(false);
+  });
+  it('performs full npm install', async () => {
+    platform.getFile.mockReturnValueOnce(
+      JSON.stringify({ dependencies: { lerna: '2.0.0' } })
+    );
+    exec.mockReturnValueOnce({});
+    const skipInstalls = false;
+    const res = await lernaHelper.generateLockFiles(
+      'npm',
+      'some-dir',
+      {},
+      skipInstalls
+    );
     expect(res.error).toBe(false);
   });
   it('generates yarn.lock files', async () => {
diff --git a/test/workers/branch/lock-files/npm.spec.js b/test/workers/branch/lock-files/npm.spec.js
index 3b405bf144..3e6a131342 100644
--- a/test/workers/branch/lock-files/npm.spec.js
+++ b/test/workers/branch/lock-files/npm.spec.js
@@ -18,10 +18,30 @@ describe('generateLockFile', () => {
       stderror: '',
     });
     fs.readFile = jest.fn(() => 'package-lock-contents');
+    const skipInstalls = true;
     const res = await npmHelper.generateLockFile(
       'some-dir',
       {},
-      'package-lock.json'
+      'package-lock.json',
+      skipInstalls
+    );
+    expect(fs.readFile.mock.calls.length).toEqual(1);
+    expect(res.error).not.toBeDefined();
+    expect(res.lockFile).toEqual('package-lock-contents');
+  });
+  it('performs full install', async () => {
+    getInstalledPath.mockReturnValueOnce('node_modules/npm');
+    exec.mockReturnValueOnce({
+      stdout: '',
+      stderror: '',
+    });
+    fs.readFile = jest.fn(() => 'package-lock-contents');
+    const skipInstalls = false;
+    const res = await npmHelper.generateLockFile(
+      'some-dir',
+      {},
+      'package-lock.json',
+      skipInstalls
     );
     expect(fs.readFile.mock.calls.length).toEqual(1);
     expect(res.error).not.toBeDefined();
diff --git a/test/workers/repository/updates/__snapshots__/flatten.spec.js.snap b/test/workers/repository/updates/__snapshots__/flatten.spec.js.snap
index 6efd5cdd19..ecfaecaac9 100644
--- a/test/workers/repository/updates/__snapshots__/flatten.spec.js.snap
+++ b/test/workers/repository/updates/__snapshots__/flatten.spec.js.snap
@@ -56,6 +56,7 @@ Array [
     "semanticCommitScope": "deps",
     "semanticCommitType": "chore",
     "semanticCommits": null,
+    "skipInstalls": true,
     "statusCheckVerify": false,
     "timezone": null,
     "unpublishSafe": false,
@@ -123,6 +124,7 @@ Array [
     "semanticCommitScope": "deps",
     "semanticCommitType": "chore",
     "semanticCommits": null,
+    "skipInstalls": true,
     "statusCheckVerify": false,
     "timezone": null,
     "unpublishSafe": false,
@@ -203,6 +205,7 @@ Array [
     "semanticCommitScope": "deps",
     "semanticCommitType": "chore",
     "semanticCommits": null,
+    "skipInstalls": true,
     "statusCheckVerify": false,
     "timezone": null,
     "unpublishSafe": false,
@@ -272,6 +275,7 @@ Array [
     "semanticCommitScope": "deps",
     "semanticCommitType": "chore",
     "semanticCommits": null,
+    "skipInstalls": true,
     "statusCheckVerify": false,
     "timezone": null,
     "unpublishSafe": false,
diff --git a/website/docs/self-hosted-configuration.md b/website/docs/self-hosted-configuration.md
index 263bb33251..8a49f03ba5 100644
--- a/website/docs/self-hosted-configuration.md
+++ b/website/docs/self-hosted-configuration.md
@@ -61,4 +61,8 @@ Set this to `false` if (a) you configure Renovate entirely on the bot side (i.e.
 
 ## requireConfig
 
+## skipInstalls
+
+By default, Renovate will use the most efficient approach to updating package files and lock files, which in most cases skips the need to perform a full module install by the bot. If this is set to false, then a full install of modules will be done. This is currently applicable to `npm` and `lerna`/`npm` only, and only used in cases where bugs in `npm` result in incorrect lock files being updated.
+
 ## token
-- 
GitLab