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