From feffa774d89e2047365c23aaa46cb68e4f162ffa Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@keylocation.sg>
Date: Thu, 27 Jul 2017 09:36:36 +0200
Subject: [PATCH] feat(lerna): Add lerna support (#535)

If a `lerna.json` is present in the root of the repository, then Renovate will automatically ignore (i.e. not renovate) all package names found in the `packages/*` path. It does not require an explicit configuration to work.
---
 lib/api/github.js                             | 13 +++++++++
 lib/api/gitlab.js                             | 15 ++++++++++
 lib/workers/dep-type/index.js                 |  7 ++++-
 lib/workers/repository/apis.js                | 16 +++++++++++
 test/api/__snapshots__/github.spec.js.snap    | 28 +++++++++++++++++++
 test/api/__snapshots__/gitlab.spec.js.snap    | 23 +++++++++++++++
 test/api/github.spec.js                       | 12 ++++++++
 test/api/gitlab.spec.js                       | 12 ++++++++
 test/workers/dep-type/index.spec.js           |  2 ++
 .../__snapshots__/apis.spec.js.snap           | 11 ++++++++
 test/workers/repository/apis.spec.js          | 24 ++++++++++++++++
 11 files changed, 162 insertions(+), 1 deletion(-)

diff --git a/lib/api/github.js b/lib/api/github.js
index f47cfb6861..036c188b07 100644
--- a/lib/api/github.js
+++ b/lib/api/github.js
@@ -35,6 +35,7 @@ module.exports = {
   updatePr,
   mergePr,
   // file
+  getSubDirectories,
   commitFilesToBranch,
   getFile,
   getFileContent,
@@ -602,6 +603,18 @@ async function getFileJson(filePath, branchName) {
   return fileJson;
 }
 
+async function getSubDirectories(path) {
+  logger.trace(`getSubDirectories(path=${path})`);
+  const res = await ghGot(`repos/${config.repoName}/contents/${path}`);
+  const directoryList = [];
+  res.body.forEach(item => {
+    if (item.type === 'dir') {
+      directoryList.push(item.name);
+    }
+  });
+  return directoryList;
+}
+
 // Add a new commit, create branch if not existing
 async function commitFilesToBranch(
   branchName,
diff --git a/lib/api/gitlab.js b/lib/api/gitlab.js
index 568755a7f6..ecce630629 100644
--- a/lib/api/gitlab.js
+++ b/lib/api/gitlab.js
@@ -28,6 +28,7 @@ module.exports = {
   updatePr,
   mergePr,
   // file
+  getSubDirectories,
   commitFilesToBranch,
   getFile,
   getFileContent,
@@ -436,6 +437,20 @@ async function updateFile(branchName, filePath, fileContents, message) {
   await glGot.put(url, opts);
 }
 
+async function getSubDirectories(path) {
+  logger.trace(`getSubDirectories(path=${path})`);
+  const res = await glGot(
+    `projects/${config.repoName}/repository/tree?path=${path}`
+  );
+  const directoryList = [];
+  res.body.forEach(item => {
+    if (item.type === 'tree') {
+      directoryList.push(item.name);
+    }
+  });
+  return directoryList;
+}
+
 // Add a new commit, create branch if not existing
 async function commitFilesToBranch(
   branchName,
diff --git a/lib/workers/dep-type/index.js b/lib/workers/dep-type/index.js
index a3d653a8fa..746ba1c70e 100644
--- a/lib/workers/dep-type/index.js
+++ b/lib/workers/dep-type/index.js
@@ -26,9 +26,14 @@ async function renovateDepType(packageContent, config) {
   logger.debug(`currentDeps length is ${currentDeps.length}`);
   logger.debug({ currentDeps }, `currentDeps`);
   // Filter out ignored dependencies
-  const filteredDeps = currentDeps.filter(
+  let filteredDeps = currentDeps.filter(
     dependency => config.ignoreDeps.indexOf(dependency.depName) === -1
   );
+  if (config.lernaPackages) {
+    filteredDeps = filteredDeps.filter(
+      dependency => config.lernaPackages.indexOf(dependency.depName) === -1
+    );
+  }
   logger.debug(`filteredDeps length is ${filteredDeps.length}`);
   logger.debug({ filteredDeps }, `filteredDeps`);
   // Obtain full config for each dependency
diff --git a/lib/workers/repository/apis.js b/lib/workers/repository/apis.js
index 266dad5161..d558eca848 100644
--- a/lib/workers/repository/apis.js
+++ b/lib/workers/repository/apis.js
@@ -10,6 +10,7 @@ module.exports = {
   setNpmrc,
   initApis,
   mergeRenovateJson,
+  checkForLerna,
   detectPackageFiles,
 };
 
@@ -28,6 +29,19 @@ async function setNpmrc(config) {
   }
 }
 
+async function checkForLerna(config) {
+  const lernaJson = await config.api.getFileContent('lerna.json');
+  if (!lernaJson) {
+    return {};
+  }
+  config.logger.debug('Found lerna config');
+  const lernaPackages = await config.api.getSubDirectories('packages');
+  if (lernaPackages.length === 0) {
+    return {};
+  }
+  return { lernaPackages };
+}
+
 async function initApis(inputConfig, token) {
   function getPlatformApi(platform) {
     if (platform === 'github') {
@@ -48,6 +62,8 @@ async function initApis(inputConfig, token) {
   );
   // Check for presence of .npmrc in repository
   Object.assign(config, platformConfig);
+  const lernaConfig = await module.exports.checkForLerna(config);
+  Object.assign(config, lernaConfig);
   await module.exports.setNpmrc(config);
   return config;
 }
diff --git a/test/api/__snapshots__/github.spec.js.snap b/test/api/__snapshots__/github.spec.js.snap
index b87fdc7ff7..a4459f8fe5 100644
--- a/test/api/__snapshots__/github.spec.js.snap
+++ b/test/api/__snapshots__/github.spec.js.snap
@@ -908,6 +908,34 @@ Array [
 ]
 `;
 
+exports[`api/github getSubDirectories(path) should return subdirectories 1`] = `
+Array [
+  Array [
+    "repos/some/repo",
+  ],
+  Array [
+    "repos/some/repo/git/refs/heads/master",
+  ],
+  Array [
+    "repos/some/repo/branches/master/protection/required_status_checks",
+    Object {
+      "headers": Object {
+        "accept": "application/vnd.github.loki-preview+json",
+      },
+    },
+  ],
+  Array [
+    "repos/some/repo/contents/some-path",
+  ],
+]
+`;
+
+exports[`api/github getSubDirectories(path) should return subdirectories 2`] = `
+Array [
+  "a",
+]
+`;
+
 exports[`api/github initRepo should detect repoForceRebase 1`] = `
 Object {
   "repoForceRebase": true,
diff --git a/test/api/__snapshots__/gitlab.spec.js.snap b/test/api/__snapshots__/gitlab.spec.js.snap
index eb2b7dce45..4471d38360 100644
--- a/test/api/__snapshots__/gitlab.spec.js.snap
+++ b/test/api/__snapshots__/gitlab.spec.js.snap
@@ -85,6 +85,29 @@ Array [
 ]
 `;
 
+exports[`api/gitlab createFile(branchName, filePath, fileContents, message) getSubDirectories(path) should return subdirectories 1`] = `
+Array [
+  Array [
+    "projects/owned",
+  ],
+  Array [
+    "projects/some%2Frepo",
+  ],
+  Array [
+    "user",
+  ],
+  Array [
+    "projects/some%2Frepo/repository/tree?path=some-path",
+  ],
+]
+`;
+
+exports[`api/gitlab createFile(branchName, filePath, fileContents, message) getSubDirectories(path) should return subdirectories 2`] = `
+Array [
+  "a",
+]
+`;
+
 exports[`api/gitlab createFile(branchName, filePath, fileContents, message) updateFile(branchName, filePath, fileContents, message) creates file with v3 1`] = `
 Array [
   Array [
diff --git a/test/api/github.spec.js b/test/api/github.spec.js
index dbf992b5ff..4aa8f16b0f 100644
--- a/test/api/github.spec.js
+++ b/test/api/github.spec.js
@@ -1291,6 +1291,18 @@ describe('api/github', () => {
       expect(content).toBeNull();
     });
   });
+  describe('getSubDirectories(path)', () => {
+    it('should return subdirectories', async () => {
+      await initRepo('some/repo', 'token');
+      ghGot.mockImplementationOnce(() => ({
+        body: [{ type: 'dir', name: 'a' }, { type: 'file', name: 'b' }],
+      }));
+      const dirList = await github.getSubDirectories('some-path');
+      expect(ghGot.mock.calls).toMatchSnapshot();
+      expect(dirList).toHaveLength(1);
+      expect(dirList).toMatchSnapshot();
+    });
+  });
   describe('commitFilesToBranch(branchName, files, message, parentBranch)', () => {
     beforeEach(async () => {
       await initRepo('some/repo', 'token');
diff --git a/test/api/gitlab.spec.js b/test/api/gitlab.spec.js
index 7082b9acb0..48813649d7 100644
--- a/test/api/gitlab.spec.js
+++ b/test/api/gitlab.spec.js
@@ -639,6 +639,18 @@ describe('api/gitlab', () => {
         expect(glGot.post.mock.calls[0][1].body.branch_name).toBeDefined();
       });
     });
+    describe('getSubDirectories(path)', () => {
+      it('should return subdirectories', async () => {
+        await initRepo('some/repo', 'token');
+        glGot.mockImplementationOnce(() => ({
+          body: [{ type: 'tree', name: 'a' }, { type: 'file', name: 'b' }],
+        }));
+        const dirList = await gitlab.getSubDirectories('some-path');
+        expect(glGot.mock.calls).toMatchSnapshot();
+        expect(dirList).toHaveLength(1);
+        expect(dirList).toMatchSnapshot();
+      });
+    });
     describe('commitFilesToBranch(branchName, files, message, parentBranch)', () => {
       it('creates branch', async () => {
         glGot.mockReturnValueOnce({ statusCode: 404 });
diff --git a/test/workers/dep-type/index.spec.js b/test/workers/dep-type/index.spec.js
index de5b480edf..cb04b3b513 100644
--- a/test/workers/dep-type/index.spec.js
+++ b/test/workers/dep-type/index.spec.js
@@ -15,6 +15,7 @@ describe('lib/workers/dep-type/index', () => {
     beforeEach(() => {
       config = {
         ignoreDeps: ['a', 'b'],
+        lernaPackages: ['e'],
       };
     });
     it('returns empty if config is disabled', async () => {
@@ -31,6 +32,7 @@ describe('lib/workers/dep-type/index', () => {
       packageJson.extractDependencies.mockReturnValueOnce([
         { depName: 'a' },
         { depName: 'b' },
+        { depName: 'e' },
       ]);
       const res = await depTypeWorker.renovateDepType({}, config);
       expect(res).toMatchObject([]);
diff --git a/test/workers/repository/__snapshots__/apis.spec.js.snap b/test/workers/repository/__snapshots__/apis.spec.js.snap
index 9cfb0ef22e..c7d31a9b5f 100644
--- a/test/workers/repository/__snapshots__/apis.spec.js.snap
+++ b/test/workers/repository/__snapshots__/apis.spec.js.snap
@@ -1,5 +1,16 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`workers/repository/apis checkForLerna(config) ignores zero length lerna 1`] = `Object {}`;
+
+exports[`workers/repository/apis checkForLerna(config) returns lerna package names 1`] = `
+Object {
+  "lernaPackages": Array [
+    "a",
+    "b",
+  ],
+}
+`;
+
 exports[`workers/repository/apis detectPackageFiles(config) adds package files to object 1`] = `
 Array [
   "package.json",
diff --git a/test/workers/repository/apis.spec.js b/test/workers/repository/apis.spec.js
index c86a30be32..3368686add 100644
--- a/test/workers/repository/apis.spec.js
+++ b/test/workers/repository/apis.spec.js
@@ -42,6 +42,30 @@ describe('workers/repository/apis', () => {
       await apis.setNpmrc(config);
     });
   });
+  describe('checkForLerna(config)', () => {
+    it('ignores zero length lerna', async () => {
+      const config = {
+        api: {
+          getFileContent: jest.fn(() => 'some content'),
+          getSubDirectories: jest.fn(() => []),
+        },
+        logger,
+      };
+      const res = await apis.checkForLerna(config);
+      expect(res).toMatchSnapshot();
+    });
+    it('returns lerna package names', async () => {
+      const config = {
+        api: {
+          getFileContent: jest.fn(() => 'some content'),
+          getSubDirectories: jest.fn(() => ['a', 'b']),
+        },
+        logger,
+      };
+      const res = await apis.checkForLerna(config);
+      expect(res).toMatchSnapshot();
+    });
+  });
   describe('initApis(config)', () => {
     beforeEach(() => {
       jest.resetAllMocks();
-- 
GitLab