From 280e74fa9f04355b88e45cf55197dce05f19409d Mon Sep 17 00:00:00 2001
From: ctaepper <ctaepper@users.noreply.github.com>
Date: Thu, 25 Jan 2018 10:38:30 +0100
Subject: [PATCH] feat: expose env to npmrc and npm/yarn/pnpm (#1407)

Adds a config option to bot administrators called `exposeEnv`, for cases where repositories are trusted. If set to true, the bot's full `process.env` can be used for `.npmrc` variable substitution and is passed to child processes when generating lock files. Disabled by default, including in the App.
---
 .gitignore                                    |  1 +
 lib/config/definitions.js                     |  9 +++++++
 lib/manager/npm/registry.js                   | 23 +++++++++++++++++-
 lib/workers/branch/lock-files.js              | 15 +++++++++---
 lib/workers/branch/npm.js                     |  4 ++--
 lib/workers/branch/pnpm.js                    |  4 ++--
 lib/workers/branch/yarn.js                    |  4 ++--
 lib/workers/global/index.js                   |  7 ++++++
 lib/workers/package-file/index.js             |  5 +++-
 lib/workers/repository/index.js               |  1 +
 lib/workers/repository/init/apis.js           |  5 +++-
 .../npm/__snapshots__/registry.spec.js.snap   | 17 +++++++++++++
 test/manager/npm/registry.spec.js             | 24 +++++++++++++++++++
 13 files changed, 107 insertions(+), 12 deletions(-)

diff --git a/.gitignore b/.gitignore
index 65493af99a..6a4aa6a96b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@
 .cache
 /*.log
 /.vscode
+/.idea
diff --git a/lib/config/definitions.js b/lib/config/definitions.js
index deadf948b8..10a65d0233 100644
--- a/lib/config/definitions.js
+++ b/lib/config/definitions.js
@@ -128,6 +128,15 @@ const options = [
     stage: 'branch',
     type: 'boolean',
   },
+  // Bot administration
+  {
+    name: 'exposeEnv',
+    description:
+      'Enable this to expose bot process.env to repositories for npmrc substitution and package installation',
+    stage: 'global',
+    type: 'boolean',
+    default: false,
+  },
   {
     name: 'platform',
     description: 'Platform type of repository',
diff --git a/lib/manager/npm/registry.js b/lib/manager/npm/registry.js
index c18f6d1dd9..1a5aeca088 100644
--- a/lib/manager/npm/registry.js
+++ b/lib/manager/npm/registry.js
@@ -30,14 +30,35 @@ function resetCache() {
   resetMemCache();
 }
 
-function setNpmrc(input) {
+function setNpmrc(input, exposeEnv = false) {
+  logger.debug('setNpmrc()');
   if (input) {
     npmrc = ini.parse(input);
+    if (!exposeEnv) {
+      return;
+    }
+    for (const key in npmrc) {
+      if (Object.prototype.hasOwnProperty.call(npmrc, key)) {
+        npmrc[key] = envReplace(npmrc[key]);
+      }
+    }
   } else {
     npmrc = null;
   }
 }
 
+function envReplace(value, env = process.env) {
+  const ENV_EXPR = /(\\*)\$\{([^}]+)\}/g;
+
+  return value.replace(ENV_EXPR, (match, esc, envVarName) => {
+    if (env[envVarName] === undefined) {
+      logger.warn('Failed to replace env in config: ' + match);
+      throw new Error('env-replace');
+    }
+    return env[envVarName];
+  });
+}
+
 async function getDependency(name) {
   logger.trace(`getDependency(${name})`);
   if (memcache[name]) {
diff --git a/lib/workers/branch/lock-files.js b/lib/workers/branch/lock-files.js
index d913b4ba95..3bc2377563 100644
--- a/lib/workers/branch/lock-files.js
+++ b/lib/workers/branch/lock-files.js
@@ -299,11 +299,18 @@ async function getUpdatedLockFiles(config) {
   await module.exports.writeExistingFiles(config);
   await module.exports.writeUpdatedPackageFiles(config);
 
+  const env =
+    config.global && config.global.exposeEnv
+      ? process.env
+      : { PATH: process.env.PATH };
+  env.NODE_ENV = 'dev';
+
   for (const lockFileDir of dirs.packageLockFileDirs) {
     logger.debug(`Generating package-lock.json for ${lockFileDir}`);
     const lockFileName = upath.join(lockFileDir, 'package-lock.json');
     const res = await npm.generateLockFile(
-      upath.join(config.tmpDir.path, lockFileDir)
+      upath.join(config.tmpDir.path, lockFileDir),
+      env
     );
     if (res.error) {
       lockFileErrors.push({
@@ -331,7 +338,8 @@ async function getUpdatedLockFiles(config) {
     logger.debug(`Generating yarn.lock for ${lockFileDir}`);
     const lockFileName = upath.join(lockFileDir, 'yarn.lock');
     const res = await yarn.generateLockFile(
-      upath.join(config.tmpDir.path, lockFileDir)
+      upath.join(config.tmpDir.path, lockFileDir),
+      env
     );
     if (res.error) {
       lockFileErrors.push({
@@ -359,7 +367,8 @@ async function getUpdatedLockFiles(config) {
     logger.debug(`Generating shrinkwrap.yaml for ${lockFileDir}`);
     const lockFileName = upath.join(lockFileDir, 'shrinkwrap.yaml');
     const res = await pnpm.generateLockFile(
-      upath.join(config.tmpDir.path, lockFileDir)
+      upath.join(config.tmpDir.path, lockFileDir),
+      env
     );
     if (res.error) {
       lockFileErrors.push({
diff --git a/lib/workers/branch/npm.js b/lib/workers/branch/npm.js
index 4c1e41bb67..4f497cabfb 100644
--- a/lib/workers/branch/npm.js
+++ b/lib/workers/branch/npm.js
@@ -7,7 +7,7 @@ module.exports = {
   generateLockFile,
 };
 
-async function generateLockFile(tmpDir) {
+async function generateLockFile(tmpDir, env) {
   logger.debug(`Spawning npm install to create ${tmpDir}/package-lock.json`);
   let lockFile = null;
   let stdout;
@@ -58,7 +58,7 @@ async function generateLockFile(tmpDir) {
     ({ stdout, stderr } = await exec(cmd, {
       cwd: tmpDir,
       shell: true,
-      env: { NODE_ENV: 'dev', PATH: process.env.PATH },
+      env,
     }));
     logger.debug(`npm stdout:\n${stdout}`);
     logger.debug(`npm stderr:\n${stderr}`);
diff --git a/lib/workers/branch/pnpm.js b/lib/workers/branch/pnpm.js
index 1032c10c28..f843220c35 100644
--- a/lib/workers/branch/pnpm.js
+++ b/lib/workers/branch/pnpm.js
@@ -7,7 +7,7 @@ module.exports = {
   generateLockFile,
 };
 
-async function generateLockFile(tmpDir) {
+async function generateLockFile(tmpDir, env) {
   logger.debug(`Spawning pnpm install to create ${tmpDir}/shrinkwrap.yaml`);
   let lockFile = null;
   let stdout;
@@ -61,7 +61,7 @@ async function generateLockFile(tmpDir) {
     ({ stdout, stderr } = await exec(cmd, {
       cwd: tmpDir,
       shell: true,
-      env: { NODE_ENV: 'dev', PATH: process.env.PATH },
+      env,
     }));
     logger.debug(`pnpm stdout:\n${stdout}`);
     logger.debug(`pnpm stderr:\n${stderr}`);
diff --git a/lib/workers/branch/yarn.js b/lib/workers/branch/yarn.js
index e7b9f1ae80..cfa5d481c6 100644
--- a/lib/workers/branch/yarn.js
+++ b/lib/workers/branch/yarn.js
@@ -7,7 +7,7 @@ module.exports = {
   generateLockFile,
 };
 
-async function generateLockFile(tmpDir) {
+async function generateLockFile(tmpDir, env) {
   logger.debug(`Spawning yarn install to create ${tmpDir}/yarn.lock`);
   let lockFile = null;
   let stdout;
@@ -60,7 +60,7 @@ async function generateLockFile(tmpDir) {
     ({ stdout, stderr } = await exec(cmd, {
       cwd: tmpDir,
       shell: true,
-      env: { NODE_ENV: 'dev', PATH: process.env.PATH },
+      env,
     }));
     logger.debug(`yarn stdout:\n${stdout}`);
     logger.debug(`yarn stderr:\n${stderr}`);
diff --git a/lib/workers/global/index.js b/lib/workers/global/index.js
index 458cec4c39..51ff763792 100644
--- a/lib/workers/global/index.js
+++ b/lib/workers/global/index.js
@@ -25,6 +25,13 @@ async function start() {
         'No repositories found - did you want to run with flag --autodiscover?'
       );
     }
+    // Move global variables that we need to use later
+    const importGlobals = ['exposeEnv'];
+    config.global = {};
+    importGlobals.forEach(key => {
+      config.global[key] = config[key];
+      delete config[key];
+    });
     // Iterate through repositories sequentially
     for (let index = 0; index < config.repositories.length; index += 1) {
       const repoConfig = module.exports.getRepositoryConfig(config, index);
diff --git a/lib/workers/package-file/index.js b/lib/workers/package-file/index.js
index b26fb31397..88423b02c9 100644
--- a/lib/workers/package-file/index.js
+++ b/lib/workers/package-file/index.js
@@ -35,7 +35,10 @@ async function renovatePackageFile(packageFileConfig) {
   logger.debug('renovatePakageFile()');
   if (config.npmrc) {
     logger.debug('Setting .npmrc');
-    npmApi.setNpmrc(config.npmrc);
+    npmApi.setNpmrc(
+      config.npmrc,
+      config.global ? config.global.exposeEnv : false
+    );
   }
   let upgrades = [];
   logger.info(`Processing package file`);
diff --git a/lib/workers/repository/index.js b/lib/workers/repository/index.js
index 2d7f0b6319..44ab838085 100644
--- a/lib/workers/repository/index.js
+++ b/lib/workers/repository/index.js
@@ -14,6 +14,7 @@ module.exports = {
 
 async function renovateRepository(repoConfig, token, loop = 1) {
   let config = { ...repoConfig, branchList: [] };
+  config.global = config.global || {};
   logger.setMeta({ repository: config.repository });
   logger.info('Renovating repository');
   logger.trace({ config, loop }, 'renovateRepository()');
diff --git a/lib/workers/repository/init/apis.js b/lib/workers/repository/init/apis.js
index 3cf04ce584..13c74d2891 100644
--- a/lib/workers/repository/init/apis.js
+++ b/lib/workers/repository/init/apis.js
@@ -27,7 +27,10 @@ async function initApis(input, token) {
   config = await getPlatformConfig(config);
   config.npmrc = config.npmrc || (await platform.getFile('.npmrc'));
   npmApi.resetMemCache();
-  npmApi.setNpmrc(config.npmrc);
+  npmApi.setNpmrc(
+    config.npmrc,
+    config.global ? config.global.exposeEnv : false
+  );
   return config;
 }
 
diff --git a/test/manager/npm/__snapshots__/registry.spec.js.snap b/test/manager/npm/__snapshots__/registry.spec.js.snap
index 2220264b22..943cf847f4 100644
--- a/test/manager/npm/__snapshots__/registry.spec.js.snap
+++ b/test/manager/npm/__snapshots__/registry.spec.js.snap
@@ -34,6 +34,23 @@ Object {
 }
 `;
 
+exports[`api/npm should replace any environment variable in npmrc 1`] = `
+Object {
+  "dist-tags": Object {
+    "latest": "0.0.1",
+  },
+  "homepage": undefined,
+  "name": undefined,
+  "renovate-config": undefined,
+  "repositoryUrl": undefined,
+  "versions": Object {
+    "0.0.1": Object {
+      "time": "",
+    },
+  },
+}
+`;
+
 exports[`api/npm should send an authorization header if provided 1`] = `
 Object {
   "dist-tags": Object {
diff --git a/test/manager/npm/registry.spec.js b/test/manager/npm/registry.spec.js
index 498085728a..a8f61c4ec8 100644
--- a/test/manager/npm/registry.spec.js
+++ b/test/manager/npm/registry.spec.js
@@ -162,4 +162,28 @@ describe('api/npm', () => {
     const res = await npm.getDependency('foobar');
     expect(res).toMatchSnapshot();
   });
+  it('should replace any environment variable in npmrc', async () => {
+    nock('https://registry.from-env.com')
+      .get('/foobar')
+      .reply(200, npmResponse);
+    process.env.REGISTRY = 'https://registry.from-env.com';
+    /* eslint-disable */
+    npm.setNpmrc('registry=${REGISTRY}', true);
+    /* eslint-enable */
+    const res = await npm.getDependency('foobar');
+    expect(res).toMatchSnapshot();
+  });
+  it('should throw error if necessary env var is not present', () => {
+    let e;
+    try {
+      /* eslint-disable */
+      npm.setNpmrc('registry=${REGISTRY_MISSING}', true);
+      /* eslint-enable */
+    } catch (err) {
+      e = err;
+    }
+    /* eslint-disable */
+    expect(e.message).toBe('env-replace');
+    /* eslint-enable */
+  });
 });
-- 
GitLab