From bae9ae0595ebf0e6b12f050e7f12c37f3a18c9a1 Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@keylocation.sg>
Date: Sat, 2 Sep 2017 08:51:49 +0200
Subject: [PATCH] feat: stop branch processing after lock file error or pin
 dependencies (#768)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

If a repository has a lock file error (e.g. can’t look up a private module) then it will no longer attempt to create every branch. Instead, it will error/exit after the first branch. Additionally, “Pin Dependencies” has been sorted to be first and further branches won’t be added or updated until Pin Dependencies has been merged.
---
 lib/workers/branch/index.js                   |  8 +--
 lib/workers/repository/index.js               | 45 ++++++++++-----
 .../__snapshots__/index.spec.js.snap          | 51 +++++++++++++++++
 test/workers/repository/index.spec.js         | 57 +++++++++++++++++++
 4 files changed, 141 insertions(+), 20 deletions(-)
 create mode 100644 test/workers/repository/__snapshots__/index.spec.js.snap

diff --git a/lib/workers/branch/index.js b/lib/workers/branch/index.js
index fce07cf9b4..ef803acd49 100644
--- a/lib/workers/branch/index.js
+++ b/lib/workers/branch/index.js
@@ -52,7 +52,7 @@ async function processBranch(branchConfig) {
     }
     Object.assign(config, await getUpdatedLockFiles(config));
     if (config.lockFileError) {
-      throw new Error('lockFileError');
+      return 'lockFileError';
     }
     if (config.updatedLockFiles.length) {
       logger.debug(
@@ -81,11 +81,7 @@ async function processBranch(branchConfig) {
       return 'automerged';
     }
   } catch (err) {
-    if (err.message !== 'lockFileError') {
-      logger.error({ err }, `Error updating branch: ${err.message}`);
-    } else {
-      logger.info('Error updating branch');
-    }
+    logger.error({ err }, `Error updating branch: ${err.message}`);
     // Don't throw here - we don't want to stop the other renovations
     return 'error';
   }
diff --git a/lib/workers/repository/index.js b/lib/workers/repository/index.js
index 61f8c6899b..709a6ba7bd 100644
--- a/lib/workers/repository/index.js
+++ b/lib/workers/repository/index.js
@@ -10,9 +10,20 @@ const cleanup = require('./cleanup');
 const { decryptConfig } = require('../../config/decrypt');
 
 module.exports = {
+  pinDependenciesFirst,
   renovateRepository,
 };
 
+function pinDependenciesFirst(a, b) {
+  if (a.type === 'pin') {
+    return false;
+  }
+  if (b.type === 'pin') {
+    return true;
+  }
+  return a.branchName > b.branchName;
+}
+
 async function renovateRepository(repoConfig, token) {
   let config = { ...repoConfig };
   const { logger } = config;
@@ -111,22 +122,28 @@ async function renovateRepository(repoConfig, token) {
       logger.debug(`Updating ${branchUpgrades.length} branch(es)`);
       logger.trace({ config: branchUpgrades }, 'branchUpgrades');
       if (config.repoIsOnboarded) {
+        logger.info(`Processing ${branchUpgrades.length} branch(es)`);
+        // eslint-disable-next-line no-loop-function
+        branchUpgrades.sort(pinDependenciesFirst);
         for (const branchUpgrade of branchUpgrades) {
-          if (!baseBranchUpdated) {
-            const branchResult = await branchWorker.processBranch(
-              branchUpgrade,
-              config.errors,
-              config.warnings
-            );
-            if (branchResult === 'automerged') {
-              // Stop procesing other branches because base branch has been changed by an automerge
-              logger.info('Restarting repo renovation after automerge');
-              baseBranchUpdated = true;
-            }
-          } else {
-            logger.debug(
-              `Skipping branchUpgrade as base branch has been modified`
+          const branchResult = await branchWorker.processBranch(
+            branchUpgrade,
+            config.errors,
+            config.warnings
+          );
+          if (branchResult === 'automerged') {
+            // Stop procesing other branches because base branch has been changed by an automerge
+            logger.info('Restarting repo renovation after automerge');
+            baseBranchUpdated = true;
+            break;
+          } else if (branchResult === 'lockFileError') {
+            logger.info('Lock file error - stopping branch updates');
+            break;
+          } else if (branchUpgrade.type === 'pin') {
+            logger.info(
+              'Stopping branch processing until Pin Dependencies is merged'
             );
+            break;
           }
         }
         branchList = branchUpgrades.map(upgrade => upgrade.branchName);
diff --git a/test/workers/repository/__snapshots__/index.spec.js.snap b/test/workers/repository/__snapshots__/index.spec.js.snap
new file mode 100644
index 0000000000..d73bfc6c3d
--- /dev/null
+++ b/test/workers/repository/__snapshots__/index.spec.js.snap
@@ -0,0 +1,51 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`workers/repository pinDependenciesFirst returns pin first 1`] = `
+Array [
+  Object {
+    "branchName": "d",
+    "type": "pin",
+  },
+  Object {
+    "branchName": "a",
+  },
+  Object {
+    "branchName": "b",
+  },
+  Object {
+    "branchName": "c",
+  },
+]
+`;
+
+exports[`workers/repository pinDependenciesFirst returns pin first 2`] = `
+Array [
+  Object {
+    "branchName": "d",
+    "type": "pin",
+  },
+  Object {
+    "branchName": "a",
+  },
+  Object {
+    "branchName": "b",
+  },
+  Object {
+    "branchName": "c",
+  },
+]
+`;
+
+exports[`workers/repository pinDependenciesFirst returns sorted if no pin 1`] = `
+Array [
+  Object {
+    "branchName": "a",
+  },
+  Object {
+    "branchName": "b",
+  },
+  Object {
+    "branchName": "c",
+  },
+]
+`;
diff --git a/test/workers/repository/index.spec.js b/test/workers/repository/index.spec.js
index eeeb26b130..f6b87e2af8 100644
--- a/test/workers/repository/index.spec.js
+++ b/test/workers/repository/index.spec.js
@@ -8,6 +8,37 @@ const upgrades = require('../../../lib/workers/repository/upgrades');
 const logger = require('../../_fixtures/logger');
 
 describe('workers/repository', () => {
+  describe('pinDependenciesFirst', () => {
+    it('returns sorted if no pin', () => {
+      const arr = [
+        { branchName: 'a' },
+        { branchName: 'c' },
+        { branchName: 'b' },
+      ];
+      arr.sort(repositoryWorker.pinDependenciesFirst);
+      expect(arr).toMatchSnapshot();
+    });
+    it('returns pin first', () => {
+      const arr = [
+        { branchName: 'a' },
+        { branchName: 'c' },
+        { branchName: 'd', type: 'pin' },
+        { branchName: 'b' },
+      ];
+      arr.sort(repositoryWorker.pinDependenciesFirst);
+      expect(arr).toMatchSnapshot();
+    });
+    it('returns pin first', () => {
+      const arr = [
+        { branchName: 'd', type: 'pin' },
+        { branchName: 'a' },
+        { branchName: 'c' },
+        { branchName: 'b' },
+      ];
+      arr.sort(repositoryWorker.pinDependenciesFirst);
+      expect(arr).toMatchSnapshot();
+    });
+  });
   describe('renovateRepository', () => {
     let config;
     beforeEach(() => {
@@ -175,6 +206,32 @@ describe('workers/repository', () => {
       expect(branchWorker.processBranch.mock.calls).toHaveLength(3);
       expect(config.logger.error.mock.calls).toHaveLength(0);
     });
+    it('stops branchWorker after lockFileError', async () => {
+      config.packageFiles = ['package.json'];
+      config.hasRenovateJson = true;
+      onboarding.getOnboardingStatus.mockReturnValue(true);
+      upgrades.branchifyUpgrades.mockReturnValueOnce({
+        upgrades: [{}, {}, {}],
+      });
+      branchWorker.processBranch.mockReturnValue('lockFileError');
+      await repositoryWorker.renovateRepository(config);
+      expect(upgrades.branchifyUpgrades.mock.calls).toHaveLength(1);
+      expect(branchWorker.processBranch.mock.calls).toHaveLength(1);
+      expect(config.logger.error.mock.calls).toHaveLength(0);
+    });
+    it('stops branchWorker after pin', async () => {
+      config.packageFiles = ['package.json'];
+      config.hasRenovateJson = true;
+      onboarding.getOnboardingStatus.mockReturnValue(true);
+      upgrades.branchifyUpgrades.mockReturnValueOnce({
+        upgrades: [{ type: 'pin' }, {}, {}],
+      });
+      branchWorker.processBranch.mockReturnValue('done');
+      await repositoryWorker.renovateRepository(config);
+      expect(upgrades.branchifyUpgrades.mock.calls).toHaveLength(1);
+      expect(branchWorker.processBranch.mock.calls).toHaveLength(1);
+      expect(config.logger.error.mock.calls).toHaveLength(0);
+    });
     it('swallows errors', async () => {
       apis.initApis.mockImplementationOnce(() => {
         throw new Error('bad init');
-- 
GitLab