From dde5756488084fdd1de7bc95f9e4a7bcad0f9e15 Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@arkins.net>
Date: Sun, 4 Mar 2018 15:52:06 +0100
Subject: [PATCH] feat: .nvmrc files (#1582)

Adds initial support for `.nvmrc` renovation. If the existing value is a fully-specified semver version then it will be upgraded, otherwise left alone.

Closes #1270
---
 lib/config/definitions.js                     |  7 ++++++
 lib/manager/index.js                          |  3 ++-
 lib/manager/nvm/detect.js                     | 13 +++++++++++
 lib/manager/nvm/extract.js                    | 13 +++++++++++
 lib/manager/nvm/index.js                      | 13 +++++++++++
 lib/manager/nvm/package.js                    | 10 +++++++++
 lib/manager/nvm/resolve.js                    | 19 ++++++++++++++++
 lib/manager/nvm/update.js                     |  8 +++++++
 lib/workers/pr/index.js                       |  6 ++++-
 test/manager/__snapshots__/index.spec.js.snap |  6 +++++
 .../__snapshots__/resolve.spec.js.snap        | 16 ++++++++++----
 test/manager/index.spec.js                    |  7 ++++++
 .../nvm/__snapshots__/extract.spec.js.snap    | 11 ++++++++++
 .../nvm/__snapshots__/package.spec.js.snap    |  3 +++
 .../nvm/__snapshots__/update.spec.js.snap     |  6 +++++
 test/manager/nvm/extract.spec.js              | 10 +++++++++
 test/manager/nvm/package.spec.js              | 22 +++++++++++++++++++
 test/manager/nvm/update.spec.js               | 13 +++++++++++
 test/manager/resolve.spec.js                  |  6 +++--
 .../2017-10-05-configuration-options.md       | 11 ++++++++++
 20 files changed, 195 insertions(+), 8 deletions(-)
 create mode 100644 lib/manager/nvm/detect.js
 create mode 100644 lib/manager/nvm/extract.js
 create mode 100644 lib/manager/nvm/index.js
 create mode 100644 lib/manager/nvm/package.js
 create mode 100644 lib/manager/nvm/resolve.js
 create mode 100644 lib/manager/nvm/update.js
 create mode 100644 test/manager/nvm/__snapshots__/extract.spec.js.snap
 create mode 100644 test/manager/nvm/__snapshots__/package.spec.js.snap
 create mode 100644 test/manager/nvm/__snapshots__/update.spec.js.snap
 create mode 100644 test/manager/nvm/extract.spec.js
 create mode 100644 test/manager/nvm/package.spec.js
 create mode 100644 test/manager/nvm/update.spec.js

diff --git a/lib/config/definitions.js b/lib/config/definitions.js
index 06509ca99e..02ff369a40 100644
--- a/lib/config/definitions.js
+++ b/lib/config/definitions.js
@@ -780,6 +780,13 @@ const options = [
     default: { enabled: false },
     mergeable: true,
   },
+  {
+    name: 'nvm',
+    description: 'Configuration object for .nvmrc files',
+    state: 'repository',
+    type: 'json',
+    default: { enabled: true },
+  },
   {
     name: 'docker',
     description: 'Configuration object for Dockerfile renovation',
diff --git a/lib/manager/index.js b/lib/manager/index.js
index 9ec78db90b..c1c1a6ef11 100644
--- a/lib/manager/index.js
+++ b/lib/manager/index.js
@@ -3,7 +3,7 @@ const { mergeChildConfig } = require('../config');
 const { checkMonorepos } = require('../manager/npm/monorepos');
 
 const managers = {};
-const managerList = ['bazel', 'docker', 'meteor', 'npm', 'travis'];
+const managerList = ['bazel', 'docker', 'meteor', 'npm', 'nvm', 'travis'];
 for (const manager of managerList) {
   // eslint-disable-next-line global-require,import/no-dynamic-require
   managers[manager] = require(`./${manager}`);
@@ -112,6 +112,7 @@ async function resolvePackageFiles(config) {
       : await detectPackageFiles(config);
   logger.debug({ allPackageFiles }, 'allPackageFiles');
   const managerFileMappings = {
+    '.nvmrc': 'nvm',
     '.travis.yml': 'travis',
     Dockerfile: 'docker',
     WORKSPACE: 'bazel',
diff --git a/lib/manager/nvm/detect.js b/lib/manager/nvm/detect.js
new file mode 100644
index 0000000000..922bc40d18
--- /dev/null
+++ b/lib/manager/nvm/detect.js
@@ -0,0 +1,13 @@
+module.exports = {
+  detectPackageFiles,
+};
+
+function detectPackageFiles(config, fileList) {
+  logger.debug('nvm.detectPackageFiles()');
+  if (config.nvm.enabled) {
+    if (fileList.includes('.nvmrc')) {
+      return ['.nvmrc'];
+    }
+  }
+  return [];
+}
diff --git a/lib/manager/nvm/extract.js b/lib/manager/nvm/extract.js
new file mode 100644
index 0000000000..9d21119433
--- /dev/null
+++ b/lib/manager/nvm/extract.js
@@ -0,0 +1,13 @@
+module.exports = {
+  extractDependencies,
+};
+
+function extractDependencies(packageContent) {
+  return [
+    {
+      depName: 'node',
+      depType: '.nvmrc',
+      currentVersion: packageContent.trim(),
+    },
+  ];
+}
diff --git a/lib/manager/nvm/index.js b/lib/manager/nvm/index.js
new file mode 100644
index 0000000000..6b0c9b22a0
--- /dev/null
+++ b/lib/manager/nvm/index.js
@@ -0,0 +1,13 @@
+const { detectPackageFiles } = require('./detect');
+const { extractDependencies } = require('./extract');
+const { getPackageUpdates } = require('./package');
+const { resolvePackageFile } = require('./resolve');
+const { setNewValue } = require('./update');
+
+module.exports = {
+  detectPackageFiles,
+  extractDependencies,
+  getPackageUpdates,
+  resolvePackageFile,
+  setNewValue,
+};
diff --git a/lib/manager/nvm/package.js b/lib/manager/nvm/package.js
new file mode 100644
index 0000000000..75a8b35efd
--- /dev/null
+++ b/lib/manager/nvm/package.js
@@ -0,0 +1,10 @@
+const nodeManager = require('../_helpers/node/package');
+
+module.exports = {
+  getPackageUpdates,
+};
+
+function getPackageUpdates(config) {
+  logger.debug('nvm.getPackageUpdates()');
+  return nodeManager.getPackageUpdates(config);
+}
diff --git a/lib/manager/nvm/resolve.js b/lib/manager/nvm/resolve.js
new file mode 100644
index 0000000000..f39498a03f
--- /dev/null
+++ b/lib/manager/nvm/resolve.js
@@ -0,0 +1,19 @@
+const { mergeChildConfig } = require('../../config');
+
+module.exports = {
+  resolvePackageFile,
+};
+
+async function resolvePackageFile(config, inputFile) {
+  const nvmConfig = mergeChildConfig(config.node, config.nvm);
+  const packageFile = mergeChildConfig(nvmConfig, inputFile);
+  logger.debug(
+    `Resolving packageFile ${JSON.stringify(packageFile.packageFile)}`
+  );
+  packageFile.content = await platform.getFile(packageFile.packageFile);
+  if (!packageFile.content) {
+    logger.debug('No packageFile content');
+    return null;
+  }
+  return packageFile;
+}
diff --git a/lib/manager/nvm/update.js b/lib/manager/nvm/update.js
new file mode 100644
index 0000000000..3d371c5a8c
--- /dev/null
+++ b/lib/manager/nvm/update.js
@@ -0,0 +1,8 @@
+module.exports = {
+  setNewValue,
+};
+
+function setNewValue(currentFileContent, upgrade) {
+  logger.debug(`nvm.setNewValue: ${upgrade.newVersions}`);
+  return `${upgrade.newVersion}\n`;
+}
diff --git a/lib/workers/pr/index.js b/lib/workers/pr/index.js
index c5b945846a..88d8a12b47 100644
--- a/lib/workers/pr/index.js
+++ b/lib/workers/pr/index.js
@@ -111,7 +111,11 @@ async function ensurePr(prConfig) {
     processedUpgrades.push(upgradeKey);
 
     let logJSON;
-    if (upgrade.depType !== 'engines') {
+    if (
+      upgrade.manager !== 'travis' &&
+      upgrade.manager !== 'nvm' &&
+      upgrade.depType !== 'engines'
+    ) {
       logJSON = await changelogHelper.getChangeLogJSON(
         upgrade.depName,
         upgrade.changeLogFromVersion,
diff --git a/test/manager/__snapshots__/index.spec.js.snap b/test/manager/__snapshots__/index.spec.js.snap
index cf561e12e6..91c455b749 100644
--- a/test/manager/__snapshots__/index.spec.js.snap
+++ b/test/manager/__snapshots__/index.spec.js.snap
@@ -7,6 +7,12 @@ Array [
 ]
 `;
 
+exports[`manager detectPackageFiles(config) finds .nvmrc files 1`] = `
+Array [
+  ".nvmrc",
+]
+`;
+
 exports[`manager detectPackageFiles(config) finds .travis.yml files 1`] = `
 Array [
   ".travis.yml",
diff --git a/test/manager/__snapshots__/resolve.spec.js.snap b/test/manager/__snapshots__/resolve.spec.js.snap
index 6f661ff19a..643d46f1b7 100644
--- a/test/manager/__snapshots__/resolve.spec.js.snap
+++ b/test/manager/__snapshots__/resolve.spec.js.snap
@@ -42,7 +42,7 @@ Array [
 ]
 `;
 
-exports[`manager/resolve resolvePackageFiles() detects meteor and docker and travis and bazel 1`] = `
+exports[`manager/resolve resolvePackageFiles() detects meteor and docker and travis and bazel and nvm 1`] = `
 Array [
   Object {
     "content": "{}",
@@ -270,8 +270,7 @@ This PR has been generated by [Renovate Bot](https://renovateapp.com).",
     "prTitle": "Update {{{depName}}} Dockerfile tag to {{#if isMajor}}v{{{newVersionMajor}}}{{else}}v{{{newTag}}}{{/if}}",
   },
   Object {
-    "content": "hello: world
-",
+    "content": "# travis",
     "enabled": false,
     "groupName": "node.js",
     "lazyGrouping": false,
@@ -279,11 +278,20 @@ This PR has been generated by [Renovate Bot](https://renovateapp.com).",
     "packageFile": ".travis.yml",
   },
   Object {
-    "content": "# travis",
+    "content": "# WORKSPACE",
     "enabled": true,
     "manager": "bazel",
     "packageFile": "WORKSPACE",
   },
+  Object {
+    "content": "8.9
+",
+    "enabled": true,
+    "groupName": "node.js",
+    "lazyGrouping": false,
+    "manager": "nvm",
+    "packageFile": ".nvmrc",
+  },
 ]
 `;
 
diff --git a/test/manager/index.spec.js b/test/manager/index.spec.js
index e6f5687d20..cc56dc3f95 100644
--- a/test/manager/index.spec.js
+++ b/test/manager/index.spec.js
@@ -78,6 +78,13 @@ describe('manager', () => {
       expect(res).toMatchSnapshot();
       expect(res).toHaveLength(1);
     });
+    it('finds .nvmrc files', async () => {
+      config.travis.enabled = true;
+      platform.getFileList.mockReturnValueOnce(['.nvmrc', 'other/.nvmrc']);
+      const res = await manager.detectPackageFiles(config);
+      expect(res).toMatchSnapshot();
+      expect(res).toHaveLength(1);
+    });
     it('finds WORKSPACE files', async () => {
       config.bazel.enabled = true;
       platform.getFileList.mockReturnValueOnce([
diff --git a/test/manager/nvm/__snapshots__/extract.spec.js.snap b/test/manager/nvm/__snapshots__/extract.spec.js.snap
new file mode 100644
index 0000000000..1b783d2a47
--- /dev/null
+++ b/test/manager/nvm/__snapshots__/extract.spec.js.snap
@@ -0,0 +1,11 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`lib/manager/nvm/extract extractDependencies() returns a result 1`] = `
+Array [
+  Object {
+    "currentVersion": "8.4.0",
+    "depName": "node",
+    "depType": ".nvmrc",
+  },
+]
+`;
diff --git a/test/manager/nvm/__snapshots__/package.spec.js.snap b/test/manager/nvm/__snapshots__/package.spec.js.snap
new file mode 100644
index 0000000000..acf0751f45
--- /dev/null
+++ b/test/manager/nvm/__snapshots__/package.spec.js.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`lib/workers/package/node getPackageUpdates returns result if needing updates 1`] = `Array []`;
diff --git a/test/manager/nvm/__snapshots__/update.spec.js.snap b/test/manager/nvm/__snapshots__/update.spec.js.snap
new file mode 100644
index 0000000000..e938db537a
--- /dev/null
+++ b/test/manager/nvm/__snapshots__/update.spec.js.snap
@@ -0,0 +1,6 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`manager/nvm/update setNewValue updates values 1`] = `
+"8.9.1
+"
+`;
diff --git a/test/manager/nvm/extract.spec.js b/test/manager/nvm/extract.spec.js
new file mode 100644
index 0000000000..d13e4d2d45
--- /dev/null
+++ b/test/manager/nvm/extract.spec.js
@@ -0,0 +1,10 @@
+const { extractDependencies } = require('../../../lib/manager/nvm/extract');
+
+describe('lib/manager/nvm/extract', () => {
+  describe('extractDependencies()', () => {
+    it('returns a result', () => {
+      const res = extractDependencies('8.4.0\n');
+      expect(res).toMatchSnapshot();
+    });
+  });
+});
diff --git a/test/manager/nvm/package.spec.js b/test/manager/nvm/package.spec.js
new file mode 100644
index 0000000000..1c53f378b2
--- /dev/null
+++ b/test/manager/nvm/package.spec.js
@@ -0,0 +1,22 @@
+const node = require('../../../lib/manager/nvm/package');
+const defaultConfig = require('../../../lib/config/defaults').getConfig();
+
+describe('lib/workers/package/node', () => {
+  describe('getPackageUpdates', () => {
+    let config;
+    beforeEach(() => {
+      config = {
+        ...defaultConfig,
+      };
+    });
+    it('returns empty if matching', async () => {
+      config.currentVersion = ['6', '8'];
+      config.supportPolicy = ['lts_active'];
+      expect(await node.getPackageUpdates(config)).toEqual([]);
+    });
+    it('returns result if needing updates', async () => {
+      config.currentVersion = ['6', '8'];
+      expect(await node.getPackageUpdates(config)).toMatchSnapshot();
+    });
+  });
+});
diff --git a/test/manager/nvm/update.spec.js b/test/manager/nvm/update.spec.js
new file mode 100644
index 0000000000..529499f600
--- /dev/null
+++ b/test/manager/nvm/update.spec.js
@@ -0,0 +1,13 @@
+const nodefile = require('../../../lib/manager/nvm/update');
+
+describe('manager/nvm/update', () => {
+  describe('setNewValue', () => {
+    it('updates values', () => {
+      const upgrade = {
+        newVersion: '8.9.1',
+      };
+      const res = nodefile.setNewValue('8.9.0\n', upgrade);
+      expect(res).toMatchSnapshot();
+    });
+  });
+});
diff --git a/test/manager/resolve.spec.js b/test/manager/resolve.spec.js
index c30ff35c6d..78c9d002b4 100644
--- a/test/manager/resolve.spec.js
+++ b/test/manager/resolve.spec.js
@@ -94,18 +94,19 @@ describe('manager/resolve', () => {
       expect(res.packageFiles).toMatchSnapshot();
       expect(res.warnings).toHaveLength(0);
     });
-    it('detects meteor and docker and travis and bazel', async () => {
+    it('detects meteor and docker and travis and bazel and nvm', async () => {
       config.packageFiles = [
         'package.js',
         'Dockerfile',
         '.travis.yml',
         'WORKSPACE',
+        '.nvmrc',
       ];
       platform.getFile.mockReturnValueOnce('{}'); // package.js
       platform.getFile.mockReturnValueOnce('# comment\nFROM node:8\n'); // Dockerfile
-      platform.getFile.mockReturnValueOnce('hello: world\n'); // Dockerfile
       platform.getFile.mockReturnValueOnce('# travis'); // .travis.yml
       platform.getFile.mockReturnValueOnce('# WORKSPACE'); // Dockerfile
+      platform.getFile.mockReturnValueOnce('8.9\n'); // Dockerfile
       const res = await resolvePackageFiles(config);
       expect(res.packageFiles).toMatchSnapshot();
     });
@@ -116,6 +117,7 @@ describe('manager/resolve', () => {
         '.travis.yml',
         'WORKSPACE',
         'package.js',
+        '.nvmrc',
       ];
       platform.getFile.mockReturnValueOnce('# comment\n'); // Dockerfile
       const res = await resolvePackageFiles(config);
diff --git a/website/docs/_posts/2017-10-05-configuration-options.md b/website/docs/_posts/2017-10-05-configuration-options.md
index 991e8b6b65..f41a95b7d2 100644
--- a/website/docs/_posts/2017-10-05-configuration-options.md
+++ b/website/docs/_posts/2017-10-05-configuration-options.md
@@ -551,6 +551,17 @@ A string copy of npmrc file.
 
 See https://renovateapp.com/docs/deep-dives/private-modules for details on how this is used.
 
+## nvm
+
+Configuration specific for `.nvmrc` files.
+
+| name    | value             |
+| ------- | ----------------- |
+| type    | object            |
+| default | { enabled: true } |
+
+For settings common to all node.js version updates (e.g. travis, nvm, etc) you can use the `node` object instead.
+
 ## optionalDependencies
 
 Configuration specific for `package.json > optionalDependencies`.
-- 
GitLab