diff --git a/lib/manager/docker/resolve.js b/lib/manager/docker/resolve.js
index e6108c50ba65657f25d7ccc0d738485e1dd9333a..cedb0a4060189d7fd8958b3317c4c3097e5f7c39 100644
--- a/lib/manager/docker/resolve.js
+++ b/lib/manager/docker/resolve.js
@@ -11,8 +11,7 @@ async function resolvePackageFile(config, inputFile) {
     `Resolving packageFile ${JSON.stringify(packageFile.packageFile)}`
   );
   packageFile.content = await config.api.getFileContent(
-    packageFile.packageFile,
-    config.contentBranch
+    packageFile.packageFile
   );
   if (!packageFile.content) {
     logger.debug('No packageFile content');
diff --git a/lib/manager/resolve.js b/lib/manager/resolve.js
new file mode 100644
index 0000000000000000000000000000000000000000..00f64e8d296ee22e949a631cdf832ca147f28b21
--- /dev/null
+++ b/lib/manager/resolve.js
@@ -0,0 +1,141 @@
+const path = require('path');
+
+const { migrateAndValidate } = require('../config/migrate-validate');
+const presets = require('../config/presets');
+
+const manager = require('./index');
+const dockerResolve = require('../manager/docker/resolve');
+const { mergeChildConfig } = require('../config');
+const { checkMonorepos } = require('../manager/npm/monorepos');
+
+module.exports = {
+  resolvePackageFiles,
+};
+
+async function resolvePackageFiles(config) {
+  const { logger } = config;
+  logger.trace({ config }, 'resolvePackageFiles()');
+  const allPackageFiles = config.packageFiles.length
+    ? config.packageFiles
+    : await manager.detectPackageFiles(config);
+  const packageFiles = [];
+  for (let packageFile of allPackageFiles) {
+    packageFile =
+      typeof packageFile === 'string' ? { packageFile } : packageFile;
+    if (packageFile.packageFile.endsWith('package.json')) {
+      logger.debug(`Resolving packageFile ${JSON.stringify(packageFile)}`);
+      const pFileRaw = await config.api.getFileContent(packageFile.packageFile);
+      if (!pFileRaw) {
+        logger.info(
+          { packageFile: packageFile.packageFile },
+          'Cannot find package.json'
+        );
+        config.errors.push({
+          depName: packageFile.packageFile,
+          message: 'Cannot find package.json',
+        });
+      } else {
+        try {
+          packageFile.content = JSON.parse(pFileRaw);
+        } catch (err) {
+          logger.info(
+            { packageFile: packageFile.packageFile },
+            'Cannot parse package.json'
+          );
+          config.warnings.push({
+            depName: packageFile.packageFile,
+            message: 'Cannot parse package.json (invalid JSON)',
+          });
+        }
+      }
+      if (!config.ignoreNpmrcFile) {
+        packageFile.npmrc = await config.api.getFileContent(
+          path.join(path.dirname(packageFile.packageFile), '.npmrc')
+        );
+      }
+      if (!packageFile.npmrc) {
+        delete packageFile.npmrc;
+      }
+      packageFile.yarnrc = await config.api.getFileContent(
+        path.join(path.dirname(packageFile.packageFile), '.yarnrc')
+      );
+      if (!packageFile.yarnrc) {
+        delete packageFile.yarnrc;
+      }
+      if (packageFile.content) {
+        // hoist renovate config if exists
+        if (packageFile.content.renovate) {
+          logger.debug(
+            {
+              packageFile: packageFile.packageFile,
+              config: packageFile.content.renovate,
+            },
+            `Found package.json renovate config`
+          );
+          const migratedConfig = migrateAndValidate(
+            config,
+            packageFile.content.renovate
+          );
+          logger.debug(
+            { config: migratedConfig },
+            'package.json migrated config'
+          );
+          const resolvedConfig = await presets.resolveConfigPresets(
+            migratedConfig,
+            config.logger
+          );
+          logger.debug(
+            { config: resolvedConfig },
+            'package.json resolved config'
+          );
+          Object.assign(packageFile, resolvedConfig);
+          delete packageFile.content.renovate;
+        } else {
+          logger.debug(
+            { packageFile: packageFile.packageFile },
+            `No renovate config`
+          );
+        }
+        // Detect if lock files are used
+        const yarnLockFileName = path.join(
+          path.dirname(packageFile.packageFile),
+          'yarn.lock'
+        );
+        packageFile.yarnLock = await config.api.getFileContent(
+          yarnLockFileName
+        );
+        if (packageFile.yarnLock) {
+          logger.debug(
+            { packageFile: packageFile.packageFile },
+            'Found yarn.lock'
+          );
+        }
+        const packageLockFileName = path.join(
+          path.dirname(packageFile.packageFile),
+          'package-lock.json'
+        );
+        packageFile.packageLock = await config.api.getFileContent(
+          packageLockFileName
+        );
+        if (packageFile.packageLock) {
+          logger.debug(
+            { packageFile: packageFile.packageFile },
+            'Found package-lock.json'
+          );
+        }
+      } else {
+        continue; // eslint-disable-line
+      }
+    } else if (packageFile.packageFile.endsWith('package.js')) {
+      // meteor
+      packageFile = mergeChildConfig(config.meteor, packageFile);
+    } else if (packageFile.packageFile.endsWith('Dockerfile')) {
+      logger.debug('Resolving Dockerfile');
+      packageFile = await dockerResolve.resolvePackageFile(config, packageFile);
+    }
+    if (packageFile) {
+      packageFiles.push(packageFile);
+    }
+  }
+  return checkMonorepos({ ...config, packageFiles });
+}
diff --git a/lib/workers/repository/apis.js b/lib/workers/repository/apis.js
deleted file mode 100644
index e78af34e8b5ee9e07aa85f37aefd2dd4c15c84d4..0000000000000000000000000000000000000000
--- a/lib/workers/repository/apis.js
+++ /dev/null
@@ -1,284 +0,0 @@
-const conventionalCommitsDetector = require('conventional-commits-detector');
-const path = require('path');
-const jsonValidator = require('json-dup-key-validator');
-const configParser = require('../../config');
-const presets = require('../../config/presets');
-// API
-const githubPlatform = require('../../platform/github');
-const gitlabPlatform = require('../../platform/gitlab');
-const dockerResolve = require('../../manager/docker/resolve');
-
-const { decryptConfig } = require('../../config/decrypt');
-const { migrateAndValidate } = require('../../config/migrate-validate');
-
-module.exports = {
-  detectSemanticCommits,
-  getNpmrc,
-  initApis,
-  mergeRenovateJson,
-  resolvePackageFiles,
-};
-
-async function detectSemanticCommits(config) {
-  const { logger } = config;
-  const commitMessages = await config.api.getCommitMessages();
-  logger.trace(`commitMessages=${JSON.stringify(commitMessages)}`);
-  const type = conventionalCommitsDetector(commitMessages);
-  if (type === 'unknown') {
-    logger.debug('No semantic commit type found');
-    return false;
-  }
-  logger.debug(
-    `Found semantic commit type ${type} - enabling semantic commits`
-  );
-  return true;
-}
-
-// Check for .npmrc in repository and pass it to npm api if found
-async function getNpmrc(config) {
-  if (config.ignoreNpmrcFile) {
-    return config;
-  }
-  const { logger } = config;
-  let npmrc;
-  try {
-    npmrc = await config.api.getFileContent('.npmrc');
-    if (npmrc) {
-      logger.debug('Found .npmrc file in repository');
-    }
-  } catch (err) {
-    logger.error('Failed to set .npmrc');
-  }
-  return { ...config, npmrc };
-}
-
-async function initApis(inputConfig, token) {
-  function getApi(platform) {
-    if (platform === 'github') {
-      return githubPlatform;
-    } else if (platform === 'gitlab') {
-      return gitlabPlatform;
-    }
-    throw new Error(`Unknown platform: ${platform}`);
-  }
-
-  const config = { ...inputConfig };
-  config.api = getApi(config.platform);
-  const platformConfig = await config.api.initRepo(
-    config.repository,
-    token,
-    config.endpoint,
-    config.logger
-  );
-  Object.assign(config, platformConfig);
-  if (config.semanticCommits === null) {
-    config.semanticCommits = await module.exports.detectSemanticCommits(config);
-  }
-  return module.exports.getNpmrc(config);
-}
-
-// Check for config in `renovate.json`
-async function mergeRenovateJson(config, branchName) {
-  const { logger } = config;
-  let returnConfig = { ...config };
-  const renovateJsonContent = await config.api.getFileContent(
-    'renovate.json',
-    branchName
-  );
-  if (!renovateJsonContent) {
-    logger.debug('No renovate.json found');
-    return returnConfig;
-  }
-  logger.debug('Found renovate.json file');
-  let renovateJson;
-  try {
-    let allowDuplicateKeys = true;
-    let jsonValidationError = jsonValidator.validate(
-      renovateJsonContent,
-      allowDuplicateKeys
-    );
-    if (jsonValidationError) {
-      const error = {
-        depName: 'renovate.json',
-        message: jsonValidationError,
-      };
-      logger.warn(error.message);
-      returnConfig.errors.push(error);
-      // Return unless error can be ignored
-      return returnConfig;
-    }
-    allowDuplicateKeys = false;
-    jsonValidationError = jsonValidator.validate(
-      renovateJsonContent,
-      allowDuplicateKeys
-    );
-    if (jsonValidationError) {
-      const error = {
-        depName: 'renovate.json',
-        message: jsonValidationError,
-      };
-      logger.warn(error.message);
-      returnConfig.errors.push(error);
-      // Return unless error can be ignored
-    }
-    renovateJson = JSON.parse(renovateJsonContent);
-    logger.debug({ config: renovateJson }, 'renovate.json config');
-    const migratedConfig = migrateAndValidate(config, renovateJson);
-    logger.debug({ config: migratedConfig }, 'renovate.json migrated config');
-    const decryptedConfig = decryptConfig(
-      migratedConfig,
-      logger,
-      config.privateKey
-    );
-    const resolvedConfig = await presets.resolveConfigPresets(
-      decryptedConfig,
-      config.logger
-    );
-    logger.debug({ config: resolvedConfig }, 'renovate.json resolved config');
-    returnConfig = configParser.mergeChildConfig(returnConfig, resolvedConfig);
-    returnConfig.renovateJsonPresent = true;
-  } catch (err) {
-    logger.info({ err, renovateJsonContent }, 'Could not parse renovate.json');
-    throw err;
-  }
-  return returnConfig;
-}
-
-async function resolvePackageFiles(inputConfig) {
-  const config = { ...inputConfig };
-  const { logger } = config;
-  logger.trace({ config }, 'resolvePackageFiles()');
-  const packageFiles = [];
-  const contentBranch = config.repoIsOnboarded
-    ? config.baseBranch || undefined
-    : config.onboardingBranch;
-  config.contentBranch = contentBranch;
-  for (let packageFile of config.packageFiles) {
-    packageFile =
-      typeof packageFile === 'string' ? { packageFile } : packageFile;
-    if (packageFile.packageFile.endsWith('package.json')) {
-      logger.debug(`Resolving packageFile ${JSON.stringify(packageFile)}`);
-      const pFileRaw = await config.api.getFileContent(
-        packageFile.packageFile,
-        contentBranch
-      );
-      if (!pFileRaw) {
-        logger.info(
-          { packageFile: packageFile.packageFile },
-          'Cannot find package.json'
-        );
-        config.errors.push({
-          depName: packageFile.packageFile,
-          message: 'Cannot find package.json',
-        });
-      } else {
-        try {
-          packageFile.content = JSON.parse(pFileRaw);
-        } catch (err) {
-          logger.info(
-            { packageFile: packageFile.packageFile },
-            'Cannot parse package.json'
-          );
-          config.warnings.push({
-            depName: packageFile.packageFile,
-            message: 'Cannot parse package.json (invalid JSON)',
-          });
-        }
-      }
-      if (!inputConfig.ignoreNpmrcFile) {
-        packageFile.npmrc = await config.api.getFileContent(
-          path.join(path.dirname(packageFile.packageFile), '.npmrc'),
-          contentBranch
-        );
-      }
-      if (!packageFile.npmrc) {
-        delete packageFile.npmrc;
-      }
-      packageFile.yarnrc = await config.api.getFileContent(
-        path.join(path.dirname(packageFile.packageFile), '.yarnrc'),
-        contentBranch
-      );
-      if (!packageFile.yarnrc) {
-        delete packageFile.yarnrc;
-      }
-      if (packageFile.content) {
-        // hoist renovate config if exists
-        if (packageFile.content.renovate) {
-          config.hasPackageJsonRenovateConfig = true;
-          logger.debug(
-            {
-              packageFile: packageFile.packageFile,
-              config: packageFile.content.renovate,
-            },
-            `Found package.json renovate config`
-          );
-          const migratedConfig = migrateAndValidate(
-            config,
-            packageFile.content.renovate
-          );
-          logger.debug(
-            { config: migratedConfig },
-            'package.json migrated config'
-          );
-          const resolvedConfig = await presets.resolveConfigPresets(
-            migratedConfig,
-            config.logger
-          );
-          logger.debug(
-            { config: resolvedConfig },
-            'package.json resolved config'
-          );
-          Object.assign(packageFile, resolvedConfig);
-          delete packageFile.content.renovate;
-        } else {
-          logger.debug(
-            { packageFile: packageFile.packageFile },
-            `No renovate config`
-          );
-        }
-        // Detect if lock files are used
-        const yarnLockFileName = path.join(
-          path.dirname(packageFile.packageFile),
-          'yarn.lock'
-        );
-        packageFile.yarnLock = await config.api.getFileContent(
-          yarnLockFileName,
-          contentBranch
-        );
-        if (packageFile.yarnLock) {
-          logger.debug(
-            { packageFile: packageFile.packageFile },
-            'Found yarn.lock'
-          );
-        }
-        const packageLockFileName = path.join(
-          path.dirname(packageFile.packageFile),
-          'package-lock.json'
-        );
-        packageFile.packageLock = await config.api.getFileContent(
-          packageLockFileName,
-          contentBranch
-        );
-        if (packageFile.packageLock) {
-          logger.debug(
-            { packageFile: packageFile.packageFile },
-            'Found package-lock.json'
-          );
-        }
-      } else {
-        continue; // eslint-disable-line
-      }
-    } else if (packageFile.packageFile.endsWith('package.js')) {
-      // meteor
-      packageFile = configParser.mergeChildConfig(config.meteor, packageFile);
-    } else if (packageFile.packageFile.endsWith('Dockerfile')) {
-      logger.debug('Resolving Dockerfile');
-      packageFile = await dockerResolve.resolvePackageFile(config, packageFile);
-    }
-    if (packageFile) {
-      packageFiles.push(packageFile);
-    }
-  }
-  config.packageFiles = packageFiles;
-  return config;
-}
diff --git a/lib/workers/repository/cleanup.js b/lib/workers/repository/cleanup.js
index b58232b1af9f87f9fe515f4d75c80c078c2ad5df..15ecb05abff6bbfe49c10cbdc37739ce2705098c 100644
--- a/lib/workers/repository/cleanup.js
+++ b/lib/workers/repository/cleanup.js
@@ -2,11 +2,17 @@ module.exports = {
   pruneStaleBranches,
 };
 
-async function pruneStaleBranches(config, branchList) {
-  const { logger } = config;
+async function pruneStaleBranches(config) {
+  // TODO: try/catch
+  const { branchList, logger } = config;
   logger.debug('Removing any stale branches');
-  logger.trace({ config, branchList }, `pruneStaleBranches`);
+  logger.trace({ config }, `pruneStaleBranches`);
+  if (!config.branchList) {
+    logger.debug('No branchList');
+    return;
+  }
   if (config.platform !== 'github') {
+    // TODO: Implement for GitLab
     logger.debug('Platform is not GitHub - returning');
     return;
   }
@@ -14,36 +20,16 @@ async function pruneStaleBranches(config, branchList) {
     config.branchPrefix
   );
   logger.debug(`renovateBranches=${renovateBranches}`);
-  if (
-    renovateBranches.indexOf(`${config.branchPrefix}lock-file-maintenance`) !==
-    -1
-  ) {
+  const lockFileBranch = `${config.branchPrefix}lock-file-maintenance`;
+  if (renovateBranches.includes(lockFileBranch)) {
     logger.debug('Checking lock file branch');
-    const pr = await config.api.getBranchPr(
-      `${config.branchPrefix}lock-file-maintenance`
-    );
-    if (pr && pr.isClosed) {
-      logger.info(
-        'Deleting lock file maintenance branch as PR has been closed'
-      );
-      await config.api.deleteBranch(
-        `${config.branchPrefix}lock-file-maintenance`
-      );
-    } else if (pr && pr.isUnmergeable) {
+    const pr = await config.api.getBranchPr(lockFileBranch);
+    if (pr && pr.isUnmergeable) {
       logger.info('Deleting lock file maintenance branch as it is unmergeable');
-      await config.api.deleteBranch(
-        `${config.branchPrefix}lock-file-maintenance`
-      );
-    } else if (pr && pr.changed_files === 0) {
-      logger.info(
-        'Deleting lock file maintenance branch as it has no changed files'
-      );
-      await config.api.deleteBranch(
-        `${config.branchPrefix}lock-file-maintenance`
-      );
+      await config.api.deleteBranch(lockFileBranch);
     }
     renovateBranches = renovateBranches.filter(
-      branch => branch !== `${config.branchPrefix}lock-file-maintenance`
+      branch => branch !== lockFileBranch
     );
   }
   const remainingBranches = renovateBranches.filter(
diff --git a/lib/workers/repository/configured.js b/lib/workers/repository/configured.js
new file mode 100644
index 0000000000000000000000000000000000000000..a758ed2759ea2a52486bf230bb797cc6a42967f1
--- /dev/null
+++ b/lib/workers/repository/configured.js
@@ -0,0 +1,12 @@
+module.exports = {
+  checkIfConfigured,
+};
+
+function checkIfConfigured(config) {
+  if (config.enabled === false) {
+    throw new Error('disabled');
+  }
+  if (config.isFork && !config.renovateFork) {
+    throw new Error('fork');
+  }
+}
diff --git a/lib/workers/repository/error.js b/lib/workers/repository/error.js
new file mode 100644
index 0000000000000000000000000000000000000000..43bdf43aa107f8de7c815adaac44ddd92db4934c
--- /dev/null
+++ b/lib/workers/repository/error.js
@@ -0,0 +1,28 @@
+module.exports = {
+  handleError,
+};
+
+function handleError(config, err) {
+  const { logger } = config;
+  if (err.message === 'uninitiated') {
+    logger.info('Repository is uninitiated - skipping');
+    return err.message;
+  } else if (err.message === 'disabled') {
+    logger.info('Repository is disabled - skipping');
+    return err.message;
+  } else if (err.message === 'fork') {
+    logger.info('Repository is a fork and not manually configured - skipping');
+    return err.message;
+  } else if (err.message === 'no-package-files') {
+    logger.info('Repository has no package files - skipping');
+    return err.message;
+  } else if (err.message === 'loops>5') {
+    logger.error('Repository has looped 5 times already');
+    return err.message;
+  }
+  // Swallow this error so that other repositories can be processed
+  logger.error({ err }, `Repository has unknown error`);
+  // delete branchList to avoid cleaning up branches
+  delete config.branchList; // eslint-disable-line no-param-reassign
+  return 'unknown-error';
+}
diff --git a/lib/workers/repository/index.js b/lib/workers/repository/index.js
index 9de200ad23660b847f7c5a5aebcace64babaf4ad..602fed0ebe7a6cf5a0b886cf2827e0a52356edb7 100644
--- a/lib/workers/repository/index.js
+++ b/lib/workers/repository/index.js
@@ -1,163 +1,40 @@
-const convertHrTime = require('convert-hrtime');
-const tmp = require('tmp-promise');
-const manager = require('../../manager');
-// Workers
-const branchWorker = require('../branch');
-// children
-const apis = require('./apis');
-const onboarding = require('./onboarding');
-const upgrades = require('./upgrades');
-const cleanup = require('./cleanup');
-
-const { checkMonorepos } = require('../../manager/npm/monorepos');
+const { initApis } = require('./init/apis');
+const { initRepo } = require('./init');
+const { determineUpdates } = require('./updates');
 const { ensureOnboardingPr } = require('./onboarding/pr');
+const { writeUpdates } = require('./write');
+const { handleError } = require('./error');
+const { pruneStaleBranches } = require('./cleanup');
+
+const { resolvePackageFiles } = require('../../manager/resolve');
 
 module.exports = {
   renovateRepository,
 };
 
-async function renovateRepository(repoConfig, token) {
-  let config = { ...repoConfig };
+async function renovateRepository(repoConfig, token, loop = 1) {
+  let config = { ...repoConfig, branchList: [] };
   const { logger } = config;
-  logger.trace({ config }, 'renovateRepository');
-  config.tmpDir = await tmp.dir({ unsafeCleanup: true });
-  config.errors = [];
-  config.warnings = [];
-  async function renovateRepositoryInner(count = 1) {
-    // istanbul ignore if
-    if (count > 5) {
-      // This is an arbitrary number added in to cut short any unintended infinite recursion
-      throw new Error('Existing renovateRepositoryInner after 5 loops');
-    }
-    logger.info(`renovateRepository loop ${count}`);
-    let branchList = [];
-    config = await apis.initApis(config, token);
-    config = await apis.mergeRenovateJson(config);
-    if (config.enabled === false) {
-      logger.debug('repository is disabled');
-      await cleanup.pruneStaleBranches(config, []);
-      return null;
-    }
-    if (config.isFork) {
-      if (config.renovateFork) {
-        logger.info('Processing forked repository');
-      } else {
-        logger.debug('repository is a fork and not manually configured');
-        await cleanup.pruneStaleBranches(config, []);
-        return null;
-      }
-    }
-    if (config.baseBranch) {
-      // Renovate should read content and target PRs here
-      if (await config.api.branchExists(config.baseBranch)) {
-        config.api.setBaseBranch(config.baseBranch);
-      } else {
-        // Warn and ignore setting (use default branch)
-        const message = `The configured baseBranch "${config.baseBranch}" is not present. Ignoring`;
-        config.errors.push({
-          depName: 'baseBranch',
-          message,
-        });
-        logger.warn(message);
-      }
-    }
-    config = await onboarding.getOnboardingStatus(config);
-    // Detect package files in default branch if not manually provisioned
-    if (config.packageFiles.length === 0) {
-      logger.debug('Detecting package files');
-      config.packageFiles = await manager.detectPackageFiles(config);
-      // If we can't detect any package.json then return
-      if (config.packageFiles.length === 0) {
-        logger.info('Cannot detect package files');
-        // istanbul ignore if
-        if (config.repoIsOnboarded === false) {
-          logger.warn('Need to delete onboarding PR');
-          const pr = await config.api.getBranchPr(config.onboardingBranch);
-          if (pr) {
-            logger.info('Found onboarding PR');
-            await config.api.updatePr(
-              pr.number,
-              'Configure Renovate - canceled',
-              'This PR was created in error and is now being deleted automatically. Sorry for the inconvenience.'
-            );
-            await config.api.deleteBranch(config.onboardingBranch);
-            throw new Error('no package files');
-          }
-        }
-        return null;
-      }
-      logger.info(
-        {
-          packageFiles: config.packageFiles,
-          count: config.packageFiles.length,
-        },
-        `Detected package files`
-      );
-    }
-    logger.debug('Resolving package files and content');
-    config = await apis.resolvePackageFiles(config);
-    config = await checkMonorepos(config);
-    logger.trace({ config }, 'post-packageFiles config');
-    // TODO: why is this fix needed?!
-    config.logger = logger;
-    const allUpgrades = await upgrades.determineRepoUpgrades(config);
-    const res = await upgrades.branchifyUpgrades(allUpgrades, logger);
-    config.errors = config.errors.concat(res.errors);
-    config.warnings = config.warnings.concat(res.warnings);
-    let branchUpgrades = res.upgrades;
-    logger.info(
-      { branches: branchUpgrades.map(upg => upg.branchName) },
-      'branchUpgrades'
-    );
-    logger.debug(`Updating ${branchUpgrades.length} branch(es)`);
-    logger.trace({ config: branchUpgrades }, 'branchUpgrades');
-    if (config.repoIsOnboarded) {
-      logger.info(`Processing ${branchUpgrades.length} branch(es)`);
-      const branchStartTime = process.hrtime();
-      branchList = branchUpgrades.map(upgrade => upgrade.branchName);
-      if (branchUpgrades.some(upg => upg.isPin)) {
-        logger.info('Processing only pin branches first');
-        branchUpgrades = branchUpgrades.filter(upg => upg.isPin);
-      }
-      for (const branchUpgrade of branchUpgrades) {
-        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');
-          return renovateRepositoryInner(count + 1);
-        }
-      }
-      logger.info(
-        { seconds: convertHrTime(process.hrtime(branchStartTime)).seconds },
-        'Finished updating branches'
-      );
-    } else {
-      await ensureOnboardingPr({ ...config, branches: branchUpgrades });
-      logger.info('"Configure Renovate" PR needs to be closed first');
-      branchList = [`${config.branchPrefix}configure`];
-    }
-    logger.debug(`branchList=${branchList}`);
-    return branchList;
-  }
+  logger.trace({ config, loop }, 'renovateRepository()');
   try {
-    const branchList = await renovateRepositoryInner();
-    if (branchList) {
-      await cleanup.pruneStaleBranches(config, branchList);
-    }
+    if (loop > 5) {
+      throw new Error('loops>5');
+    }
+    config = await initApis(config, token);
+    config = await initRepo(config);
+    config = await resolvePackageFiles(config);
+    config = await determineUpdates(config);
+    const res = config.repoIsOnboarded
+      ? await writeUpdates(config)
+      : await ensureOnboardingPr(config);
+    if (res === 'automerged') {
+      logger.info('Restarting repo renovation after automerge');
+      return renovateRepository(repoConfig, token, loop + 1);
+    }
+    return res;
   } catch (err) {
-    // Swallow this error so that other repositories can be processed
-    if (err.message === 'uninitiated') {
-      logger.info('Repository is uninitiated - skipping');
-    } else if (err.message === 'no package files') {
-      logger.info('Repository has no package files - skipping');
-    } else {
-      logger.error(`Failed to process repository: ${err.message}`);
-      logger.debug({ err });
-    }
+    return handleError(config, err);
+  } finally {
+    await pruneStaleBranches(config);
   }
-  config.tmpDir.cleanup();
 }
diff --git a/lib/workers/repository/init/apis.js b/lib/workers/repository/init/apis.js
new file mode 100644
index 0000000000000000000000000000000000000000..6855575cb4993997a4f44d39da37b8bb0547a43c
--- /dev/null
+++ b/lib/workers/repository/init/apis.js
@@ -0,0 +1,36 @@
+const githubPlatform = require('../../../platform/github');
+const gitlabPlatform = require('../../../platform/gitlab');
+const { detectSemanticCommits } = require('./semantic');
+
+function assignPlatform(config) {
+  const platforms = {
+    github: githubPlatform,
+    gitlab: gitlabPlatform,
+  };
+  return { ...config, api: platforms[config.platform] };
+}
+
+async function getPlatformConfig(config) {
+  const platformConfig = await config.api.initRepo(
+    config.repository,
+    config.token,
+    config.endpoint,
+    config.logger
+  );
+  return {
+    ...config,
+    ...platformConfig,
+  };
+}
+
+async function initApis(input, token) {
+  let config = { ...input, token };
+  config = await assignPlatform(config);
+  config = await getPlatformConfig(config);
+  config = await detectSemanticCommits(config);
+  return config;
+}
+
+module.exports = {
+  initApis,
+};
diff --git a/lib/workers/repository/init/base.js b/lib/workers/repository/init/base.js
new file mode 100644
index 0000000000000000000000000000000000000000..314ac5633bf03c6c028eb7a27810dafc3d356085
--- /dev/null
+++ b/lib/workers/repository/init/base.js
@@ -0,0 +1,25 @@
+async function checkBaseBranch(config) {
+  const { logger } = config;
+  let error = [];
+  if (config.baseBranch) {
+    // Renovate should read content and target PRs here
+    if (await config.api.branchExists(config.baseBranch)) {
+      await config.api.setBaseBranch(config.baseBranch);
+    } else {
+      // Warn and ignore setting (use default branch)
+      const message = `The configured baseBranch "${config.baseBranch}" is not present. Ignoring`;
+      error = [
+        {
+          depName: 'baseBranch',
+          message,
+        },
+      ];
+      logger.warn(message);
+    }
+  }
+  return { ...config, errors: config.errors.concat(error) };
+}
+
+module.exports = {
+  checkBaseBranch,
+};
diff --git a/lib/workers/repository/init/config.js b/lib/workers/repository/init/config.js
new file mode 100644
index 0000000000000000000000000000000000000000..e6433014b3b151cd96bc107a483a1e1ace5de983
--- /dev/null
+++ b/lib/workers/repository/init/config.js
@@ -0,0 +1,68 @@
+const jsonValidator = require('json-dup-key-validator');
+
+const { mergeChildConfig } = require('../../../config');
+const { migrateAndValidate } = require('../../../config/migrate-validate');
+const { decryptConfig } = require('../../../config/decrypt');
+const presets = require('../../../config/presets');
+
+// Check for config in `renovate.json`
+async function mergeRenovateJson(config) {
+  const { logger } = config;
+  let returnConfig = { ...config };
+  const renovateJsonContent = await config.api.getFileContent('renovate.json');
+  if (!renovateJsonContent) {
+    logger.debug('No renovate.json found');
+    return returnConfig;
+  }
+  logger.debug('Found renovate.json file');
+  let allowDuplicateKeys = true;
+  let jsonValidationError = jsonValidator.validate(
+    renovateJsonContent,
+    allowDuplicateKeys
+  );
+  if (jsonValidationError) {
+    const error = {
+      depName: 'renovate.json',
+      message: jsonValidationError,
+    };
+    logger.warn(error.message);
+    returnConfig.errors.push(error);
+    // Return unless error can be ignored
+    return returnConfig;
+  }
+  allowDuplicateKeys = false;
+  jsonValidationError = jsonValidator.validate(
+    renovateJsonContent,
+    allowDuplicateKeys
+  );
+  if (jsonValidationError) {
+    const error = {
+      depName: 'renovate.json',
+      message: jsonValidationError,
+    };
+    logger.warn(error.message);
+    returnConfig.errors.push(error);
+    // Return unless error can be ignored
+  }
+  const renovateJson = JSON.parse(renovateJsonContent);
+  logger.debug({ config: renovateJson }, 'renovate.json config');
+  const migratedConfig = migrateAndValidate(config, renovateJson);
+  logger.debug({ config: migratedConfig }, 'renovate.json migrated config');
+  const decryptedConfig = decryptConfig(
+    migratedConfig,
+    config.logger,
+    config.privateKey
+  );
+  const resolvedConfig = await presets.resolveConfigPresets(
+    decryptedConfig,
+    logger
+  );
+  logger.trace({ config: resolvedConfig }, 'renovate.json resolved config');
+  returnConfig = mergeChildConfig(returnConfig, resolvedConfig);
+  returnConfig.renovateJsonPresent = true;
+  return returnConfig;
+}
+
+module.exports = {
+  mergeRenovateJson,
+};
diff --git a/lib/workers/repository/init/index.js b/lib/workers/repository/init/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..d2fa4c196a8f11ad0067b6d4d255dafbaf2968c6
--- /dev/null
+++ b/lib/workers/repository/init/index.js
@@ -0,0 +1,18 @@
+const { checkOnboardingBranch } = require('../onboarding/branch');
+const { checkIfConfigured } = require('../configured');
+
+const { checkBaseBranch } = require('./base');
+const { mergeRenovateJson } = require('./config');
+
+async function initRepo(input) {
+  let config = { ...input, errors: [], warnings: [] };
+  config = await checkOnboardingBranch(config);
+  config = await mergeRenovateJson(config);
+  checkIfConfigured(config);
+  await checkBaseBranch(config);
+  return config;
+}
+
+module.exports = {
+  initRepo,
+};
diff --git a/lib/workers/repository/init/semantic.js b/lib/workers/repository/init/semantic.js
new file mode 100644
index 0000000000000000000000000000000000000000..8e4f365ab1131de82991f15b9f28447ccc6e4b74
--- /dev/null
+++ b/lib/workers/repository/init/semantic.js
@@ -0,0 +1,23 @@
+const conventionalCommitsDetector = require('conventional-commits-detector');
+
+async function detectSemanticCommits(config) {
+  const { logger } = config;
+  if (config.semanticCommits !== null) {
+    return config;
+  }
+  const commitMessages = await config.api.getCommitMessages();
+  logger.trace(`commitMessages=${JSON.stringify(commitMessages)}`);
+  const type = conventionalCommitsDetector(commitMessages);
+  if (type === 'unknown') {
+    logger.debug('No semantic commit type found');
+    return { ...config, semanticCommits: false };
+  }
+  logger.debug(
+    `Found semantic commit type ${type} - enabling semantic commits`
+  );
+  return { ...config, semanticCommits: true };
+}
+
+module.exports = {
+  detectSemanticCommits,
+};
diff --git a/lib/workers/repository/onboarding.js b/lib/workers/repository/onboarding.js
deleted file mode 100644
index 4f18caf37ae7f655b2ace85560b68c5e1c59b228..0000000000000000000000000000000000000000
--- a/lib/workers/repository/onboarding.js
+++ /dev/null
@@ -1,64 +0,0 @@
-const apis = require('./apis');
-const manager = require('../../manager');
-
-module.exports = {
-  createOnboardingBranch,
-  getOnboardingStatus,
-};
-
-async function createOnboardingBranch(inputConfig) {
-  const config = { ...inputConfig };
-  const { logger } = config;
-  logger.debug('Creating onboarding branch');
-  config.packageFiles = await manager.detectPackageFiles(config);
-  if (config.packageFiles.length === 0) {
-    throw new Error('no package files');
-  }
-  const renovateJson = {
-    extends: ['config:base'],
-  };
-  logger.info({ renovateJson }, 'Creating onboarding branch');
-  await config.api.commitFilesToBranch(
-    config.onboardingBranch,
-    [
-      {
-        name: 'renovate.json',
-        contents: `${JSON.stringify(renovateJson, null, 2)}\n`,
-      },
-    ],
-    'Add renovate.json'
-  );
-  return config;
-}
-
-async function getOnboardingStatus(inputConfig) {
-  let config = { ...inputConfig };
-  const { logger } = config;
-  logger.debug('Checking if repo is onboarded');
-  // Check if repository is configured
-  if (config.onboarding === false) {
-    logger.debug('Repo onboarding is disabled');
-    return { ...config, repoIsOnboarded: true };
-  }
-  if (config.renovateJsonPresent) {
-    logger.debug('Repo has renovate.json');
-    return { ...config, repoIsOnboarded: true };
-  }
-  config.onboardingBranch = `${config.branchPrefix}configure`;
-  const pr = await config.api.findPr(
-    config.onboardingBranch,
-    'Configure Renovate'
-  );
-  if (pr && pr.isClosed) {
-    logger.debug('Found closed Configure Renovate PR');
-    return { ...config, repoIsOnboarded: true };
-  }
-  if (pr) {
-    logger.debug(`Found existing onboarding PR #${pr.number}`);
-  } else {
-    config = await module.exports.createOnboardingBranch(config);
-  }
-  logger.debug('Merging renovate.json from onboarding branch');
-  config = await apis.mergeRenovateJson(config, config.onboardingBranch);
-  return { ...config, repoIsOnboarded: false };
-}
diff --git a/lib/workers/repository/onboarding/branch/check.js b/lib/workers/repository/onboarding/branch/check.js
new file mode 100644
index 0000000000000000000000000000000000000000..15165500f1c65ce138c66e4a44e3b14938b347f9
--- /dev/null
+++ b/lib/workers/repository/onboarding/branch/check.js
@@ -0,0 +1,44 @@
+const findFile = async (config, fileName) => {
+  const { logger } = config;
+  logger.debug('findFile()');
+  logger.trace({ config });
+  const fileList = await config.api.getFileList();
+  return fileList.includes(fileName);
+};
+
+const renovateJsonExists = config => findFile(config, 'renovate.json');
+
+const closedPrExists = config =>
+  config.api.findPr(
+    `${config.branchPrefix}configure`,
+    'Configure Renovate',
+    'closed'
+  );
+
+const isOnboarded = async config => {
+  const { logger } = config;
+  logger.debug('isOnboarded()');
+  if (await renovateJsonExists(config)) {
+    logger.debug('renovate.json exists');
+    return true;
+  }
+  logger.debug('renovate.json not found');
+  if (await closedPrExists(config)) {
+    logger.debug('Found closed onboarding PR');
+    return true;
+  }
+  logger.debug('Found no closed onboarding PR');
+  return false;
+};
+
+const onboardingPrExists = config =>
+  config.api.findPr(
+    `${config.branchPrefix}configure`,
+    'Configure Renovate',
+    'open'
+  );
+
+module.exports = {
+  isOnboarded,
+  onboardingPrExists,
+};
diff --git a/lib/workers/repository/onboarding/branch/create.js b/lib/workers/repository/onboarding/branch/create.js
new file mode 100644
index 0000000000000000000000000000000000000000..d957d5965832cfd5e33b00033c8f5df36c945def
--- /dev/null
+++ b/lib/workers/repository/onboarding/branch/create.js
@@ -0,0 +1,22 @@
+async function createOnboardingBranch(config) {
+  const { logger } = config;
+  logger.debug('Creating onboarding branch');
+  const renovateJson = {
+    extends: ['config:base'],
+  };
+  logger.info({ renovateJson }, 'Creating onboarding branch');
+  await config.api.commitFilesToBranch(
+    `${config.branchPrefix}configure`,
+    [
+      {
+        name: 'renovate.json',
+        contents: `${JSON.stringify(renovateJson, null, 2)}\n`,
+      },
+    ],
+    'Add renovate.json'
+  );
+}
+
+module.exports = {
+  createOnboardingBranch,
+};
diff --git a/lib/workers/repository/onboarding/branch/index.js b/lib/workers/repository/onboarding/branch/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..a0c842e5f4e9472ca6f4a8fc4fc90d1ec54f81de
--- /dev/null
+++ b/lib/workers/repository/onboarding/branch/index.js
@@ -0,0 +1,32 @@
+const { detectPackageFiles } = require('../../../../manager');
+const { createOnboardingBranch } = require('./create');
+const { isOnboarded, onboardingPrExists } = require('./check');
+
+async function checkOnboardingBranch(config) {
+  const { logger } = config;
+  logger.debug('checkOnboarding()');
+  logger.trace({ config });
+  const repoIsOnboarded = await isOnboarded(config);
+  if (repoIsOnboarded) {
+    logger.debug('Repo is onboarded');
+    return { ...config, repoIsOnboarded };
+  }
+  if (config.isFork) {
+    throw new Error('fork');
+  }
+  logger.info('Repo is not onboarded');
+  if (!await onboardingPrExists(config)) {
+    if ((await detectPackageFiles(config)).length === 0) {
+      throw new Error('no-package-files');
+    }
+    logger.info('Need to create onboarding PR');
+    await createOnboardingBranch(config);
+  }
+  await config.api.setBaseBranch(`${config.branchPrefix}configure`);
+  const branchList = [`${config.branchPrefix}configure`];
+  return { ...config, repoIsOnboarded, branchList };
+}
+
+module.exports = {
+  checkOnboardingBranch,
+};
diff --git a/lib/workers/repository/updates/branchify.js b/lib/workers/repository/updates/branchify.js
new file mode 100644
index 0000000000000000000000000000000000000000..fcaa73d4496999d10a8d9142767bc9c2b8bacc97
--- /dev/null
+++ b/lib/workers/repository/updates/branchify.js
@@ -0,0 +1,65 @@
+const handlebars = require('handlebars');
+
+const { generateBranchConfig } = require('./generate');
+
+function branchifyUpgrades(config) {
+  const { logger } = config;
+  logger.debug('branchifyUpgrades');
+  logger.trace({ config });
+  const errors = [];
+  const warnings = [];
+  const branchUpgrades = {};
+  const branches = [];
+  for (const upg of config.upgrades) {
+    const upgrade = { ...upg };
+    // Split out errors and wrnings first
+    if (upgrade.type === 'error') {
+      errors.push(upgrade);
+    } else if (upgrade.type === 'warning') {
+      warnings.push(upgrade);
+    } else {
+      // Check whether to use a group name
+      let branchName;
+      if (upgrade.groupName) {
+        logger.debug('Using group branchName template');
+        logger.debug(
+          `Dependency ${upgrade.depName} is part of group ${upgrade.groupName}`
+        );
+        upgrade.groupSlug =
+          upgrade.groupSlug ||
+          upgrade.groupName
+            .toString()
+            .toLowerCase()
+            .replace(/[^a-z0-9+]+/g, '-');
+        branchName = handlebars.compile(upgrade.group.branchName)(upgrade);
+      } else {
+        logger.debug('Using regular branchName template');
+        branchName = handlebars.compile(upgrade.branchName)(upgrade);
+      }
+      branchUpgrades[branchName] = branchUpgrades[branchName] || [];
+      branchUpgrades[branchName] = [upgrade].concat(branchUpgrades[branchName]);
+    }
+  }
+  logger.debug(`Returning ${Object.keys(branchUpgrades).length} branch(es)`);
+  for (const branchName of Object.keys(branchUpgrades)) {
+    const branch = generateBranchConfig(branchUpgrades[branchName], logger);
+    branch.branchName = branchName;
+    branch.logger = logger;
+    branches.push(branch);
+  }
+  const branchList = config.repoIsOnboarded
+    ? branches.map(upgrade => upgrade.branchName)
+    : config.branchList;
+  return {
+    ...config,
+    errors: config.errors.concat(errors),
+    warnings: config.warnings.concat(warnings),
+    branches,
+    branchList,
+    upgrades: null,
+  };
+}
+
+module.exports = {
+  branchifyUpgrades,
+};
diff --git a/lib/workers/repository/updates/determine.js b/lib/workers/repository/updates/determine.js
new file mode 100644
index 0000000000000000000000000000000000000000..d78d22977b0e882eadc24623e5e57695fce1a221
--- /dev/null
+++ b/lib/workers/repository/updates/determine.js
@@ -0,0 +1,52 @@
+const packageFileWorker = require('../../package-file');
+const { mergeChildConfig, filterConfig } = require('../../../config');
+
+async function determineRepoUpgrades(config) {
+  const { logger } = config;
+  logger.debug('determineRepoUpgrades()');
+  logger.trace({ config });
+  let upgrades = [];
+  logger.debug(`Found ${config.packageFiles.length} package files`);
+  // Iterate through repositories sequentially
+  for (const packageFile of config.packageFiles) {
+    logger.debug({ packageFile }, 'Getting packageFile config');
+    let packageFileConfig = mergeChildConfig(config, packageFile);
+    packageFileConfig = filterConfig(packageFileConfig, 'packageFile');
+    packageFileConfig.logger = packageFileConfig.logger.child({
+      repository: packageFileConfig.repository,
+      packageFile: packageFileConfig.packageFile,
+    });
+    if (packageFileConfig.packageFile.endsWith('package.json')) {
+      logger.info(
+        { packageFile: packageFileConfig.packageFile },
+        'Renovating package.json dependencies'
+      );
+      upgrades = upgrades.concat(
+        await packageFileWorker.renovatePackageFile(packageFileConfig)
+      );
+    } else if (packageFileConfig.packageFile.endsWith('package.js')) {
+      logger.info('Renovating package.js (meteor) dependencies');
+      upgrades = upgrades.concat(
+        await packageFileWorker.renovateMeteorPackageFile(packageFileConfig)
+      );
+    } else if (packageFileConfig.packageFile.endsWith('Dockerfile')) {
+      logger.info('Renovating Dockerfile FROM');
+      upgrades = upgrades.concat(
+        await packageFileWorker.renovateDockerfile(packageFileConfig)
+      );
+    }
+  }
+  // Sanitize depNames
+  upgrades = upgrades.map(upgrade => ({
+    ...upgrade,
+    depNameSanitized: upgrade.depName
+      ? upgrade.depName
+          .replace('@', '')
+          .replace('/', '-')
+          .toLowerCase()
+      : undefined,
+  }));
+  return { ...config, upgrades };
+}
+
+module.exports = { determineRepoUpgrades };
diff --git a/lib/workers/repository/updates/generate.js b/lib/workers/repository/updates/generate.js
new file mode 100644
index 0000000000000000000000000000000000000000..13cba0e0d42122250735f64e14321610f8d70117
--- /dev/null
+++ b/lib/workers/repository/updates/generate.js
@@ -0,0 +1,52 @@
+const handlebars = require('handlebars');
+
+function generateBranchConfig(branchUpgrades, logger) {
+  const config = {
+    upgrades: [],
+  };
+  const hasGroupName = branchUpgrades[0].groupName !== null;
+  logger.debug(`hasGroupName: ${hasGroupName}`);
+  // Use group settings only if multiple upgrades or lazy grouping is disabled
+  const depNames = [];
+  branchUpgrades.forEach(upg => {
+    if (!depNames.includes(upg.depName)) {
+      depNames.push(upg.depName);
+    }
+  });
+  const groupEligible =
+    depNames.length > 1 || branchUpgrades[0].lazyGrouping === false;
+  logger.debug(`groupEligible: ${groupEligible}`);
+  const useGroupSettings = hasGroupName && groupEligible;
+  logger.debug(`useGroupSettings: ${useGroupSettings}`);
+  for (const branchUpgrade of branchUpgrades) {
+    const upgrade = { ...branchUpgrade };
+    if (useGroupSettings) {
+      // Now overwrite original config with group config
+      Object.assign(upgrade, upgrade.group);
+    } else {
+      delete upgrade.groupName;
+    }
+    // Delete group config regardless of whether it was applied
+    delete upgrade.group;
+    delete upgrade.lazyGrouping;
+    // Use templates to generate strings
+    logger.debug(
+      { branchName: upgrade.branchName, prTitle: upgrade.prTitle },
+      'Compiling branchName and prTitle'
+    );
+    upgrade.branchName = handlebars.compile(upgrade.branchName)(upgrade);
+    upgrade.prTitle = handlebars.compile(upgrade.prTitle)(upgrade);
+    if (upgrade.semanticCommits) {
+      logger.debug('Upgrade has semantic commits enabled');
+      upgrade.prTitle = `${upgrade.semanticPrefix} ${upgrade.prTitle.toLowerCase()}`;
+    }
+    logger.debug(`${upgrade.branchName}, ${upgrade.prTitle}`);
+    config.upgrades.push(upgrade);
+  }
+  // Now assign first upgrade's config as branch config
+  return { ...config, ...config.upgrades[0] };
+}
+
+module.exports = {
+  generateBranchConfig,
+};
diff --git a/lib/workers/repository/updates/index.js b/lib/workers/repository/updates/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..8572ea739575c64e77ddbbbbd41d4cb9b5bf0e27
--- /dev/null
+++ b/lib/workers/repository/updates/index.js
@@ -0,0 +1,16 @@
+const { determineRepoUpgrades } = require('./determine');
+const { branchifyUpgrades } = require('./branchify');
+
+module.exports = {
+  determineUpdates,
+};
+
+async function determineUpdates(input) {
+  let config = { ...input };
+  const { logger } = config;
+  logger.debug('determineUpdates()');
+  logger.trace({ config });
+  config = await determineRepoUpgrades(config);
+  config = branchifyUpgrades(config);
+  return config;
+}
diff --git a/lib/workers/repository/upgrades.js b/lib/workers/repository/upgrades.js
deleted file mode 100644
index b7280de06d18574f4a47edf83f98c0497b21b286..0000000000000000000000000000000000000000
--- a/lib/workers/repository/upgrades.js
+++ /dev/null
@@ -1,201 +0,0 @@
-const convertHrTime = require('convert-hrtime');
-const handlebars = require('handlebars');
-const configParser = require('../../config');
-const packageFileWorker = require('../package-file');
-
-let logger = require('../../logger');
-
-module.exports = {
-  determineRepoUpgrades,
-  groupByBranch,
-  generateConfig,
-  branchifyUpgrades,
-  getPackageFileConfig,
-};
-
-async function determineRepoUpgrades(config) {
-  logger.trace({ config }, 'determineRepoUpgrades');
-  const startTime = process.hrtime();
-  if (config.packageFiles.length === 0) {
-    logger.info('No package files found');
-  }
-  let upgrades = [];
-  // Iterate through repositories sequentially
-  let index = 0;
-  for (const packageFile of config.packageFiles) {
-    logger.trace({ packageFile }, 'Getting packageFile config');
-    const packageFileConfig = module.exports.getPackageFileConfig(
-      config,
-      index
-    );
-    if (packageFileConfig.packageFile.endsWith('package.json')) {
-      logger.info(
-        { packageFile: packageFileConfig.packageFile },
-        'Renovating package.json dependencies'
-      );
-      upgrades = upgrades.concat(
-        await packageFileWorker.renovatePackageFile(packageFileConfig)
-      );
-    } else if (packageFileConfig.packageFile.endsWith('package.js')) {
-      logger.info('Renovating package.js (meteor) dependencies');
-      upgrades = upgrades.concat(
-        await packageFileWorker.renovateMeteorPackageFile(packageFileConfig)
-      );
-    } else if (packageFileConfig.packageFile.endsWith('Dockerfile')) {
-      logger.info('Renovating Dockerfile FROM');
-      upgrades = upgrades.concat(
-        await packageFileWorker.renovateDockerfile(packageFileConfig)
-      );
-    }
-    index += 1;
-  }
-  logger.info(
-    { seconds: convertHrTime(process.hrtime(startTime)).seconds },
-    'Finished determining repo upgrades'
-  );
-  // Sanitize depNames
-  return upgrades.map(upgrade => ({
-    ...upgrade,
-    depNameSanitized: upgrade.depName
-      ? upgrade.depName
-          .replace('@', '')
-          .replace('/', '-')
-          .toLowerCase()
-      : undefined,
-  }));
-}
-
-function generateConfig(branchUpgrades) {
-  const config = {
-    upgrades: [],
-  };
-  const hasGroupName = branchUpgrades[0].groupName !== null;
-  logger.debug(`hasGroupName: ${hasGroupName}`);
-  // Use group settings only if multiple upgrades or lazy grouping is disabled
-  const depNames = [];
-  branchUpgrades.forEach(upg => {
-    if (!depNames.includes(upg.depName)) {
-      depNames.push(upg.depName);
-    }
-  });
-  const groupEligible =
-    depNames.length > 1 || branchUpgrades[0].lazyGrouping === false;
-  logger.debug(`groupEligible: ${groupEligible}`);
-  const useGroupSettings = hasGroupName && groupEligible;
-  logger.debug(`useGroupSettings: ${useGroupSettings}`);
-  for (const branchUpgrade of branchUpgrades) {
-    const upgrade = { ...branchUpgrade };
-    if (useGroupSettings) {
-      // Now overwrite original config with group config
-      Object.assign(upgrade, upgrade.group);
-    } else {
-      delete upgrade.groupName;
-    }
-    // Delete group config regardless of whether it was applied
-    delete upgrade.group;
-    delete upgrade.lazyGrouping;
-    // Use templates to generate strings
-    logger.debug(
-      { branchName: upgrade.branchName, prTitle: upgrade.prTitle },
-      'Compiling branchName and prTitle'
-    );
-    upgrade.branchName = handlebars.compile(upgrade.branchName)(upgrade);
-    upgrade.prTitle = handlebars.compile(upgrade.prTitle)(upgrade);
-    if (upgrade.semanticCommits) {
-      logger.debug('Upgrade has semantic commits enabled');
-      upgrade.prTitle = `${upgrade.semanticPrefix} ${upgrade.prTitle.toLowerCase()}`;
-    }
-    logger.debug(`${upgrade.branchName}, ${upgrade.prTitle}`);
-    config.upgrades.push(upgrade);
-  }
-  // Now assign first upgrade's config as branch config
-  return { ...config, ...config.upgrades[0] };
-}
-
-function groupByBranch(upgrades) {
-  logger.trace({ config: upgrades }, 'groupByBranch');
-  logger.info(`Processing ${upgrades.length} dependency upgrade(s)`);
-  const result = {
-    errors: [],
-    warnings: [],
-    branchUpgrades: {},
-  };
-  for (const upg of upgrades) {
-    const upgrade = { ...upg };
-    // Split out errors and wrnings first
-    if (upgrade.type === 'error') {
-      result.errors.push(upgrade);
-    } else if (upgrade.type === 'warning') {
-      result.warnings.push(upgrade);
-    } else {
-      // Check whether to use a group name
-      let branchName;
-      if (upgrade.groupName) {
-        logger.debug('Using group branchName template');
-        logger.debug(
-          `Dependency ${upgrade.depName} is part of group ${upgrade.groupName}`
-        );
-        upgrade.groupSlug =
-          upgrade.groupSlug ||
-          upgrade.groupName
-            .toString()
-            .toLowerCase()
-            .replace(/[^a-z0-9+]+/g, '-');
-        branchName = handlebars.compile(upgrade.group.branchName)(upgrade);
-      } else {
-        logger.debug('Using regular branchName template');
-        branchName = handlebars.compile(upgrade.branchName)(upgrade);
-      }
-      result.branchUpgrades[branchName] =
-        result.branchUpgrades[branchName] || [];
-      result.branchUpgrades[branchName] = [upgrade].concat(
-        result.branchUpgrades[branchName]
-      );
-    }
-  }
-  logger.debug(
-    `Returning ${Object.keys(result.branchUpgrades).length} branch(es)`
-  );
-  return result;
-}
-
-function branchifyUpgrades(upgrades, parentLogger) {
-  logger = parentLogger || logger;
-  logger.debug('branchifyUpgrades');
-  logger.trace({ config: upgrades }, 'branchifyUpgrades');
-  const branchConfigs = [];
-  const res = module.exports.groupByBranch(upgrades);
-  for (const branchName of Object.keys(res.branchUpgrades)) {
-    logger = logger.child({ branch: branchName });
-    const branchUpgrades = res.branchUpgrades[branchName];
-    const branchConfig = module.exports.generateConfig(branchUpgrades);
-    branchConfig.branchName = branchName;
-    branchConfig.logger = logger;
-    branchConfigs.push(branchConfig);
-  }
-  return {
-    errors: res.errors,
-    warnings: res.warnings,
-    upgrades: branchConfigs,
-  };
-}
-
-function getPackageFileConfig(repoConfig, index) {
-  let packageFile = repoConfig.packageFiles[index];
-  if (typeof packageFile === 'string') {
-    packageFile = { packageFile };
-  }
-  const packageFileConfig = configParser.mergeChildConfig(
-    repoConfig,
-    packageFile
-  );
-  packageFileConfig.logger = packageFileConfig.logger.child({
-    repository: packageFileConfig.repository,
-    packageFile: packageFileConfig.packageFile,
-  });
-  packageFileConfig.logger.trace(
-    { config: packageFileConfig },
-    'packageFileConfig'
-  );
-  return configParser.filterConfig(packageFileConfig, 'packageFile');
-}
diff --git a/lib/workers/repository/write.js b/lib/workers/repository/write.js
new file mode 100644
index 0000000000000000000000000000000000000000..6c6f902f8497248012304eca1e7d5da7cb277582
--- /dev/null
+++ b/lib/workers/repository/write.js
@@ -0,0 +1,30 @@
+const tmp = require('tmp-promise');
+
+const branchWorker = require('../branch');
+
+module.exports = {
+  writeUpdates,
+};
+
+async function writeUpdates(config) {
+  const { logger } = config;
+  let { branches } = config;
+  logger.info(`Processing ${branches.length} branch(es)`);
+  if (branches.some(upg => upg.isPin)) {
+    branches = branches.filter(upg => upg.isPin);
+    logger.info(`Processing ${branches.length} "pin" PRs first`);
+  }
+  const tmpDir = await tmp.dir({ unsafeCleanup: true });
+  try {
+    for (const branch of branches) {
+      const res = await branchWorker.processBranch({ ...branch, tmpDir });
+      if (res === 'automerged') {
+        // Stop procesing other branches because base branch has been changed by an automerge
+        return 'automerged';
+      }
+    }
+    return 'done';
+  } finally {
+    tmpDir.cleanup();
+  }
+}
diff --git a/test/config/decrypt.spec.js b/test/config/decrypt.spec.js
index 62543105a9ef3ac00ccdde699be01f43b870237a..48078be5442ca24b12f2ac01190aae9ce0398a19 100644
--- a/test/config/decrypt.spec.js
+++ b/test/config/decrypt.spec.js
@@ -48,6 +48,7 @@ describe('config/decrypt', () => {
             },
           },
         },
+        'backend/package.json',
       ];
       const res = decryptConfig(config, logger, privateKey);
       expect(res.encrypted).not.toBeDefined();
diff --git a/test/manager/__snapshots__/resolve.spec.js.snap b/test/manager/__snapshots__/resolve.spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..b2bbf155e6bd7e9d9a2b5b4f8711da57f91f30f0
--- /dev/null
+++ b/test/manager/__snapshots__/resolve.spec.js.snap
@@ -0,0 +1,3477 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`manager/resolve resolvePackageFiles() deetect package.json and warns if cannot parse 1`] = `
+Object {
+  "api": Object {
+    "addAssignees": [Function],
+    "addComment": [Function],
+    "addReviewers": [Function],
+    "branchExists": [Function],
+    "commitFilesToBranch": [Function],
+    "createPr": [Function],
+    "deleteBranch": [Function],
+    "deleteComment": [Function],
+    "editComment": [Function],
+    "ensureComment": [Function],
+    "ensureCommentRemoval": [Function],
+    "findPr": [Function],
+    "getAllRenovateBranches": [Function],
+    "getBranchCommit": [Function],
+    "getBranchLastCommitTime": [Function],
+    "getBranchPr": [Function],
+    "getBranchStatus": [Function],
+    "getBranchStatusCheck": [Function],
+    "getComments": [Function],
+    "getCommitDetails": [Function],
+    "getCommitMessages": [Function],
+    "getFile": [Function],
+    "getFileContent": [Function],
+    "getFileJson": [Function],
+    "getFileList": [Function],
+    "getPr": [Function],
+    "getPrList": [Function],
+    "getRepos": [Function],
+    "getSubDirectories": [Function],
+    "initRepo": [Function],
+    "isBranchStale": [Function],
+    "mergeBranch": [Function],
+    "mergePr": [Function],
+    "setBaseBranch": [Function],
+    "setBranchStatus": [Function],
+    "updatePr": [Function],
+  },
+  "assignees": Array [],
+  "autodiscover": false,
+  "automerge": false,
+  "automergeType": "pr",
+  "baseBranch": null,
+  "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.x",
+  "branchPrefix": "renovate/",
+  "commitMessage": "Update dependency {{depName}} to v{{newVersion}}",
+  "dependencies": Object {
+    "semanticPrefix": "fix(deps):",
+  },
+  "description": Array [],
+  "devDependencies": Object {
+    "pin": Object {
+      "group": Object {
+        "commitMessage": "Pin devDependencies",
+        "prTitle": "Pin devDependencies",
+      },
+    },
+  },
+  "digest": Object {
+    "semanticPrefix": "refactor(deps):",
+  },
+  "docker": Object {
+    "branchName": "{{branchPrefix}}docker-{{depNameSanitized}}-{{newVersionMajor}}.x",
+    "commitMessage": "Update {{depName}} to tag {{newTag}}",
+    "digest": Object {
+      "branchName": "{{branchPrefix}}docker-{{depNameSanitized}}-{{currentTag}}",
+      "commitMessage": "Update {{depName}}:{{currentTag}} digest",
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Docker base image \`{{depName}}@{{currentTag}}\` to the latest digest (\`{{newDigest}}\`).
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Update Dockerfile {{depName}} image {{currentTag}} digest ({{newDigestShort}})",
+    },
+    "enabled": true,
+    "group": Object {
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Dockerfiles to use image digests.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: \`{{upgrade.newDigest}}\`
+{{/each}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Update Docker {{groupName}} digests",
+    },
+    "major": Object {
+      "enabled": false,
+    },
+    "minor": Object {
+      "enabled": false,
+    },
+    "patch": Object {
+      "enabled": false,
+    },
+    "pin": Object {
+      "branchName": "{{branchPrefix}}docker-pin-{{depNameSanitized}}-{{currentTag}}",
+      "group": Object {
+        "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Dockerfiles to use image digests.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: \`{{upgrade.newDigest}}\`
+{{/each}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+        "prTitle": "Pin Docker digests",
+      },
+      "groupName": "Pin Docker Digests",
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Docker base image \`{{depName}}@{{currentTag}}\` to use a digest (\`{{newDigest}}\`).
+This digest will then be kept updated via Pull Requests whenever the image is updated on the Docker registry.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Pin Dockerfile {{depName}}@{{currentTag}} image digest",
+    },
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Docker base image \`{{depName}}\` from tag \`{{currentTag}}\` to new tag \`{{newTag}}\`.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Update {{depName}} Dockerfile tag to {{#if isMajor}}v{{newVersionMajor}}{{else}}v{{newTag}}{{/if}}",
+  },
+  "enabled": true,
+  "encrypted": null,
+  "endpoint": null,
+  "errors": Array [],
+  "excludePackageNames": Array [],
+  "excludePackagePatterns": Array [],
+  "extends": Array [],
+  "group": Object {
+    "branchName": "{{branchPrefix}}{{groupSlug}}",
+    "commitMessage": "Renovate {{groupName}} packages",
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request renovates the package group \\"{{groupName}}\\".
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: from \`{{upgrade.currentVersion}}\` to \`{{upgrade.newVersion}}\`
+{{/each}}
+
+{{#unless isPin}}
+### Commits
+
+{{#each upgrades as |upgrade|}}
+{{#if upgrade.releases.length}}
+<details>
+<summary>{{upgrade.githubName}}</summary>
+{{#each upgrade.releases as |release|}}
+
+#### {{release.version}}
+{{#each release.commits as |commit|}}
+-   [\`{{commit.shortSha}}\`]({{commit.url}}){{commit.message}}
+{{/each}}
+{{/each}}
+
+</details>
+{{/if}}
+{{/each}}
+{{/unless}}
+<br />
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Renovate {{groupName}} packages",
+    "recreateClosed": true,
+  },
+  "groupName": null,
+  "groupSlug": null,
+  "ignoreDeps": Array [],
+  "ignoreFuture": true,
+  "ignoreNpmrcFile": false,
+  "ignorePaths": Array [
+    "**/node_modules/**",
+  ],
+  "ignoreUnstable": true,
+  "labels": Array [],
+  "lazyGrouping": true,
+  "lockFileMaintenance": Object {
+    "branchName": "{{branchPrefix}}lock-file-maintenance",
+    "commitMessage": "Update lock file",
+    "enabled": false,
+    "groupName": null,
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates \`package.json\` lock files to use the latest dependency versions.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Lock file maintenance",
+    "recreateClosed": true,
+    "schedule": Array [
+      "before 5am on monday",
+    ],
+  },
+  "logFile": null,
+  "logFileLevel": "debug",
+  "logLevel": "info",
+  "logger": Object {
+    "child": [Function],
+    "debug": [Function],
+    "error": [Function],
+    "fatal": [Function],
+    "info": [Function],
+    "trace": [Function],
+    "warn": [Function],
+  },
+  "major": Object {},
+  "meteor": Object {
+    "enabled": true,
+  },
+  "minor": Object {},
+  "monorepoPackages": Array [],
+  "npm": Object {
+    "enabled": true,
+    "pin": Object {
+      "automerge": true,
+    },
+  },
+  "npmrc": null,
+  "onboarding": true,
+  "optionalDependencies": Object {},
+  "packageFiles": Array [],
+  "packageNames": Array [],
+  "packagePatterns": Array [],
+  "packageRules": Array [],
+  "patch": Object {
+    "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.{{newVersionMinor}}.x",
+  },
+  "peerDependencies": Object {
+    "enabled": false,
+  },
+  "pin": Object {
+    "group": Object {
+      "commitMessage": "Pin Dependencies",
+      "prTitle": "{{groupName}}",
+      "semanticPrefix": "refactor(deps):",
+    },
+    "groupName": "Pin Dependencies",
+    "rebaseStalePrs": true,
+    "recreateClosed": true,
+    "unpublishSafe": false,
+  },
+  "pinDigests": true,
+  "pinVersions": null,
+  "platform": "github",
+  "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request {{#if isRollback}}rolls back{{else}}updates{{/if}} dependency {{#if repositoryUrl}}[{{depName}}]({{repositoryUrl}}){{else}}\`{{depName}}\`{{/if}} from \`{{#unless isRange}}{{#unless isPin}}v{{/unless}}{{/unless}}{{currentVersion}}\` to \`{{#unless isRange}}v{{/unless}}{{newVersion}}\`{{#if isRollback}}. This is necessary and important because \`v{{currentVersion}}\` cannot be found in the npm registry - probably because of it being unpublished.{{/if}}
+{{#if releases.length}}
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+### Commits
+
+<details>
+<summary>{{githubName}}</summary>
+
+{{#each releases as |release|}}
+#### {{release.version}}
+{{#each release.commits as |commit|}}
+-   [\`{{commit.shortSha}}\`]({{commit.url}}) {{commit.message}}
+{{/each}}
+{{/each}}
+
+</details>
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+  "prCreation": "immediate",
+  "prNotPendingHours": 12,
+  "prTitle": "{{#if isPin}}Pin{{else}}{{#if isRollback}}Roll back{{else}}Update{{/if}}{{/if}} dependency {{depName}} to {{#if isRange}}{{newVersion}}{{else}}{{#if isMajor}}v{{newVersionMajor}}{{else}}v{{newVersion}}{{/if}}{{/if}}",
+  "privateKey": null,
+  "rebaseStalePrs": false,
+  "recreateClosed": false,
+  "renovateFork": false,
+  "repositories": Array [],
+  "requiredStatusChecks": Array [],
+  "respectLatest": true,
+  "reviewers": Array [],
+  "schedule": Array [],
+  "semanticCommits": null,
+  "semanticPrefix": "chore(deps):",
+  "separateMajorReleases": true,
+  "separatePatchReleases": false,
+  "timezone": null,
+  "token": null,
+  "unpublishSafe": false,
+  "updateNotScheduled": true,
+  "warnings": Array [
+    Object {
+      "depName": "package.json",
+      "message": "Cannot parse package.json (invalid JSON)",
+    },
+  ],
+  "workspaceDir": undefined,
+  "yarnrc": null,
+}
+`;
+
+exports[`manager/resolve resolvePackageFiles() detects meteor and docker 1`] = `
+Object {
+  "api": Object {
+    "addAssignees": [Function],
+    "addComment": [Function],
+    "addReviewers": [Function],
+    "branchExists": [Function],
+    "commitFilesToBranch": [Function],
+    "createPr": [Function],
+    "deleteBranch": [Function],
+    "deleteComment": [Function],
+    "editComment": [Function],
+    "ensureComment": [Function],
+    "ensureCommentRemoval": [Function],
+    "findPr": [Function],
+    "getAllRenovateBranches": [Function],
+    "getBranchCommit": [Function],
+    "getBranchLastCommitTime": [Function],
+    "getBranchPr": [Function],
+    "getBranchStatus": [Function],
+    "getBranchStatusCheck": [Function],
+    "getComments": [Function],
+    "getCommitDetails": [Function],
+    "getCommitMessages": [Function],
+    "getFile": [Function],
+    "getFileContent": [Function],
+    "getFileJson": [Function],
+    "getFileList": [Function],
+    "getPr": [Function],
+    "getPrList": [Function],
+    "getRepos": [Function],
+    "getSubDirectories": [Function],
+    "initRepo": [Function],
+    "isBranchStale": [Function],
+    "mergeBranch": [Function],
+    "mergePr": [Function],
+    "setBaseBranch": [Function],
+    "setBranchStatus": [Function],
+    "updatePr": [Function],
+  },
+  "assignees": Array [],
+  "autodiscover": false,
+  "automerge": false,
+  "automergeType": "pr",
+  "baseBranch": null,
+  "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.x",
+  "branchPrefix": "renovate/",
+  "commitMessage": "Update dependency {{depName}} to v{{newVersion}}",
+  "dependencies": Object {
+    "semanticPrefix": "fix(deps):",
+  },
+  "description": Array [],
+  "devDependencies": Object {
+    "pin": Object {
+      "group": Object {
+        "commitMessage": "Pin devDependencies",
+        "prTitle": "Pin devDependencies",
+      },
+    },
+  },
+  "digest": Object {
+    "semanticPrefix": "refactor(deps):",
+  },
+  "docker": Object {
+    "branchName": "{{branchPrefix}}docker-{{depNameSanitized}}-{{newVersionMajor}}.x",
+    "commitMessage": "Update {{depName}} to tag {{newTag}}",
+    "digest": Object {
+      "branchName": "{{branchPrefix}}docker-{{depNameSanitized}}-{{currentTag}}",
+      "commitMessage": "Update {{depName}}:{{currentTag}} digest",
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Docker base image \`{{depName}}@{{currentTag}}\` to the latest digest (\`{{newDigest}}\`).
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Update Dockerfile {{depName}} image {{currentTag}} digest ({{newDigestShort}})",
+    },
+    "enabled": true,
+    "group": Object {
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Dockerfiles to use image digests.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: \`{{upgrade.newDigest}}\`
+{{/each}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Update Docker {{groupName}} digests",
+    },
+    "major": Object {
+      "enabled": false,
+    },
+    "minor": Object {
+      "enabled": false,
+    },
+    "patch": Object {
+      "enabled": false,
+    },
+    "pin": Object {
+      "branchName": "{{branchPrefix}}docker-pin-{{depNameSanitized}}-{{currentTag}}",
+      "group": Object {
+        "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Dockerfiles to use image digests.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: \`{{upgrade.newDigest}}\`
+{{/each}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+        "prTitle": "Pin Docker digests",
+      },
+      "groupName": "Pin Docker Digests",
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Docker base image \`{{depName}}@{{currentTag}}\` to use a digest (\`{{newDigest}}\`).
+This digest will then be kept updated via Pull Requests whenever the image is updated on the Docker registry.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Pin Dockerfile {{depName}}@{{currentTag}} image digest",
+    },
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Docker base image \`{{depName}}\` from tag \`{{currentTag}}\` to new tag \`{{newTag}}\`.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Update {{depName}} Dockerfile tag to {{#if isMajor}}v{{newVersionMajor}}{{else}}v{{newTag}}{{/if}}",
+  },
+  "enabled": true,
+  "encrypted": null,
+  "endpoint": null,
+  "errors": Array [],
+  "excludePackageNames": Array [],
+  "excludePackagePatterns": Array [],
+  "extends": Array [],
+  "group": Object {
+    "branchName": "{{branchPrefix}}{{groupSlug}}",
+    "commitMessage": "Renovate {{groupName}} packages",
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request renovates the package group \\"{{groupName}}\\".
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: from \`{{upgrade.currentVersion}}\` to \`{{upgrade.newVersion}}\`
+{{/each}}
+
+{{#unless isPin}}
+### Commits
+
+{{#each upgrades as |upgrade|}}
+{{#if upgrade.releases.length}}
+<details>
+<summary>{{upgrade.githubName}}</summary>
+{{#each upgrade.releases as |release|}}
+
+#### {{release.version}}
+{{#each release.commits as |commit|}}
+-   [\`{{commit.shortSha}}\`]({{commit.url}}){{commit.message}}
+{{/each}}
+{{/each}}
+
+</details>
+{{/if}}
+{{/each}}
+{{/unless}}
+<br />
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Renovate {{groupName}} packages",
+    "recreateClosed": true,
+  },
+  "groupName": null,
+  "groupSlug": null,
+  "ignoreDeps": Array [],
+  "ignoreFuture": true,
+  "ignoreNpmrcFile": false,
+  "ignorePaths": Array [
+    "**/node_modules/**",
+  ],
+  "ignoreUnstable": true,
+  "labels": Array [],
+  "lazyGrouping": true,
+  "lockFileMaintenance": Object {
+    "branchName": "{{branchPrefix}}lock-file-maintenance",
+    "commitMessage": "Update lock file",
+    "enabled": false,
+    "groupName": null,
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates \`package.json\` lock files to use the latest dependency versions.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Lock file maintenance",
+    "recreateClosed": true,
+    "schedule": Array [
+      "before 5am on monday",
+    ],
+  },
+  "logFile": null,
+  "logFileLevel": "debug",
+  "logLevel": "info",
+  "logger": Object {
+    "child": [Function],
+    "debug": [Function],
+    "error": [Function],
+    "fatal": [Function],
+    "info": [Function],
+    "trace": [Function],
+    "warn": [Function],
+  },
+  "major": Object {},
+  "meteor": Object {
+    "enabled": true,
+  },
+  "minor": Object {},
+  "monorepoPackages": Array [],
+  "npm": Object {
+    "enabled": true,
+    "pin": Object {
+      "automerge": true,
+    },
+  },
+  "npmrc": null,
+  "onboarding": true,
+  "optionalDependencies": Object {},
+  "packageFiles": Array [
+    Object {
+      "enabled": true,
+      "packageFile": "package.js",
+    },
+    Object {
+      "branchName": "{{branchPrefix}}docker-{{depNameSanitized}}-{{newVersionMajor}}.x",
+      "commitMessage": "Update {{depName}} to tag {{newTag}}",
+      "content": "# comment
+FROM node:8
+",
+      "currentFrom": "node:8",
+      "digest": Object {
+        "branchName": "{{branchPrefix}}docker-{{depNameSanitized}}-{{currentTag}}",
+        "commitMessage": "Update {{depName}}:{{currentTag}} digest",
+        "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Docker base image \`{{depName}}@{{currentTag}}\` to the latest digest (\`{{newDigest}}\`).
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+        "prTitle": "Update Dockerfile {{depName}} image {{currentTag}} digest ({{newDigestShort}})",
+      },
+      "enabled": true,
+      "group": Object {
+        "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Dockerfiles to use image digests.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: \`{{upgrade.newDigest}}\`
+{{/each}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+        "prTitle": "Update Docker {{groupName}} digests",
+      },
+      "major": Object {
+        "enabled": false,
+      },
+      "minor": Object {
+        "enabled": false,
+      },
+      "packageFile": "Dockerfile",
+      "patch": Object {
+        "enabled": false,
+      },
+      "pin": Object {
+        "branchName": "{{branchPrefix}}docker-pin-{{depNameSanitized}}-{{currentTag}}",
+        "group": Object {
+          "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Dockerfiles to use image digests.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: \`{{upgrade.newDigest}}\`
+{{/each}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+          "prTitle": "Pin Docker digests",
+        },
+        "groupName": "Pin Docker Digests",
+        "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Docker base image \`{{depName}}@{{currentTag}}\` to use a digest (\`{{newDigest}}\`).
+This digest will then be kept updated via Pull Requests whenever the image is updated on the Docker registry.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+        "prTitle": "Pin Dockerfile {{depName}}@{{currentTag}} image digest",
+      },
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Docker base image \`{{depName}}\` from tag \`{{currentTag}}\` to new tag \`{{newTag}}\`.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Update {{depName}} Dockerfile tag to {{#if isMajor}}v{{newVersionMajor}}{{else}}v{{newTag}}{{/if}}",
+    },
+  ],
+  "packageNames": Array [],
+  "packagePatterns": Array [],
+  "packageRules": Array [],
+  "patch": Object {
+    "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.{{newVersionMinor}}.x",
+  },
+  "peerDependencies": Object {
+    "enabled": false,
+  },
+  "pin": Object {
+    "group": Object {
+      "commitMessage": "Pin Dependencies",
+      "prTitle": "{{groupName}}",
+      "semanticPrefix": "refactor(deps):",
+    },
+    "groupName": "Pin Dependencies",
+    "rebaseStalePrs": true,
+    "recreateClosed": true,
+    "unpublishSafe": false,
+  },
+  "pinDigests": true,
+  "pinVersions": null,
+  "platform": "github",
+  "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request {{#if isRollback}}rolls back{{else}}updates{{/if}} dependency {{#if repositoryUrl}}[{{depName}}]({{repositoryUrl}}){{else}}\`{{depName}}\`{{/if}} from \`{{#unless isRange}}{{#unless isPin}}v{{/unless}}{{/unless}}{{currentVersion}}\` to \`{{#unless isRange}}v{{/unless}}{{newVersion}}\`{{#if isRollback}}. This is necessary and important because \`v{{currentVersion}}\` cannot be found in the npm registry - probably because of it being unpublished.{{/if}}
+{{#if releases.length}}
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+### Commits
+
+<details>
+<summary>{{githubName}}</summary>
+
+{{#each releases as |release|}}
+#### {{release.version}}
+{{#each release.commits as |commit|}}
+-   [\`{{commit.shortSha}}\`]({{commit.url}}) {{commit.message}}
+{{/each}}
+{{/each}}
+
+</details>
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+  "prCreation": "immediate",
+  "prNotPendingHours": 12,
+  "prTitle": "{{#if isPin}}Pin{{else}}{{#if isRollback}}Roll back{{else}}Update{{/if}}{{/if}} dependency {{depName}} to {{#if isRange}}{{newVersion}}{{else}}{{#if isMajor}}v{{newVersionMajor}}{{else}}v{{newVersion}}{{/if}}{{/if}}",
+  "privateKey": null,
+  "rebaseStalePrs": false,
+  "recreateClosed": false,
+  "renovateFork": false,
+  "repositories": Array [],
+  "requiredStatusChecks": Array [],
+  "respectLatest": true,
+  "reviewers": Array [],
+  "schedule": Array [],
+  "semanticCommits": null,
+  "semanticPrefix": "chore(deps):",
+  "separateMajorReleases": true,
+  "separatePatchReleases": false,
+  "timezone": null,
+  "token": null,
+  "unpublishSafe": false,
+  "updateNotScheduled": true,
+  "warnings": Array [],
+  "workspaceDir": undefined,
+  "yarnrc": null,
+}
+`;
+
+exports[`manager/resolve resolvePackageFiles() detects package.json and parses json with renovate config 1`] = `
+Object {
+  "api": Object {
+    "addAssignees": [Function],
+    "addComment": [Function],
+    "addReviewers": [Function],
+    "branchExists": [Function],
+    "commitFilesToBranch": [Function],
+    "createPr": [Function],
+    "deleteBranch": [Function],
+    "deleteComment": [Function],
+    "editComment": [Function],
+    "ensureComment": [Function],
+    "ensureCommentRemoval": [Function],
+    "findPr": [Function],
+    "getAllRenovateBranches": [Function],
+    "getBranchCommit": [Function],
+    "getBranchLastCommitTime": [Function],
+    "getBranchPr": [Function],
+    "getBranchStatus": [Function],
+    "getBranchStatusCheck": [Function],
+    "getComments": [Function],
+    "getCommitDetails": [Function],
+    "getCommitMessages": [Function],
+    "getFile": [Function],
+    "getFileContent": [Function],
+    "getFileJson": [Function],
+    "getFileList": [Function],
+    "getPr": [Function],
+    "getPrList": [Function],
+    "getRepos": [Function],
+    "getSubDirectories": [Function],
+    "initRepo": [Function],
+    "isBranchStale": [Function],
+    "mergeBranch": [Function],
+    "mergePr": [Function],
+    "setBaseBranch": [Function],
+    "setBranchStatus": [Function],
+    "updatePr": [Function],
+  },
+  "assignees": Array [],
+  "autodiscover": false,
+  "automerge": false,
+  "automergeType": "pr",
+  "baseBranch": null,
+  "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.x",
+  "branchPrefix": "renovate/",
+  "commitMessage": "Update dependency {{depName}} to v{{newVersion}}",
+  "dependencies": Object {
+    "semanticPrefix": "fix(deps):",
+  },
+  "description": Array [],
+  "devDependencies": Object {
+    "pin": Object {
+      "group": Object {
+        "commitMessage": "Pin devDependencies",
+        "prTitle": "Pin devDependencies",
+      },
+    },
+  },
+  "digest": Object {
+    "semanticPrefix": "refactor(deps):",
+  },
+  "docker": Object {
+    "branchName": "{{branchPrefix}}docker-{{depNameSanitized}}-{{newVersionMajor}}.x",
+    "commitMessage": "Update {{depName}} to tag {{newTag}}",
+    "digest": Object {
+      "branchName": "{{branchPrefix}}docker-{{depNameSanitized}}-{{currentTag}}",
+      "commitMessage": "Update {{depName}}:{{currentTag}} digest",
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Docker base image \`{{depName}}@{{currentTag}}\` to the latest digest (\`{{newDigest}}\`).
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Update Dockerfile {{depName}} image {{currentTag}} digest ({{newDigestShort}})",
+    },
+    "enabled": true,
+    "group": Object {
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Dockerfiles to use image digests.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: \`{{upgrade.newDigest}}\`
+{{/each}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Update Docker {{groupName}} digests",
+    },
+    "major": Object {
+      "enabled": false,
+    },
+    "minor": Object {
+      "enabled": false,
+    },
+    "patch": Object {
+      "enabled": false,
+    },
+    "pin": Object {
+      "branchName": "{{branchPrefix}}docker-pin-{{depNameSanitized}}-{{currentTag}}",
+      "group": Object {
+        "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Dockerfiles to use image digests.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: \`{{upgrade.newDigest}}\`
+{{/each}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+        "prTitle": "Pin Docker digests",
+      },
+      "groupName": "Pin Docker Digests",
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Docker base image \`{{depName}}@{{currentTag}}\` to use a digest (\`{{newDigest}}\`).
+This digest will then be kept updated via Pull Requests whenever the image is updated on the Docker registry.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Pin Dockerfile {{depName}}@{{currentTag}} image digest",
+    },
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Docker base image \`{{depName}}\` from tag \`{{currentTag}}\` to new tag \`{{newTag}}\`.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Update {{depName}} Dockerfile tag to {{#if isMajor}}v{{newVersionMajor}}{{else}}v{{newTag}}{{/if}}",
+  },
+  "enabled": true,
+  "encrypted": null,
+  "endpoint": null,
+  "errors": Array [],
+  "excludePackageNames": Array [],
+  "excludePackagePatterns": Array [],
+  "extends": Array [],
+  "group": Object {
+    "branchName": "{{branchPrefix}}{{groupSlug}}",
+    "commitMessage": "Renovate {{groupName}} packages",
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request renovates the package group \\"{{groupName}}\\".
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: from \`{{upgrade.currentVersion}}\` to \`{{upgrade.newVersion}}\`
+{{/each}}
+
+{{#unless isPin}}
+### Commits
+
+{{#each upgrades as |upgrade|}}
+{{#if upgrade.releases.length}}
+<details>
+<summary>{{upgrade.githubName}}</summary>
+{{#each upgrade.releases as |release|}}
+
+#### {{release.version}}
+{{#each release.commits as |commit|}}
+-   [\`{{commit.shortSha}}\`]({{commit.url}}){{commit.message}}
+{{/each}}
+{{/each}}
+
+</details>
+{{/if}}
+{{/each}}
+{{/unless}}
+<br />
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Renovate {{groupName}} packages",
+    "recreateClosed": true,
+  },
+  "groupName": null,
+  "groupSlug": null,
+  "ignoreDeps": Array [],
+  "ignoreFuture": true,
+  "ignoreNpmrcFile": false,
+  "ignorePaths": Array [
+    "**/node_modules/**",
+  ],
+  "ignoreUnstable": true,
+  "labels": Array [],
+  "lazyGrouping": true,
+  "lockFileMaintenance": Object {
+    "branchName": "{{branchPrefix}}lock-file-maintenance",
+    "commitMessage": "Update lock file",
+    "enabled": false,
+    "groupName": null,
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates \`package.json\` lock files to use the latest dependency versions.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Lock file maintenance",
+    "recreateClosed": true,
+    "schedule": Array [
+      "before 5am on monday",
+    ],
+  },
+  "logFile": null,
+  "logFileLevel": "debug",
+  "logLevel": "info",
+  "logger": Object {
+    "child": [Function],
+    "debug": [Function],
+    "error": [Function],
+    "fatal": [Function],
+    "info": [Function],
+    "trace": [Function],
+    "warn": [Function],
+  },
+  "major": Object {},
+  "meteor": Object {
+    "enabled": true,
+  },
+  "minor": Object {},
+  "monorepoPackages": Array [],
+  "npm": Object {
+    "enabled": true,
+    "pin": Object {
+      "automerge": true,
+    },
+  },
+  "npmrc": null,
+  "onboarding": true,
+  "optionalDependencies": Object {},
+  "packageFiles": Array [
+    Object {
+      "automerge": true,
+      "content": Object {},
+      "errors": Array [],
+      "packageFile": "package.json",
+      "packageLock": undefined,
+      "warnings": Array [],
+      "yarnLock": undefined,
+    },
+  ],
+  "packageNames": Array [],
+  "packagePatterns": Array [],
+  "packageRules": Array [],
+  "patch": Object {
+    "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.{{newVersionMinor}}.x",
+  },
+  "peerDependencies": Object {
+    "enabled": false,
+  },
+  "pin": Object {
+    "group": Object {
+      "commitMessage": "Pin Dependencies",
+      "prTitle": "{{groupName}}",
+      "semanticPrefix": "refactor(deps):",
+    },
+    "groupName": "Pin Dependencies",
+    "rebaseStalePrs": true,
+    "recreateClosed": true,
+    "unpublishSafe": false,
+  },
+  "pinDigests": true,
+  "pinVersions": null,
+  "platform": "github",
+  "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request {{#if isRollback}}rolls back{{else}}updates{{/if}} dependency {{#if repositoryUrl}}[{{depName}}]({{repositoryUrl}}){{else}}\`{{depName}}\`{{/if}} from \`{{#unless isRange}}{{#unless isPin}}v{{/unless}}{{/unless}}{{currentVersion}}\` to \`{{#unless isRange}}v{{/unless}}{{newVersion}}\`{{#if isRollback}}. This is necessary and important because \`v{{currentVersion}}\` cannot be found in the npm registry - probably because of it being unpublished.{{/if}}
+{{#if releases.length}}
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+### Commits
+
+<details>
+<summary>{{githubName}}</summary>
+
+{{#each releases as |release|}}
+#### {{release.version}}
+{{#each release.commits as |commit|}}
+-   [\`{{commit.shortSha}}\`]({{commit.url}}) {{commit.message}}
+{{/each}}
+{{/each}}
+
+</details>
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+  "prCreation": "immediate",
+  "prNotPendingHours": 12,
+  "prTitle": "{{#if isPin}}Pin{{else}}{{#if isRollback}}Roll back{{else}}Update{{/if}}{{/if}} dependency {{depName}} to {{#if isRange}}{{newVersion}}{{else}}{{#if isMajor}}v{{newVersionMajor}}{{else}}v{{newVersion}}{{/if}}{{/if}}",
+  "privateKey": null,
+  "rebaseStalePrs": false,
+  "recreateClosed": false,
+  "renovateFork": false,
+  "repositories": Array [],
+  "requiredStatusChecks": Array [],
+  "respectLatest": true,
+  "reviewers": Array [],
+  "schedule": Array [],
+  "semanticCommits": null,
+  "semanticPrefix": "chore(deps):",
+  "separateMajorReleases": true,
+  "separatePatchReleases": false,
+  "timezone": null,
+  "token": null,
+  "unpublishSafe": false,
+  "updateNotScheduled": true,
+  "warnings": Array [],
+  "workspaceDir": undefined,
+  "yarnrc": null,
+}
+`;
+
+exports[`manager/resolve resolvePackageFiles() downloads accompanying files 1`] = `
+Object {
+  "api": Object {
+    "addAssignees": [Function],
+    "addComment": [Function],
+    "addReviewers": [Function],
+    "branchExists": [Function],
+    "commitFilesToBranch": [Function],
+    "createPr": [Function],
+    "deleteBranch": [Function],
+    "deleteComment": [Function],
+    "editComment": [Function],
+    "ensureComment": [Function],
+    "ensureCommentRemoval": [Function],
+    "findPr": [Function],
+    "getAllRenovateBranches": [Function],
+    "getBranchCommit": [Function],
+    "getBranchLastCommitTime": [Function],
+    "getBranchPr": [Function],
+    "getBranchStatus": [Function],
+    "getBranchStatusCheck": [Function],
+    "getComments": [Function],
+    "getCommitDetails": [Function],
+    "getCommitMessages": [Function],
+    "getFile": [Function],
+    "getFileContent": [Function],
+    "getFileJson": [Function],
+    "getFileList": [Function],
+    "getPr": [Function],
+    "getPrList": [Function],
+    "getRepos": [Function],
+    "getSubDirectories": [Function],
+    "initRepo": [Function],
+    "isBranchStale": [Function],
+    "mergeBranch": [Function],
+    "mergePr": [Function],
+    "setBaseBranch": [Function],
+    "setBranchStatus": [Function],
+    "updatePr": [Function],
+  },
+  "assignees": Array [],
+  "autodiscover": false,
+  "automerge": false,
+  "automergeType": "pr",
+  "baseBranch": null,
+  "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.x",
+  "branchPrefix": "renovate/",
+  "commitMessage": "Update dependency {{depName}} to v{{newVersion}}",
+  "dependencies": Object {
+    "semanticPrefix": "fix(deps):",
+  },
+  "description": Array [],
+  "devDependencies": Object {
+    "pin": Object {
+      "group": Object {
+        "commitMessage": "Pin devDependencies",
+        "prTitle": "Pin devDependencies",
+      },
+    },
+  },
+  "digest": Object {
+    "semanticPrefix": "refactor(deps):",
+  },
+  "docker": Object {
+    "branchName": "{{branchPrefix}}docker-{{depNameSanitized}}-{{newVersionMajor}}.x",
+    "commitMessage": "Update {{depName}} to tag {{newTag}}",
+    "digest": Object {
+      "branchName": "{{branchPrefix}}docker-{{depNameSanitized}}-{{currentTag}}",
+      "commitMessage": "Update {{depName}}:{{currentTag}} digest",
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Docker base image \`{{depName}}@{{currentTag}}\` to the latest digest (\`{{newDigest}}\`).
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Update Dockerfile {{depName}} image {{currentTag}} digest ({{newDigestShort}})",
+    },
+    "enabled": true,
+    "group": Object {
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Dockerfiles to use image digests.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: \`{{upgrade.newDigest}}\`
+{{/each}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Update Docker {{groupName}} digests",
+    },
+    "major": Object {
+      "enabled": false,
+    },
+    "minor": Object {
+      "enabled": false,
+    },
+    "patch": Object {
+      "enabled": false,
+    },
+    "pin": Object {
+      "branchName": "{{branchPrefix}}docker-pin-{{depNameSanitized}}-{{currentTag}}",
+      "group": Object {
+        "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Dockerfiles to use image digests.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: \`{{upgrade.newDigest}}\`
+{{/each}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+        "prTitle": "Pin Docker digests",
+      },
+      "groupName": "Pin Docker Digests",
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Docker base image \`{{depName}}@{{currentTag}}\` to use a digest (\`{{newDigest}}\`).
+This digest will then be kept updated via Pull Requests whenever the image is updated on the Docker registry.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Pin Dockerfile {{depName}}@{{currentTag}} image digest",
+    },
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Docker base image \`{{depName}}\` from tag \`{{currentTag}}\` to new tag \`{{newTag}}\`.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Update {{depName}} Dockerfile tag to {{#if isMajor}}v{{newVersionMajor}}{{else}}v{{newTag}}{{/if}}",
+  },
+  "enabled": true,
+  "encrypted": null,
+  "endpoint": null,
+  "errors": Array [],
+  "excludePackageNames": Array [],
+  "excludePackagePatterns": Array [],
+  "extends": Array [],
+  "group": Object {
+    "branchName": "{{branchPrefix}}{{groupSlug}}",
+    "commitMessage": "Renovate {{groupName}} packages",
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request renovates the package group \\"{{groupName}}\\".
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: from \`{{upgrade.currentVersion}}\` to \`{{upgrade.newVersion}}\`
+{{/each}}
+
+{{#unless isPin}}
+### Commits
+
+{{#each upgrades as |upgrade|}}
+{{#if upgrade.releases.length}}
+<details>
+<summary>{{upgrade.githubName}}</summary>
+{{#each upgrade.releases as |release|}}
+
+#### {{release.version}}
+{{#each release.commits as |commit|}}
+-   [\`{{commit.shortSha}}\`]({{commit.url}}){{commit.message}}
+{{/each}}
+{{/each}}
+
+</details>
+{{/if}}
+{{/each}}
+{{/unless}}
+<br />
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Renovate {{groupName}} packages",
+    "recreateClosed": true,
+  },
+  "groupName": null,
+  "groupSlug": null,
+  "ignoreDeps": Array [],
+  "ignoreFuture": true,
+  "ignoreNpmrcFile": false,
+  "ignorePaths": Array [
+    "**/node_modules/**",
+  ],
+  "ignoreUnstable": true,
+  "labels": Array [],
+  "lazyGrouping": true,
+  "lockFileMaintenance": Object {
+    "branchName": "{{branchPrefix}}lock-file-maintenance",
+    "commitMessage": "Update lock file",
+    "enabled": false,
+    "groupName": null,
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates \`package.json\` lock files to use the latest dependency versions.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Lock file maintenance",
+    "recreateClosed": true,
+    "schedule": Array [
+      "before 5am on monday",
+    ],
+  },
+  "logFile": null,
+  "logFileLevel": "debug",
+  "logLevel": "info",
+  "logger": Object {
+    "child": [Function],
+    "debug": [Function],
+    "error": [Function],
+    "fatal": [Function],
+    "info": [Function],
+    "trace": [Function],
+    "warn": [Function],
+  },
+  "major": Object {},
+  "meteor": Object {
+    "enabled": true,
+  },
+  "minor": Object {},
+  "monorepoPackages": Array [],
+  "npm": Object {
+    "enabled": true,
+    "pin": Object {
+      "automerge": true,
+    },
+  },
+  "npmrc": null,
+  "onboarding": true,
+  "optionalDependencies": Object {},
+  "packageFiles": Array [
+    Object {
+      "content": Object {
+        "name": "package.json",
+      },
+      "npmrc": "npmrc",
+      "packageFile": "package.json",
+      "packageLock": "{\\"name\\": \\"packge-lock.json\\"}",
+      "yarnLock": "# yarn.lock",
+      "yarnrc": "yarnrc",
+    },
+  ],
+  "packageNames": Array [],
+  "packagePatterns": Array [],
+  "packageRules": Array [],
+  "patch": Object {
+    "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.{{newVersionMinor}}.x",
+  },
+  "peerDependencies": Object {
+    "enabled": false,
+  },
+  "pin": Object {
+    "group": Object {
+      "commitMessage": "Pin Dependencies",
+      "prTitle": "{{groupName}}",
+      "semanticPrefix": "refactor(deps):",
+    },
+    "groupName": "Pin Dependencies",
+    "rebaseStalePrs": true,
+    "recreateClosed": true,
+    "unpublishSafe": false,
+  },
+  "pinDigests": true,
+  "pinVersions": null,
+  "platform": "github",
+  "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request {{#if isRollback}}rolls back{{else}}updates{{/if}} dependency {{#if repositoryUrl}}[{{depName}}]({{repositoryUrl}}){{else}}\`{{depName}}\`{{/if}} from \`{{#unless isRange}}{{#unless isPin}}v{{/unless}}{{/unless}}{{currentVersion}}\` to \`{{#unless isRange}}v{{/unless}}{{newVersion}}\`{{#if isRollback}}. This is necessary and important because \`v{{currentVersion}}\` cannot be found in the npm registry - probably because of it being unpublished.{{/if}}
+{{#if releases.length}}
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+### Commits
+
+<details>
+<summary>{{githubName}}</summary>
+
+{{#each releases as |release|}}
+#### {{release.version}}
+{{#each release.commits as |commit|}}
+-   [\`{{commit.shortSha}}\`]({{commit.url}}) {{commit.message}}
+{{/each}}
+{{/each}}
+
+</details>
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+  "prCreation": "immediate",
+  "prNotPendingHours": 12,
+  "prTitle": "{{#if isPin}}Pin{{else}}{{#if isRollback}}Roll back{{else}}Update{{/if}}{{/if}} dependency {{depName}} to {{#if isRange}}{{newVersion}}{{else}}{{#if isMajor}}v{{newVersionMajor}}{{else}}v{{newVersion}}{{/if}}{{/if}}",
+  "privateKey": null,
+  "rebaseStalePrs": false,
+  "recreateClosed": false,
+  "renovateFork": false,
+  "repositories": Array [],
+  "requiredStatusChecks": Array [],
+  "respectLatest": true,
+  "reviewers": Array [],
+  "schedule": Array [],
+  "semanticCommits": null,
+  "semanticPrefix": "chore(deps):",
+  "separateMajorReleases": true,
+  "separatePatchReleases": false,
+  "timezone": null,
+  "token": null,
+  "unpublishSafe": false,
+  "updateNotScheduled": true,
+  "warnings": Array [],
+  "workspaceDir": undefined,
+  "yarnrc": null,
+}
+`;
+
+exports[`manager/resolve resolvePackageFiles() skips docker if no content or no match 1`] = `
+Object {
+  "api": Object {
+    "addAssignees": [Function],
+    "addComment": [Function],
+    "addReviewers": [Function],
+    "branchExists": [Function],
+    "commitFilesToBranch": [Function],
+    "createPr": [Function],
+    "deleteBranch": [Function],
+    "deleteComment": [Function],
+    "editComment": [Function],
+    "ensureComment": [Function],
+    "ensureCommentRemoval": [Function],
+    "findPr": [Function],
+    "getAllRenovateBranches": [Function],
+    "getBranchCommit": [Function],
+    "getBranchLastCommitTime": [Function],
+    "getBranchPr": [Function],
+    "getBranchStatus": [Function],
+    "getBranchStatusCheck": [Function],
+    "getComments": [Function],
+    "getCommitDetails": [Function],
+    "getCommitMessages": [Function],
+    "getFile": [Function],
+    "getFileContent": [Function],
+    "getFileJson": [Function],
+    "getFileList": [Function],
+    "getPr": [Function],
+    "getPrList": [Function],
+    "getRepos": [Function],
+    "getSubDirectories": [Function],
+    "initRepo": [Function],
+    "isBranchStale": [Function],
+    "mergeBranch": [Function],
+    "mergePr": [Function],
+    "setBaseBranch": [Function],
+    "setBranchStatus": [Function],
+    "updatePr": [Function],
+  },
+  "assignees": Array [],
+  "autodiscover": false,
+  "automerge": false,
+  "automergeType": "pr",
+  "baseBranch": null,
+  "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.x",
+  "branchPrefix": "renovate/",
+  "commitMessage": "Update dependency {{depName}} to v{{newVersion}}",
+  "dependencies": Object {
+    "semanticPrefix": "fix(deps):",
+  },
+  "description": Array [],
+  "devDependencies": Object {
+    "pin": Object {
+      "group": Object {
+        "commitMessage": "Pin devDependencies",
+        "prTitle": "Pin devDependencies",
+      },
+    },
+  },
+  "digest": Object {
+    "semanticPrefix": "refactor(deps):",
+  },
+  "docker": Object {
+    "branchName": "{{branchPrefix}}docker-{{depNameSanitized}}-{{newVersionMajor}}.x",
+    "commitMessage": "Update {{depName}} to tag {{newTag}}",
+    "digest": Object {
+      "branchName": "{{branchPrefix}}docker-{{depNameSanitized}}-{{currentTag}}",
+      "commitMessage": "Update {{depName}}:{{currentTag}} digest",
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Docker base image \`{{depName}}@{{currentTag}}\` to the latest digest (\`{{newDigest}}\`).
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Update Dockerfile {{depName}} image {{currentTag}} digest ({{newDigestShort}})",
+    },
+    "enabled": true,
+    "group": Object {
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Dockerfiles to use image digests.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: \`{{upgrade.newDigest}}\`
+{{/each}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Update Docker {{groupName}} digests",
+    },
+    "major": Object {
+      "enabled": false,
+    },
+    "minor": Object {
+      "enabled": false,
+    },
+    "patch": Object {
+      "enabled": false,
+    },
+    "pin": Object {
+      "branchName": "{{branchPrefix}}docker-pin-{{depNameSanitized}}-{{currentTag}}",
+      "group": Object {
+        "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Dockerfiles to use image digests.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: \`{{upgrade.newDigest}}\`
+{{/each}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+        "prTitle": "Pin Docker digests",
+      },
+      "groupName": "Pin Docker Digests",
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Docker base image \`{{depName}}@{{currentTag}}\` to use a digest (\`{{newDigest}}\`).
+This digest will then be kept updated via Pull Requests whenever the image is updated on the Docker registry.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Pin Dockerfile {{depName}}@{{currentTag}} image digest",
+    },
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Docker base image \`{{depName}}\` from tag \`{{currentTag}}\` to new tag \`{{newTag}}\`.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Update {{depName}} Dockerfile tag to {{#if isMajor}}v{{newVersionMajor}}{{else}}v{{newTag}}{{/if}}",
+  },
+  "enabled": true,
+  "encrypted": null,
+  "endpoint": null,
+  "errors": Array [],
+  "excludePackageNames": Array [],
+  "excludePackagePatterns": Array [],
+  "extends": Array [],
+  "group": Object {
+    "branchName": "{{branchPrefix}}{{groupSlug}}",
+    "commitMessage": "Renovate {{groupName}} packages",
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request renovates the package group \\"{{groupName}}\\".
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: from \`{{upgrade.currentVersion}}\` to \`{{upgrade.newVersion}}\`
+{{/each}}
+
+{{#unless isPin}}
+### Commits
+
+{{#each upgrades as |upgrade|}}
+{{#if upgrade.releases.length}}
+<details>
+<summary>{{upgrade.githubName}}</summary>
+{{#each upgrade.releases as |release|}}
+
+#### {{release.version}}
+{{#each release.commits as |commit|}}
+-   [\`{{commit.shortSha}}\`]({{commit.url}}){{commit.message}}
+{{/each}}
+{{/each}}
+
+</details>
+{{/if}}
+{{/each}}
+{{/unless}}
+<br />
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Renovate {{groupName}} packages",
+    "recreateClosed": true,
+  },
+  "groupName": null,
+  "groupSlug": null,
+  "ignoreDeps": Array [],
+  "ignoreFuture": true,
+  "ignoreNpmrcFile": false,
+  "ignorePaths": Array [
+    "**/node_modules/**",
+  ],
+  "ignoreUnstable": true,
+  "labels": Array [],
+  "lazyGrouping": true,
+  "lockFileMaintenance": Object {
+    "branchName": "{{branchPrefix}}lock-file-maintenance",
+    "commitMessage": "Update lock file",
+    "enabled": false,
+    "groupName": null,
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates \`package.json\` lock files to use the latest dependency versions.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Lock file maintenance",
+    "recreateClosed": true,
+    "schedule": Array [
+      "before 5am on monday",
+    ],
+  },
+  "logFile": null,
+  "logFileLevel": "debug",
+  "logLevel": "info",
+  "logger": Object {
+    "child": [Function],
+    "debug": [Function],
+    "error": [Function],
+    "fatal": [Function],
+    "info": [Function],
+    "trace": [Function],
+    "warn": [Function],
+  },
+  "major": Object {},
+  "meteor": Object {
+    "enabled": true,
+  },
+  "minor": Object {},
+  "monorepoPackages": Array [],
+  "npm": Object {
+    "enabled": true,
+    "pin": Object {
+      "automerge": true,
+    },
+  },
+  "npmrc": null,
+  "onboarding": true,
+  "optionalDependencies": Object {},
+  "packageFiles": Array [],
+  "packageNames": Array [],
+  "packagePatterns": Array [],
+  "packageRules": Array [],
+  "patch": Object {
+    "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.{{newVersionMinor}}.x",
+  },
+  "peerDependencies": Object {
+    "enabled": false,
+  },
+  "pin": Object {
+    "group": Object {
+      "commitMessage": "Pin Dependencies",
+      "prTitle": "{{groupName}}",
+      "semanticPrefix": "refactor(deps):",
+    },
+    "groupName": "Pin Dependencies",
+    "rebaseStalePrs": true,
+    "recreateClosed": true,
+    "unpublishSafe": false,
+  },
+  "pinDigests": true,
+  "pinVersions": null,
+  "platform": "github",
+  "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request {{#if isRollback}}rolls back{{else}}updates{{/if}} dependency {{#if repositoryUrl}}[{{depName}}]({{repositoryUrl}}){{else}}\`{{depName}}\`{{/if}} from \`{{#unless isRange}}{{#unless isPin}}v{{/unless}}{{/unless}}{{currentVersion}}\` to \`{{#unless isRange}}v{{/unless}}{{newVersion}}\`{{#if isRollback}}. This is necessary and important because \`v{{currentVersion}}\` cannot be found in the npm registry - probably because of it being unpublished.{{/if}}
+{{#if releases.length}}
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+### Commits
+
+<details>
+<summary>{{githubName}}</summary>
+
+{{#each releases as |release|}}
+#### {{release.version}}
+{{#each release.commits as |commit|}}
+-   [\`{{commit.shortSha}}\`]({{commit.url}}) {{commit.message}}
+{{/each}}
+{{/each}}
+
+</details>
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+  "prCreation": "immediate",
+  "prNotPendingHours": 12,
+  "prTitle": "{{#if isPin}}Pin{{else}}{{#if isRollback}}Roll back{{else}}Update{{/if}}{{/if}} dependency {{depName}} to {{#if isRange}}{{newVersion}}{{else}}{{#if isMajor}}v{{newVersionMajor}}{{else}}v{{newVersion}}{{/if}}{{/if}}",
+  "privateKey": null,
+  "rebaseStalePrs": false,
+  "recreateClosed": false,
+  "renovateFork": false,
+  "repositories": Array [],
+  "requiredStatusChecks": Array [],
+  "respectLatest": true,
+  "reviewers": Array [],
+  "schedule": Array [],
+  "semanticCommits": null,
+  "semanticPrefix": "chore(deps):",
+  "separateMajorReleases": true,
+  "separatePatchReleases": false,
+  "timezone": null,
+  "token": null,
+  "unpublishSafe": false,
+  "updateNotScheduled": true,
+  "warnings": Array [],
+  "workspaceDir": undefined,
+  "yarnrc": null,
+}
+`;
+
+exports[`manager/resolve resolvePackageFiles() uses packageFiles if already configured and raises error if not found 1`] = `
+Object {
+  "api": Object {
+    "addAssignees": [Function],
+    "addComment": [Function],
+    "addReviewers": [Function],
+    "branchExists": [Function],
+    "commitFilesToBranch": [Function],
+    "createPr": [Function],
+    "deleteBranch": [Function],
+    "deleteComment": [Function],
+    "editComment": [Function],
+    "ensureComment": [Function],
+    "ensureCommentRemoval": [Function],
+    "findPr": [Function],
+    "getAllRenovateBranches": [Function],
+    "getBranchCommit": [Function],
+    "getBranchLastCommitTime": [Function],
+    "getBranchPr": [Function],
+    "getBranchStatus": [Function],
+    "getBranchStatusCheck": [Function],
+    "getComments": [Function],
+    "getCommitDetails": [Function],
+    "getCommitMessages": [Function],
+    "getFile": [Function],
+    "getFileContent": [Function],
+    "getFileJson": [Function],
+    "getFileList": [Function],
+    "getPr": [Function],
+    "getPrList": [Function],
+    "getRepos": [Function],
+    "getSubDirectories": [Function],
+    "initRepo": [Function],
+    "isBranchStale": [Function],
+    "mergeBranch": [Function],
+    "mergePr": [Function],
+    "setBaseBranch": [Function],
+    "setBranchStatus": [Function],
+    "updatePr": [Function],
+  },
+  "assignees": Array [],
+  "autodiscover": false,
+  "automerge": false,
+  "automergeType": "pr",
+  "baseBranch": null,
+  "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.x",
+  "branchPrefix": "renovate/",
+  "commitMessage": "Update dependency {{depName}} to v{{newVersion}}",
+  "dependencies": Object {
+    "semanticPrefix": "fix(deps):",
+  },
+  "description": Array [],
+  "devDependencies": Object {
+    "pin": Object {
+      "group": Object {
+        "commitMessage": "Pin devDependencies",
+        "prTitle": "Pin devDependencies",
+      },
+    },
+  },
+  "digest": Object {
+    "semanticPrefix": "refactor(deps):",
+  },
+  "docker": Object {
+    "branchName": "{{branchPrefix}}docker-{{depNameSanitized}}-{{newVersionMajor}}.x",
+    "commitMessage": "Update {{depName}} to tag {{newTag}}",
+    "digest": Object {
+      "branchName": "{{branchPrefix}}docker-{{depNameSanitized}}-{{currentTag}}",
+      "commitMessage": "Update {{depName}}:{{currentTag}} digest",
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Docker base image \`{{depName}}@{{currentTag}}\` to the latest digest (\`{{newDigest}}\`).
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Update Dockerfile {{depName}} image {{currentTag}} digest ({{newDigestShort}})",
+    },
+    "enabled": true,
+    "group": Object {
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Dockerfiles to use image digests.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: \`{{upgrade.newDigest}}\`
+{{/each}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Update Docker {{groupName}} digests",
+    },
+    "major": Object {
+      "enabled": false,
+    },
+    "minor": Object {
+      "enabled": false,
+    },
+    "patch": Object {
+      "enabled": false,
+    },
+    "pin": Object {
+      "branchName": "{{branchPrefix}}docker-pin-{{depNameSanitized}}-{{currentTag}}",
+      "group": Object {
+        "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Dockerfiles to use image digests.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: \`{{upgrade.newDigest}}\`
+{{/each}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+        "prTitle": "Pin Docker digests",
+      },
+      "groupName": "Pin Docker Digests",
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Docker base image \`{{depName}}@{{currentTag}}\` to use a digest (\`{{newDigest}}\`).
+This digest will then be kept updated via Pull Requests whenever the image is updated on the Docker registry.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Pin Dockerfile {{depName}}@{{currentTag}} image digest",
+    },
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Docker base image \`{{depName}}\` from tag \`{{currentTag}}\` to new tag \`{{newTag}}\`.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Update {{depName}} Dockerfile tag to {{#if isMajor}}v{{newVersionMajor}}{{else}}v{{newTag}}{{/if}}",
+  },
+  "enabled": true,
+  "encrypted": null,
+  "endpoint": null,
+  "errors": Array [
+    Object {
+      "depName": "package.json",
+      "message": "Cannot find package.json",
+    },
+    Object {
+      "depName": "backend/package.json",
+      "message": "Cannot find package.json",
+    },
+  ],
+  "excludePackageNames": Array [],
+  "excludePackagePatterns": Array [],
+  "extends": Array [],
+  "group": Object {
+    "branchName": "{{branchPrefix}}{{groupSlug}}",
+    "commitMessage": "Renovate {{groupName}} packages",
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request renovates the package group \\"{{groupName}}\\".
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: from \`{{upgrade.currentVersion}}\` to \`{{upgrade.newVersion}}\`
+{{/each}}
+
+{{#unless isPin}}
+### Commits
+
+{{#each upgrades as |upgrade|}}
+{{#if upgrade.releases.length}}
+<details>
+<summary>{{upgrade.githubName}}</summary>
+{{#each upgrade.releases as |release|}}
+
+#### {{release.version}}
+{{#each release.commits as |commit|}}
+-   [\`{{commit.shortSha}}\`]({{commit.url}}){{commit.message}}
+{{/each}}
+{{/each}}
+
+</details>
+{{/if}}
+{{/each}}
+{{/unless}}
+<br />
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Renovate {{groupName}} packages",
+    "recreateClosed": true,
+  },
+  "groupName": null,
+  "groupSlug": null,
+  "ignoreDeps": Array [],
+  "ignoreFuture": true,
+  "ignoreNpmrcFile": false,
+  "ignorePaths": Array [
+    "**/node_modules/**",
+  ],
+  "ignoreUnstable": true,
+  "labels": Array [],
+  "lazyGrouping": true,
+  "lockFileMaintenance": Object {
+    "branchName": "{{branchPrefix}}lock-file-maintenance",
+    "commitMessage": "Update lock file",
+    "enabled": false,
+    "groupName": null,
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates \`package.json\` lock files to use the latest dependency versions.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Lock file maintenance",
+    "recreateClosed": true,
+    "schedule": Array [
+      "before 5am on monday",
+    ],
+  },
+  "logFile": null,
+  "logFileLevel": "debug",
+  "logLevel": "info",
+  "logger": Object {
+    "child": [Function],
+    "debug": [Function],
+    "error": [Function],
+    "fatal": [Function],
+    "info": [Function],
+    "trace": [Function],
+    "warn": [Function],
+  },
+  "major": Object {},
+  "meteor": Object {
+    "enabled": true,
+  },
+  "minor": Object {},
+  "monorepoPackages": Array [],
+  "npm": Object {
+    "enabled": true,
+    "pin": Object {
+      "automerge": true,
+    },
+  },
+  "npmrc": null,
+  "onboarding": true,
+  "optionalDependencies": Object {},
+  "packageFiles": Array [],
+  "packageNames": Array [],
+  "packagePatterns": Array [],
+  "packageRules": Array [],
+  "patch": Object {
+    "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.{{newVersionMinor}}.x",
+  },
+  "peerDependencies": Object {
+    "enabled": false,
+  },
+  "pin": Object {
+    "group": Object {
+      "commitMessage": "Pin Dependencies",
+      "prTitle": "{{groupName}}",
+      "semanticPrefix": "refactor(deps):",
+    },
+    "groupName": "Pin Dependencies",
+    "rebaseStalePrs": true,
+    "recreateClosed": true,
+    "unpublishSafe": false,
+  },
+  "pinDigests": true,
+  "pinVersions": null,
+  "platform": "github",
+  "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request {{#if isRollback}}rolls back{{else}}updates{{/if}} dependency {{#if repositoryUrl}}[{{depName}}]({{repositoryUrl}}){{else}}\`{{depName}}\`{{/if}} from \`{{#unless isRange}}{{#unless isPin}}v{{/unless}}{{/unless}}{{currentVersion}}\` to \`{{#unless isRange}}v{{/unless}}{{newVersion}}\`{{#if isRollback}}. This is necessary and important because \`v{{currentVersion}}\` cannot be found in the npm registry - probably because of it being unpublished.{{/if}}
+{{#if releases.length}}
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+### Commits
+
+<details>
+<summary>{{githubName}}</summary>
+
+{{#each releases as |release|}}
+#### {{release.version}}
+{{#each release.commits as |commit|}}
+-   [\`{{commit.shortSha}}\`]({{commit.url}}) {{commit.message}}
+{{/each}}
+{{/each}}
+
+</details>
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+  "prCreation": "immediate",
+  "prNotPendingHours": 12,
+  "prTitle": "{{#if isPin}}Pin{{else}}{{#if isRollback}}Roll back{{else}}Update{{/if}}{{/if}} dependency {{depName}} to {{#if isRange}}{{newVersion}}{{else}}{{#if isMajor}}v{{newVersionMajor}}{{else}}v{{newVersion}}{{/if}}{{/if}}",
+  "privateKey": null,
+  "rebaseStalePrs": false,
+  "recreateClosed": false,
+  "renovateFork": false,
+  "repositories": Array [],
+  "requiredStatusChecks": Array [],
+  "respectLatest": true,
+  "reviewers": Array [],
+  "schedule": Array [],
+  "semanticCommits": null,
+  "semanticPrefix": "chore(deps):",
+  "separateMajorReleases": true,
+  "separatePatchReleases": false,
+  "timezone": null,
+  "token": null,
+  "unpublishSafe": false,
+  "updateNotScheduled": true,
+  "warnings": Array [],
+  "workspaceDir": undefined,
+  "yarnrc": null,
+}
+`;
diff --git a/test/manager/resolve.spec.js b/test/manager/resolve.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..9356237316fbd198af7c7248eceeba3bf4cc1c8b
--- /dev/null
+++ b/test/manager/resolve.spec.js
@@ -0,0 +1,74 @@
+const { resolvePackageFiles } = require('../../lib/manager/resolve');
+const manager = require('../../lib/manager');
+
+let config;
+beforeEach(() => {
+  jest.resetAllMocks();
+  config = { ...require('../_fixtures/config') };
+  config.errors = [];
+  config.warnings = [];
+});
+
+describe('manager/resolve', () => {
+  describe('resolvePackageFiles()', () => {
+    it('uses packageFiles if already configured and raises error if not found', async () => {
+      config.packageFiles = [
+        'package.json',
+        { packageFile: 'backend/package.json' },
+      ];
+      const res = await resolvePackageFiles(config);
+      expect(res).toMatchSnapshot();
+      expect(res.errors).toHaveLength(2);
+    });
+    it('deetect package.json and warns if cannot parse', async () => {
+      manager.detectPackageFiles = jest.fn(() => [
+        { packageFile: 'package.json' },
+      ]);
+      config.api.getFileContent.mockReturnValueOnce('not json');
+      const res = await resolvePackageFiles(config);
+      expect(res).toMatchSnapshot();
+      expect(res.warnings).toHaveLength(1);
+    });
+    it('detects package.json and parses json with renovate config', async () => {
+      manager.detectPackageFiles = jest.fn(() => [
+        { packageFile: 'package.json' },
+      ]);
+      const pJson = {
+        renovate: {
+          automerge: true,
+        },
+      };
+      config.api.getFileContent.mockReturnValueOnce(JSON.stringify(pJson));
+      const res = await resolvePackageFiles(config);
+      expect(res).toMatchSnapshot();
+      expect(res.warnings).toHaveLength(0);
+    });
+    it('downloads accompanying files', async () => {
+      manager.detectPackageFiles = jest.fn(() => [
+        { packageFile: 'package.json' },
+      ]);
+      config.api.getFileContent.mockReturnValueOnce('{"name": "package.json"}');
+      config.api.getFileContent.mockReturnValueOnce('npmrc');
+      config.api.getFileContent.mockReturnValueOnce('yarnrc');
+      config.api.getFileContent.mockReturnValueOnce('# yarn.lock');
+      config.api.getFileContent.mockReturnValueOnce(
+        '{"name": "packge-lock.json"}'
+      );
+      const res = await resolvePackageFiles(config);
+      expect(res).toMatchSnapshot();
+      expect(res.warnings).toHaveLength(0);
+    });
+    it('detects meteor and docker', async () => {
+      config.packageFiles = ['package.js', 'Dockerfile'];
+      config.api.getFileContent.mockReturnValueOnce('# comment\nFROM node:8\n'); // Dockerfile
+      const res = await resolvePackageFiles(config);
+      expect(res).toMatchSnapshot();
+    });
+    it('skips docker if no content or no match', async () => {
+      config.packageFiles = ['Dockerfile', 'other/Dockerfile'];
+      config.api.getFileContent.mockReturnValueOnce('# comment\n'); // Dockerfile
+      const res = await resolvePackageFiles(config);
+      expect(res).toMatchSnapshot();
+    });
+  });
+});
diff --git a/test/workers/repository/__snapshots__/apis.spec.js.snap b/test/workers/repository/__snapshots__/apis.spec.js.snap
deleted file mode 100644
index e1fd6165dd9ff00e8cc137143079a4a7c6a47b87..0000000000000000000000000000000000000000
--- a/test/workers/repository/__snapshots__/apis.spec.js.snap
+++ /dev/null
@@ -1,44 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`workers/repository/apis initApis(config) throws if unknown platform 1`] = `"Unknown platform: foo"`;
-
-exports[`workers/repository/apis mergeRenovateJson(config) returns error in config if renovate.json cannot be parsed 1`] = `
-Array [
-  Object {
-    "depName": "renovate.json",
-    "message": "Syntax error: expecting String near { enabled:",
-  },
-]
-`;
-
-exports[`workers/repository/apis mergeRenovateJson(config) returns error plus extended config if duplicate keys 1`] = `
-Array [
-  Object {
-    "depName": "renovate.json",
-    "message": "Syntax error: duplicated keys \\"enabled\\" near \\": false }",
-  },
-]
-`;
-
-exports[`workers/repository/apis mergeRenovateJson(config) returns warning + error plus extended config if unknown keys 1`] = `Array []`;
-
-exports[`workers/repository/apis resolvePackageFiles includes files with content 1`] = `
-Array [
-  Object {
-    "content": Object {
-      "workspaces": Array [],
-    },
-    "npmrc": "npmrc-1",
-    "packageFile": "package.json",
-    "packageLock": "packageLock-1",
-    "yarnLock": "yarnLock-1",
-    "yarnrc": "yarnrc-1",
-  },
-  Object {
-    "content": Object {},
-    "packageFile": "a/package.json",
-    "packageLock": undefined,
-    "yarnLock": undefined,
-  },
-]
-`;
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 0000000000000000000000000000000000000000..2b844f24c33cb85a0ac463b522d0488e9b301fbb
--- /dev/null
+++ b/test/workers/repository/__snapshots__/index.spec.js.snap
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`workers/repository renovateRepository() ensures onboarding pr 1`] = `"onboarding"`;
+
+exports[`workers/repository renovateRepository() exits after 6 loops 1`] = `"loops>5"`;
+
+exports[`workers/repository renovateRepository() writes 1`] = `"onboarded"`;
diff --git a/test/workers/repository/__snapshots__/onboarding.spec.js.snap b/test/workers/repository/__snapshots__/onboarding.spec.js.snap
deleted file mode 100644
index a4a877009340171ac7620d996cd3cd2b86a0ea4b..0000000000000000000000000000000000000000
--- a/test/workers/repository/__snapshots__/onboarding.spec.js.snap
+++ /dev/null
@@ -1,21 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`lib/workers/repository/onboarding getOnboardingStatus(config) commits files and returns false if no pr 1`] = `
-Array [
-  "renovate/configure",
-  Array [
-    Object {
-      "contents": "{
-  \\"extends\\": [
-    \\"config:base\\"
-  ]
-}
-",
-      "name": "renovate.json",
-    },
-  ],
-  "Add renovate.json",
-]
-`;
-
-exports[`lib/workers/repository/onboarding getOnboardingStatus(config) throws if no packageFiles 1`] = `[Error: no package files]`;
diff --git a/test/workers/repository/__snapshots__/upgrades.spec.js.snap b/test/workers/repository/__snapshots__/upgrades.spec.js.snap
deleted file mode 100644
index 08154511a58ae457b3e60058c1cfc31abfc288a9..0000000000000000000000000000000000000000
--- a/test/workers/repository/__snapshots__/upgrades.spec.js.snap
+++ /dev/null
@@ -1,231 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`workers/repository/upgrades generateConfig(branchUpgrades) does not group single upgrade 1`] = `
-Object {
-  "branchName": "some-branch",
-  "depName": "some-dep",
-  "foo": 1,
-  "prTitle": "some-prefix: some-title",
-  "semanticCommits": true,
-  "semanticPrefix": "some-prefix:",
-  "upgrades": Array [
-    Object {
-      "branchName": "some-branch",
-      "depName": "some-dep",
-      "foo": 1,
-      "prTitle": "some-prefix: some-title",
-      "semanticCommits": true,
-      "semanticPrefix": "some-prefix:",
-    },
-  ],
-}
-`;
-
-exports[`workers/repository/upgrades generateConfig(branchUpgrades) groups multiple upgrades 1`] = `
-Object {
-  "branchName": "some-branch",
-  "depName": "some-dep",
-  "foo": 2,
-  "groupName": "some-group",
-  "prTitle": "some-title",
-  "upgrades": Array [
-    Object {
-      "branchName": "some-branch",
-      "depName": "some-dep",
-      "foo": 2,
-      "groupName": "some-group",
-      "prTitle": "some-title",
-    },
-    Object {
-      "branchName": "some-branch",
-      "depName": "some-other-dep",
-      "foo": 2,
-      "groupName": "some-group",
-      "prTitle": "some-title",
-    },
-  ],
-}
-`;
-
-exports[`workers/repository/upgrades generateConfig(branchUpgrades) groups single upgrade if not lazyGrouping 1`] = `
-Object {
-  "branchName": "some-branch",
-  "depName": "some-dep",
-  "foo": 2,
-  "groupName": "some-group",
-  "prTitle": "some-title",
-  "upgrades": Array [
-    Object {
-      "branchName": "some-branch",
-      "depName": "some-dep",
-      "foo": 2,
-      "groupName": "some-group",
-      "prTitle": "some-title",
-    },
-  ],
-}
-`;
-
-exports[`workers/repository/upgrades groupByBranch(upgrades) does not group if different compiled branch names 1`] = `
-Object {
-  "branchUpgrades": Object {
-    "bar-1.1.0": Array [
-      Object {
-        "branchName": "bar-{{version}}",
-        "depName": "bar",
-        "prTitle": "some-title",
-        "version": "1.1.0",
-      },
-    ],
-    "foo-1.1.0": Array [
-      Object {
-        "branchName": "foo-{{version}}",
-        "depName": "foo",
-        "prTitle": "some-title",
-        "version": "1.1.0",
-      },
-    ],
-    "foo-2.0.0": Array [
-      Object {
-        "branchName": "foo-{{version}}",
-        "depName": "foo",
-        "prTitle": "some-title",
-        "version": "2.0.0",
-      },
-    ],
-  },
-  "errors": Array [],
-  "warnings": Array [],
-}
-`;
-
-exports[`workers/repository/upgrades groupByBranch(upgrades) groups if same compiled branch names 1`] = `
-Object {
-  "branchUpgrades": Object {
-    "bar-1.1.0": Array [
-      Object {
-        "branchName": "bar-{{version}}",
-        "depName": "bar",
-        "prTitle": "some-title",
-        "version": "1.1.0",
-      },
-    ],
-    "foo": Array [
-      Object {
-        "branchName": "foo",
-        "depName": "foo",
-        "prTitle": "some-title",
-        "version": "2.0.0",
-      },
-      Object {
-        "branchName": "foo",
-        "depName": "foo",
-        "prTitle": "some-title",
-        "version": "1.1.0",
-      },
-    ],
-  },
-  "errors": Array [],
-  "warnings": Array [],
-}
-`;
-
-exports[`workers/repository/upgrades groupByBranch(upgrades) groups if same compiled group name 1`] = `
-Object {
-  "branchUpgrades": Object {
-    "foo": Array [
-      Object {
-        "branchName": "foo",
-        "depName": "foo",
-        "prTitle": "some-title",
-        "version": "2.0.0",
-      },
-    ],
-    "renovate/my-group": Array [
-      Object {
-        "branchName": "bar-{{version}}",
-        "depName": "bar",
-        "group": Object {
-          "branchName": "renovate/my-group",
-        },
-        "groupName": "My Group",
-        "groupSlug": "my-group",
-        "prTitle": "some-title",
-        "version": "1.1.0",
-      },
-      Object {
-        "branchName": "foo",
-        "depName": "foo",
-        "group": Object {
-          "branchName": "renovate/{{groupSlug}}",
-        },
-        "groupName": "My Group",
-        "groupSlug": "my-group",
-        "prTitle": "some-title",
-        "version": "1.1.0",
-      },
-    ],
-  },
-  "errors": Array [],
-  "warnings": Array [],
-}
-`;
-
-exports[`workers/repository/upgrades groupByBranch(upgrades) mixes errors and warnings 1`] = `
-Object {
-  "branchUpgrades": Object {
-    "bar-1.1.0": Array [
-      Object {
-        "branchName": "bar-{{version}}",
-        "prTitle": "some-title",
-        "version": "1.1.0",
-      },
-    ],
-    "foo-1.1.0": Array [
-      Object {
-        "branchName": "foo-{{version}}",
-        "prTitle": "some-title",
-        "version": "1.1.0",
-      },
-    ],
-  },
-  "errors": Array [
-    Object {
-      "type": "error",
-    },
-  ],
-  "warnings": Array [
-    Object {
-      "branchName": "foo-{{version}}",
-      "prTitle": "some-title",
-      "type": "warning",
-      "version": "2.0.0",
-    },
-  ],
-}
-`;
-
-exports[`workers/repository/upgrades groupByBranch(upgrades) returns empty object if no input array 1`] = `
-Object {
-  "branchUpgrades": Object {},
-  "errors": Array [],
-  "warnings": Array [],
-}
-`;
-
-exports[`workers/repository/upgrades groupByBranch(upgrades) returns one branch if one input 1`] = `
-Object {
-  "branchUpgrades": Object {
-    "foo-1.1.0": Array [
-      Object {
-        "branchName": "foo-{{version}}",
-        "depName": "foo",
-        "prTitle": "some-title",
-        "version": "1.1.0",
-      },
-    ],
-  },
-  "errors": Array [],
-  "warnings": Array [],
-}
-`;
diff --git a/test/workers/repository/apis.spec.js b/test/workers/repository/apis.spec.js
deleted file mode 100644
index 5006d051c0c9d57e0813cdebcdb1d2f70460a069..0000000000000000000000000000000000000000
--- a/test/workers/repository/apis.spec.js
+++ /dev/null
@@ -1,241 +0,0 @@
-const jsonValidator = require('json-dup-key-validator');
-
-const apis = require('../../../lib/workers/repository/apis');
-const logger = require('../../_fixtures/logger');
-
-const githubApi = require('../../../lib/platform/github');
-const gitlabApi = require('../../../lib/platform/gitlab');
-const npmApi = require('../../../lib/manager/npm/registry');
-
-jest.mock('../../../lib/platform/github');
-jest.mock('../../../lib/platform/gitlab');
-jest.mock('../../../lib/manager/npm/registry');
-
-describe('workers/repository/apis', () => {
-  describe('getNpmrc', () => {
-    it('Skips if ignoring npmrc', async () => {
-      const config = {
-        foo: 1,
-        ignoreNpmrcFile: true,
-      };
-      expect(await apis.getNpmrc(config)).toMatchObject(config);
-    });
-    it('Skips if npmrc not found', async () => {
-      const config = {
-        api: {
-          getFileContent: jest.fn(),
-        },
-      };
-      expect(await apis.getNpmrc(config)).toMatchObject(config);
-    });
-    it('Parses if npmrc found', async () => {
-      const config = {
-        api: {
-          getFileContent: jest.fn(() => 'a = b'),
-        },
-        logger,
-      };
-      const res = await apis.getNpmrc(config);
-      expect(res.npmrc).toEqual('a = b');
-    });
-    it('Catches errors', async () => {
-      const config = {
-        api: {
-          getFileContent: jest.fn(() => {
-            throw new Error('file error');
-          }),
-        },
-        logger,
-      };
-      expect(await apis.getNpmrc(config)).toMatchObject(config);
-    });
-  });
-  describe('detectSemanticCommits', () => {
-    it('disables semantic commits', async () => {
-      const config = {
-        api: {
-          getCommitMessages: jest.fn(() => []),
-        },
-        logger,
-      };
-      const res = await apis.detectSemanticCommits(config);
-      expect(res).toEqual(false);
-    });
-    it('enables semantic commits', async () => {
-      const config = {
-        api: {
-          getCommitMessages: jest.fn(() => []),
-        },
-        logger,
-      };
-      config.api.getCommitMessages.mockReturnValueOnce(['fix: something']);
-      const res = await apis.detectSemanticCommits(config);
-      expect(res).toEqual(true);
-    });
-  });
-  describe('initApis(config)', () => {
-    beforeEach(() => {
-      jest.resetAllMocks();
-    });
-    it('returns github api', async () => {
-      const config = { logger, platform: 'github', semanticCommits: null };
-      const res = await apis.initApis(config);
-      expect(res.platform).toEqual('github');
-      expect(githubApi.initRepo.mock.calls.length).toBe(1);
-      expect(gitlabApi.initRepo.mock.calls.length).toBe(0);
-      expect(npmApi.setNpmrc.mock.calls.length).toBe(0);
-    });
-    it('returns gitlab api', async () => {
-      const config = { logger, platform: 'gitlab' };
-      const res = await apis.initApis(config);
-      expect(res.platform).toEqual('gitlab');
-      expect(githubApi.initRepo.mock.calls.length).toBe(0);
-      expect(gitlabApi.initRepo.mock.calls.length).toBe(1);
-      expect(npmApi.setNpmrc.mock.calls.length).toBe(0);
-    });
-    it('throws if unknown platform', async () => {
-      const config = { platform: 'foo' };
-      let e;
-      try {
-        await apis.initApis(config);
-      } catch (err) {
-        e = err;
-      }
-      expect(e.message).toMatchSnapshot();
-      expect(githubApi.initRepo.mock.calls.length).toBe(0);
-      expect(gitlabApi.initRepo.mock.calls.length).toBe(0);
-      expect(npmApi.setNpmrc.mock.calls.length).toBe(0);
-    });
-  });
-  describe('mergeRenovateJson(config)', () => {
-    let config;
-    beforeEach(() => {
-      config = {
-        errors: [],
-        warnings: [],
-        api: {
-          getFileContent: jest.fn(),
-        },
-        logger,
-      };
-    });
-    it('returns same config if no renovate.json found', async () => {
-      expect(await apis.mergeRenovateJson(config)).toEqual(config);
-    });
-    it('returns extended config if renovate.json found', async () => {
-      config.api.getFileContent.mockReturnValueOnce('{ "enabled": true }');
-      const returnConfig = await apis.mergeRenovateJson(config);
-      expect(returnConfig.enabled).toBe(true);
-      expect(returnConfig.renovateJsonPresent).toBe(true);
-      expect(returnConfig.errors).toHaveLength(0);
-    });
-    it('returns warning + error plus extended config if unknown keys', async () => {
-      config.repoIsOnboarded = true;
-      config.api.getFileContent.mockReturnValueOnce(
-        '{ "enabled": true, "foo": false, "maintainYarnLock": true, "schedule": "before 5am", "minor": {} }'
-      );
-      const returnConfig = await apis.mergeRenovateJson(config);
-      expect(returnConfig.enabled).toBe(true);
-      expect(returnConfig.renovateJsonPresent).toBe(true);
-      expect(returnConfig.errors).toHaveLength(0); // TODO: Update to 1 later
-      expect(returnConfig.errors).toMatchSnapshot();
-    });
-    it('returns error plus extended config if duplicate keys', async () => {
-      config.repoIsOnboarded = true;
-      config.api.getFileContent.mockReturnValueOnce(
-        '{ "enabled": true, "enabled": false }'
-      );
-      const returnConfig = await apis.mergeRenovateJson(config);
-      expect(returnConfig.enabled).toBe(false);
-      expect(returnConfig.renovateJsonPresent).toBe(true);
-      expect(returnConfig.errors).toHaveLength(1);
-      expect(returnConfig.errors).toMatchSnapshot();
-    });
-    it('returns error in config if renovate.json cannot be parsed', async () => {
-      config.api.getFileContent.mockReturnValueOnce('{ enabled: true }');
-      const returnConfig = await apis.mergeRenovateJson(config);
-      expect(returnConfig.enabled).toBeUndefined();
-      expect(returnConfig.renovateJsonPresent).toBeUndefined();
-      expect(returnConfig.errors).toMatchSnapshot();
-    });
-    it('returns error in JSON.parse', async () => {
-      config.api.getFileContent.mockReturnValueOnce('{ enabled: true }');
-      jsonValidator.validate = jest.fn();
-      jsonValidator.validate.mockReturnValueOnce(false);
-      jsonValidator.validate.mockReturnValueOnce(false);
-      let e;
-      try {
-        await apis.mergeRenovateJson(config);
-      } catch (err) {
-        e = err;
-      }
-      expect(e).toBeDefined();
-    });
-  });
-  describe('resolvePackageFiles', () => {
-    let config;
-    beforeEach(() => {
-      config = {
-        errors: [],
-        warnings: [],
-        packageFiles: ['package.json', { packageFile: 'a/package.json' }],
-        api: {
-          getFileContent: jest.fn(() => null),
-        },
-        logger,
-      };
-    });
-    it('skips files with no content', async () => {
-      const res = await apis.resolvePackageFiles(config);
-      expect(res.packageFiles).toEqual([]);
-    });
-    it('skips files with invalid JSON', async () => {
-      config.api.getFileContent.mockReturnValueOnce('not json');
-      const res = await apis.resolvePackageFiles(config);
-      expect(res.packageFiles).toEqual([]);
-    });
-    it('includes files with content', async () => {
-      config.repoIsOnboarded = true;
-      config.api.getFileContent.mockReturnValueOnce(
-        JSON.stringify({
-          renovate: {},
-          workspaces: [],
-        })
-      );
-      config.api.getFileContent.mockReturnValueOnce('npmrc-1');
-      config.api.getFileContent.mockReturnValueOnce('yarnrc-1');
-      config.api.getFileContent.mockReturnValueOnce('yarnLock-1');
-      config.api.getFileContent.mockReturnValueOnce('packageLock-1');
-      config.api.getFileContent.mockReturnValueOnce('{}');
-      const res = await apis.resolvePackageFiles(config);
-      expect(res.packageFiles).toHaveLength(2);
-      expect(res.packageFiles).toMatchSnapshot();
-    });
-    it('handles meteor', async () => {
-      config.packageFiles = [{ packageFile: 'package.js' }];
-      const res = await apis.resolvePackageFiles(config);
-      expect(res.packageFiles).toHaveLength(1);
-    });
-    it('handles dockerfile', async () => {
-      config.packageFiles = [{ packageFile: 'Dockerfile' }];
-      config.api.getFileContent.mockReturnValueOnce(
-        '# some content\nFROM node:8\nRUN something'
-      );
-      const res = await apis.resolvePackageFiles(config);
-      expect(res.packageFiles).toHaveLength(1);
-    });
-    it('handles dockerfile with no content', async () => {
-      config.packageFiles = [{ packageFile: 'Dockerfile' }];
-      const res = await apis.resolvePackageFiles(config);
-      expect(res.packageFiles).toHaveLength(0);
-    });
-    it('handles dockerfile with no FROM', async () => {
-      config.packageFiles = [{ packageFile: 'Dockerfile' }];
-      config.api.getFileContent.mockReturnValueOnce(
-        '# some content\n# FROM node:8\nRUN something'
-      );
-      const res = await apis.resolvePackageFiles(config);
-      expect(res.packageFiles).toHaveLength(0);
-    });
-  });
-});
diff --git a/test/workers/repository/cleanup.spec.js b/test/workers/repository/cleanup.spec.js
index 5164214118c649f332fa0ef18f9a0cbdff8ab65c..0d8f2f96b68191d87da502ae3b6c86205cac2491 100644
--- a/test/workers/repository/cleanup.spec.js
+++ b/test/workers/repository/cleanup.spec.js
@@ -1,59 +1,47 @@
-const defaultConfig = require('../../../lib/config/defaults').getConfig();
 const cleanup = require('../../../lib/workers/repository/cleanup');
-const logger = require('../../_fixtures/logger');
+
+let config;
+beforeEach(() => {
+  jest.resetAllMocks();
+  config = require('../../_fixtures/config');
+  config.platform = 'github';
+  config.errors = [];
+  config.warnings = [];
+});
 
 describe('workers/repository/cleanup', () => {
-  describe('pruneStaleBranches(config, branchUpgradeNames)', () => {
-    let branchNames;
-    let config;
-    beforeEach(() => {
-      branchNames = [];
-      config = { ...defaultConfig };
-      config.api = {
-        getAllRenovateBranches: jest.fn(),
-        getPr: jest.fn(),
-        deleteBranch: jest.fn(),
-        findPr: jest.fn(),
-        updatePr: jest.fn(),
-      };
-      config.logger = logger;
+  describe('pruneStaleBranches()', () => {
+    it('returns if no branchList', async () => {
+      delete config.branchList;
+      await cleanup.pruneStaleBranches(config, config.branchList);
+      expect(config.api.getAllRenovateBranches.mock.calls).toHaveLength(0);
     });
     it('returns if config is not github', async () => {
+      config.branchList = [];
       config.platform = 'gitlab';
-      await cleanup.pruneStaleBranches(config, branchNames);
+      await cleanup.pruneStaleBranches(config, config.branchList);
       expect(config.api.getAllRenovateBranches.mock.calls).toHaveLength(0);
     });
     it('returns if no remaining branches', async () => {
-      branchNames = ['renovate/a', 'renovate/b'];
-      config.api.getAllRenovateBranches.mockReturnValueOnce(branchNames);
-      await cleanup.pruneStaleBranches(config, branchNames);
+      config.branchList = ['renovate/a', 'renovate/b'];
+      config.api.getAllRenovateBranches.mockReturnValueOnce(config.branchList);
+      await cleanup.pruneStaleBranches(config, config.branchList);
       expect(config.api.getAllRenovateBranches.mock.calls).toHaveLength(1);
+      expect(config.api.deleteBranch.mock.calls).toHaveLength(0);
     });
     it('renames deletes remaining branch', async () => {
-      branchNames = ['renovate/a', 'renovate/b'];
+      config.branchList = ['renovate/a', 'renovate/b'];
       config.api.getAllRenovateBranches.mockReturnValueOnce(
-        branchNames.concat(['renovate/c'])
+        config.branchList.concat(['renovate/c'])
       );
       config.api.findPr.mockReturnValueOnce({});
-      await cleanup.pruneStaleBranches(config, branchNames);
+      await cleanup.pruneStaleBranches(config, config.branchList);
       expect(config.api.getAllRenovateBranches.mock.calls).toHaveLength(1);
       expect(config.api.deleteBranch.mock.calls).toHaveLength(1);
       expect(config.api.updatePr.mock.calls).toHaveLength(1);
     });
-    it('deletes lock file maintenance if pr is closed', async () => {
-      branchNames = ['renovate/lock-file-maintenance'];
-      config.api.getAllRenovateBranches.mockReturnValueOnce([
-        'renovate/lock-file-maintenance',
-      ]);
-      config.api.getBranchPr = jest.fn(() => ({ isClosed: true }));
-      await cleanup.pruneStaleBranches(config, [
-        'renovate/lock-file-maintenance',
-      ]);
-      expect(config.api.getAllRenovateBranches.mock.calls).toHaveLength(1);
-      expect(config.api.deleteBranch.mock.calls).toHaveLength(1);
-    });
     it('deletes lock file maintenance if pr is unmergeable', async () => {
-      branchNames = ['renovate/lock-file-maintenance'];
+      config.branchList = ['renovate/lock-file-maintenance'];
       config.api.getAllRenovateBranches.mockReturnValueOnce([
         'renovate/lock-file-maintenance',
       ]);
@@ -64,24 +52,12 @@ describe('workers/repository/cleanup', () => {
       expect(config.api.getAllRenovateBranches.mock.calls).toHaveLength(1);
       expect(config.api.deleteBranch.mock.calls).toHaveLength(1);
     });
-    it('deletes lock file maintenance if no changed files', async () => {
-      branchNames = ['renovate/lock-file-maintenance'];
-      config.api.getAllRenovateBranches.mockReturnValueOnce([
-        'renovate/lock-file-maintenance',
-      ]);
-      config.api.getBranchPr = jest.fn(() => ({ changed_files: 0 }));
-      await cleanup.pruneStaleBranches(config, [
-        'renovate/lock-file-maintenance',
-      ]);
-      expect(config.api.getAllRenovateBranches.mock.calls).toHaveLength(1);
-      expect(config.api.deleteBranch.mock.calls).toHaveLength(1);
-    });
     it('calls delete only once', async () => {
-      branchNames = ['renovate/lock-file-maintenance'];
+      config.branchList = ['renovate/lock-file-maintenance'];
       config.api.getAllRenovateBranches.mockReturnValueOnce([
         'renovate/lock-file-maintenance',
       ]);
-      config.api.getBranchPr = jest.fn(() => ({ isClosed: true }));
+      config.api.getBranchPr = jest.fn(() => ({ isUnmergeable: true }));
       await cleanup.pruneStaleBranches(config, []);
       expect(config.api.getAllRenovateBranches.mock.calls).toHaveLength(1);
       expect(config.api.deleteBranch.mock.calls).toHaveLength(1);
diff --git a/test/workers/repository/configured.spec.js b/test/workers/repository/configured.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..badb6cd75aadb4823a2e958e7e3e3edcbc561e73
--- /dev/null
+++ b/test/workers/repository/configured.spec.js
@@ -0,0 +1,39 @@
+const {
+  checkIfConfigured,
+} = require('../../../lib/workers/repository/configured');
+
+let config;
+beforeEach(() => {
+  jest.resetAllMocks();
+  config = { ...require('../../_fixtures/config') };
+});
+
+describe('workers/repository/configured', () => {
+  describe('checkIfConfigured()', () => {
+    it('returns', () => {
+      checkIfConfigured(config);
+    });
+    it('throws if disabled', () => {
+      config.enabled = false;
+      let e;
+      try {
+        checkIfConfigured(config);
+      } catch (err) {
+        e = err;
+      }
+      expect(e).toBeDefined();
+    });
+    it('throws if unconfigured fork', () => {
+      config.enabled = true;
+      config.isFork = true;
+      config.renovateJsonPresent = false;
+      let e;
+      try {
+        checkIfConfigured(config);
+      } catch (err) {
+        e = err;
+      }
+      expect(e).toBeDefined();
+    });
+  });
+});
diff --git a/test/workers/repository/error.spec.js b/test/workers/repository/error.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..1846e76a6f332efbef195bf39a06fcd334516437
--- /dev/null
+++ b/test/workers/repository/error.spec.js
@@ -0,0 +1,29 @@
+const { handleError } = require('../../../lib/workers/repository/error');
+
+let config;
+beforeEach(() => {
+  jest.resetAllMocks();
+  config = require('../../_fixtures/config');
+});
+
+describe('workers/repository/error', () => {
+  describe('handleError()', () => {
+    const errors = [
+      'uninitiated',
+      'disabled',
+      'fork',
+      'no-package-files',
+      'loops>5',
+    ];
+    errors.forEach(err => {
+      it(`errors ${err}`, () => {
+        const res = handleError(config, new Error(err));
+        expect(res).toEqual(err);
+      });
+    });
+    it('handles unknown error', () => {
+      const res = handleError(config, new Error('abcdefg'));
+      expect(res).toEqual('unknown-error');
+    });
+  });
+});
diff --git a/test/workers/repository/index.spec.js b/test/workers/repository/index.spec.js
index 2d612bf17f41d9d74460792f7dec4c20f4c05785..2c39859c8c292a94a9854b4f87c6e383173cd899 100644
--- a/test/workers/repository/index.spec.js
+++ b/test/workers/repository/index.spec.js
@@ -1,223 +1,42 @@
-const repositoryWorker = require('../../../lib/workers/repository/index');
-const branchWorker = require('../../../lib/workers/branch');
-
-const apis = require('../../../lib/workers/repository/apis');
-const manager = require('../../../lib/manager');
-const onboarding = require('../../../lib/workers/repository/onboarding');
-const upgrades = require('../../../lib/workers/repository/upgrades');
-
-const logger = require('../../_fixtures/logger');
-const onboardingPr = require('../../../lib/workers/repository/onboarding/pr');
+const { determineUpdates } = require('../../../lib/workers/repository/updates');
+const { writeUpdates } = require('../../../lib/workers/repository/write');
+const {
+  ensureOnboardingPr,
+} = require('../../../lib/workers/repository/onboarding/pr');
+const { renovateRepository } = require('../../../lib/workers/repository/index');
 
+jest.mock('../../../lib/workers/repository/init');
+jest.mock('../../../lib/workers/repository/init/apis');
+jest.mock('../../../lib/workers/repository/updates');
 jest.mock('../../../lib/workers/repository/onboarding/pr');
+jest.mock('../../../lib/workers/repository/write');
+jest.mock('../../../lib/workers/repository/cleanup');
+jest.mock('../../../lib/manager/resolve');
+
+let config;
+beforeEach(() => {
+  jest.resetAllMocks();
+  config = require('../../_fixtures/config');
+});
 
 describe('workers/repository', () => {
-  describe('renovateRepository', () => {
-    let config;
-    beforeEach(() => {
-      jest.resetAllMocks();
-      apis.initApis = jest.fn(input => input);
-      apis.mergeRenovateJson = jest.fn(input => input);
-      manager.detectPackageFiles = jest.fn();
-      apis.resolvePackageFiles = jest.fn(input => input);
-      apis.checkMonorepos = jest.fn(input => input);
-      onboarding.getOnboardingStatus = jest.fn(input => input);
-      upgrades.determineRepoUpgrades = jest.fn(() => []);
-      upgrades.branchifyUpgrades = jest.fn(() => ({ branchUpgrades: {} }));
-      branchWorker.processBranch = jest.fn(() => 'done');
-      config = {
-        lockFileMaintenance: true,
-        api: {
-          getFileJson: jest.fn(),
-          setBaseBranch: jest.fn(),
-          branchExists: jest.fn(),
-        },
-        logger,
-        packageFiles: [],
-      };
-    });
-    it('skips repository if config is disabled', async () => {
-      config.enabled = false;
-      await repositoryWorker.renovateRepository(config);
-      expect(manager.detectPackageFiles.mock.calls.length).toBe(0);
-    });
-    it('skips repository if its unconfigured fork', async () => {
-      config.isFork = true;
-      config.renovateJsonPresent = false;
-      await repositoryWorker.renovateRepository(config);
-      expect(manager.detectPackageFiles.mock.calls.length).toBe(0);
-    });
-    it('does not skip repository if its a configured fork', async () => {
-      config.isFork = true;
-      config.renovateFork = true;
-      manager.detectPackageFiles.mockReturnValueOnce([]);
-      await repositoryWorker.renovateRepository(config);
-    });
-    it('sets custom base branch', async () => {
-      config.baseBranch = 'some-branch';
-      config.api.branchExists.mockReturnValueOnce(true);
-      manager.detectPackageFiles.mockReturnValueOnce([]);
-      await repositoryWorker.renovateRepository(config);
-      expect(config.api.setBaseBranch.mock.calls).toHaveLength(1);
-    });
-    it('errors when missing custom base branch', async () => {
-      config.baseBranch = 'some-branch';
-      config.api.branchExists.mockReturnValueOnce(false);
-      manager.detectPackageFiles.mockReturnValueOnce([]);
-      await repositoryWorker.renovateRepository(config);
-      expect(config.api.setBaseBranch.mock.calls).toHaveLength(0);
-    });
-    it('skips repository if no package.json', async () => {
-      manager.detectPackageFiles.mockReturnValueOnce([]);
-      await repositoryWorker.renovateRepository(config);
-      expect(apis.resolvePackageFiles.mock.calls.length).toBe(0);
-      expect(config.logger.error.mock.calls.length).toBe(0);
-    });
-    it('does not skip repository if package.json', async () => {
-      manager.detectPackageFiles.mockReturnValueOnce(['package.json']);
-      config.api.getFileJson = jest.fn(() => ({ a: 1 }));
-      apis.mergeRenovateJson.mockImplementationOnce(input => ({
-        ...input,
-        ...{ packageFiles: ['package.json'] },
-      }));
-      apis.mergeRenovateJson.mockImplementationOnce(input => ({
-        ...input,
-        ...{ packageFiles: ['package.json'] },
-      }));
-      upgrades.branchifyUpgrades.mockReturnValueOnce({
-        upgrades: [{}, {}, {}],
-      });
-      await repositoryWorker.renovateRepository(config);
-      expect(onboarding.getOnboardingStatus.mock.calls.length).toBe(1);
-      expect(branchWorker.processBranch.mock.calls.length).toBe(0);
-      expect(onboardingPr.ensureOnboardingPr.mock.calls.length).toBe(1);
-      expect(config.logger.error.mock.calls.length).toBe(0);
-    });
-    it('uses onboarding custom baseBranch', async () => {
-      manager.detectPackageFiles.mockReturnValueOnce(['package.json']);
-      config.api.getFileJson = jest.fn(() => ({ a: 1 }));
-      manager.detectPackageFiles.mockReturnValueOnce(['package.json']);
-      apis.mergeRenovateJson.mockImplementationOnce(input => ({
-        ...input,
-        ...{ packageFiles: ['package.json'], baseBranch: 'next' },
-      }));
-      config.api.branchExists.mockReturnValueOnce(true);
-      upgrades.branchifyUpgrades.mockReturnValueOnce({
-        upgrades: [{}, {}, {}],
-      });
-      await repositoryWorker.renovateRepository(config);
-      expect(onboarding.getOnboardingStatus.mock.calls.length).toBe(1);
-      expect(branchWorker.processBranch.mock.calls.length).toBe(0);
-      expect(onboardingPr.ensureOnboardingPr.mock.calls.length).toBe(1);
-      expect(config.logger.error.mock.calls.length).toBe(0);
-    });
-    it('errors onboarding custom baseBranch', async () => {
-      manager.detectPackageFiles.mockReturnValueOnce(['package.json']);
-      config.api.getFileJson = jest.fn(() => ({ a: 1 }));
-      apis.mergeRenovateJson.mockImplementationOnce(input => ({
-        ...input,
-        ...{ packageFiles: [] },
-      }));
-      apis.mergeRenovateJson.mockImplementationOnce(input => ({
-        ...input,
-        ...{ packageFiles: [], baseBranch: 'next' },
-      }));
-      upgrades.branchifyUpgrades.mockReturnValueOnce({
-        upgrades: [{}, {}, {}],
-      });
-      await repositoryWorker.renovateRepository(config);
-      expect(onboarding.getOnboardingStatus.mock.calls.length).toBe(1);
-      expect(branchWorker.processBranch.mock.calls.length).toBe(0);
-      expect(onboardingPr.ensureOnboardingPr.mock.calls.length).toBe(1);
-      expect(config.logger.error.mock.calls.length).toBe(0);
-    });
-    it('calls branchWorker', async () => {
-      config.packageFiles = ['package.json'];
-      config.hasRenovateJson = true;
-      onboarding.getOnboardingStatus.mockImplementation(input => ({
-        ...input,
-        repoIsOnboarded: true,
-      }));
-      upgrades.branchifyUpgrades.mockReturnValueOnce({
-        upgrades: [{}, {}, {}],
-      });
-      await repositoryWorker.renovateRepository(config);
-      expect(branchWorker.processBranch.mock.calls.length).toBe(3);
-      expect(config.logger.error.mock.calls.length).toBe(0);
-    });
-    it('skips branchWorker after automerging', async () => {
-      config.packageFiles = ['package.json'];
-      config.hasRenovateJson = true;
-      onboarding.getOnboardingStatus.mockImplementation(input => ({
-        ...input,
-        repoIsOnboarded: true,
-      }));
-      upgrades.branchifyUpgrades.mockReturnValueOnce({
-        upgrades: [{}, {}, {}],
-      });
-      upgrades.branchifyUpgrades.mockReturnValueOnce({
-        upgrades: [{}, {}],
-      });
-      upgrades.branchifyUpgrades.mockReturnValueOnce({
-        upgrades: [{}],
-      });
-      upgrades.branchifyUpgrades.mockReturnValueOnce({
-        upgrades: [],
-      });
-      branchWorker.processBranch.mockReturnValue('automerged');
-      await repositoryWorker.renovateRepository(config);
-      expect(upgrades.branchifyUpgrades.mock.calls).toHaveLength(4);
-      expect(branchWorker.processBranch.mock.calls).toHaveLength(3);
-      expect(config.logger.error.mock.calls).toHaveLength(0);
-    });
-    it('only processes pins first', async () => {
-      config.packageFiles = ['package.json'];
-      config.hasRenovateJson = true;
-      onboarding.getOnboardingStatus.mockImplementation(input => ({
-        ...input,
-        repoIsOnboarded: true,
-      }));
-      upgrades.branchifyUpgrades.mockReturnValueOnce({
-        upgrades: [{ isPin: true }, {}, {}],
-      });
-      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');
-      });
-      await repositoryWorker.renovateRepository(config);
-      expect(config.logger.error.mock.calls.length).toBe(1);
-    });
-    it('handles special uninitiated error', async () => {
-      apis.initApis.mockImplementationOnce(() => {
-        // Create a new object, that prototypically inherits from the Error constructor
-        function MyError() {
-          this.message = 'uninitiated';
-        }
-        MyError.prototype = Object.create(Error.prototype);
-        MyError.prototype.constructor = MyError;
-        throw new MyError();
-      });
-      await repositoryWorker.renovateRepository(config);
-      expect(config.logger.error.mock.calls.length).toBe(0);
-    });
-    it('handles special no package files error', async () => {
-      apis.initApis.mockImplementationOnce(() => {
-        // Create a new object, that prototypically inherits from the Error constructor
-        function MyError() {
-          this.message = 'no package files';
-        }
-        MyError.prototype = Object.create(Error.prototype);
-        MyError.prototype.constructor = MyError;
-        throw new MyError();
-      });
-      await repositoryWorker.renovateRepository(config);
-      expect(config.logger.error.mock.calls.length).toBe(0);
+  describe('renovateRepository()', () => {
+    it('exits after 6 loops', async () => {
+      const res = await renovateRepository(config, 'some-token', 6);
+      expect(res).toMatchSnapshot();
+    });
+    it('writes', async () => {
+      determineUpdates.mockReturnValue({ repoIsOnboarded: true });
+      writeUpdates.mockReturnValueOnce('automerged');
+      writeUpdates.mockReturnValueOnce('onboarded');
+      const res = await renovateRepository(config, 'some-token');
+      expect(res).toMatchSnapshot();
+    });
+    it('ensures onboarding pr', async () => {
+      determineUpdates.mockReturnValue({ repoIsOnboarded: false });
+      ensureOnboardingPr.mockReturnValue('onboarding');
+      const res = await renovateRepository(config, 'some-token');
+      expect(res).toMatchSnapshot();
     });
   });
 });
diff --git a/test/workers/repository/init/__snapshots__/config.spec.js.snap b/test/workers/repository/init/__snapshots__/config.spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..17c4c467fad6681626eda9e4f724442aa9fc25e3
--- /dev/null
+++ b/test/workers/repository/init/__snapshots__/config.spec.js.snap
@@ -0,0 +1,15 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`workers/repository/init/config mergeRenovateJson() returns error if cannot parse 1`] = `
+Object {
+  "depName": "renovate.json",
+  "message": "Syntax error near cannot par",
+}
+`;
+
+exports[`workers/repository/init/config mergeRenovateJson() returns error if duplicate keys 1`] = `
+Object {
+  "depName": "renovate.json",
+  "message": "Syntax error: duplicated keys \\"enabled\\" near \\": false }",
+}
+`;
diff --git a/test/workers/repository/init/apis.spec.js b/test/workers/repository/init/apis.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..682fbfa2f3e4c5b7a73755640915e542ea5917e3
--- /dev/null
+++ b/test/workers/repository/init/apis.spec.js
@@ -0,0 +1,19 @@
+let config;
+beforeEach(() => {
+  jest.resetAllMocks();
+  config = require('../../../_fixtures/config');
+  config.errors = [];
+  config.warnings = [];
+});
+
+const { initApis } = require('../../../../lib/workers/repository/init/apis');
+
+jest.mock('../../../../lib/platform/github');
+
+describe('workers/repository/init/apis', () => {
+  describe('initApis', () => {
+    it('runs', async () => {
+      await initApis(config, 'some-token');
+    });
+  });
+});
diff --git a/test/workers/repository/init/base.spec.js b/test/workers/repository/init/base.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..601ade331d5265f6e38a2efac0623d4a5be93ddf
--- /dev/null
+++ b/test/workers/repository/init/base.spec.js
@@ -0,0 +1,28 @@
+let config;
+beforeEach(() => {
+  jest.resetAllMocks();
+  config = require('../../../_fixtures/config');
+  config.errors = [];
+  config.warnings = [];
+});
+
+const {
+  checkBaseBranch,
+} = require('../../../../lib/workers/repository/init/base');
+
+describe('workers/repository/init/base', () => {
+  describe('checkBaseBranch()', () => {
+    it('errors', async () => {
+      config.baseBranch = 'some-base';
+      const res = await checkBaseBranch(config);
+      expect(res.errors).toHaveLength(1);
+    });
+    it('sets baseBranch', async () => {
+      config.baseBranch = 'ssome-base';
+      config.api.branchExists.mockReturnValue(true);
+      const res = await checkBaseBranch(config);
+      expect(res.errors).toHaveLength(0);
+      expect(config.api.setBaseBranch.mock.calls).toHaveLength(1);
+    });
+  });
+});
diff --git a/test/workers/repository/init/config.spec.js b/test/workers/repository/init/config.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..8a97de5210ac24e698f9c38189bb18293169cb59
--- /dev/null
+++ b/test/workers/repository/init/config.spec.js
@@ -0,0 +1,34 @@
+let config;
+beforeEach(() => {
+  jest.resetAllMocks();
+  config = require('../../../_fixtures/config');
+  config.errors = [];
+  config.warnings = [];
+});
+
+const {
+  mergeRenovateJson,
+} = require('../../../../lib/workers/repository/init/config');
+
+describe('workers/repository/init/config', () => {
+  describe('mergeRenovateJson()', () => {
+    it('returns config if not found', async () => {
+      const res = await mergeRenovateJson(config);
+      expect(res).toMatchObject(config);
+    });
+    it('returns error if cannot parse', async () => {
+      config.api.getFileContent.mockReturnValue('cannot parse');
+      const res = await mergeRenovateJson(config);
+      expect(res.errors).toHaveLength(1);
+      expect(res.errors[0]).toMatchSnapshot();
+    });
+    it('returns error if duplicate keys', async () => {
+      config.api.getFileContent.mockReturnValue(
+        '{ "enabled": true, "enabled": false }'
+      );
+      const res = await mergeRenovateJson(config);
+      expect(res.errors).toHaveLength(1);
+      expect(res.errors[0]).toMatchSnapshot();
+    });
+  });
+});
diff --git a/test/workers/repository/init/index.spec.js b/test/workers/repository/init/index.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..2b68c1607bb57829fe425d080d03d65deb1fd415
--- /dev/null
+++ b/test/workers/repository/init/index.spec.js
@@ -0,0 +1,14 @@
+jest
+  .enableAutomock()
+  .dontMock('chalk')
+  .dontMock('../../../../lib/workers/repository/init');
+
+const { initRepo } = require('../../../../lib/workers/repository/init');
+
+describe('workers/repository/init', () => {
+  describe('initRepo', () => {
+    it('runs', async () => {
+      await initRepo({}, null);
+    });
+  });
+});
diff --git a/test/workers/repository/init/semantic.spec.js b/test/workers/repository/init/semantic.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..944103753d194769073320a112c6a024001a08ba
--- /dev/null
+++ b/test/workers/repository/init/semantic.spec.js
@@ -0,0 +1,36 @@
+let config;
+beforeEach(() => {
+  jest.resetAllMocks();
+  config = require('../../../_fixtures/config');
+  config.errors = [];
+  config.warnings = [];
+});
+
+const {
+  detectSemanticCommits,
+} = require('../../../../lib/workers/repository/init/semantic');
+
+describe('workers/repository/init/semantic', () => {
+  describe('detectSemanticCommits()', () => {
+    it('returns config if already set', async () => {
+      config.semanticCommits = true;
+      const res = await detectSemanticCommits(config);
+      expect(res).toBe(config);
+    });
+    it('detects false if unknown', async () => {
+      config.semanticCommits = null;
+      config.api.getCommitMessages.mockReturnValue(['foo', 'bar']);
+      const res = await detectSemanticCommits(config);
+      expect(res.semanticCommits).toBe(false);
+    });
+    it('detects true if known', async () => {
+      config.semanticCommits = null;
+      config.api.getCommitMessages.mockReturnValue([
+        'fix: foo',
+        'refactor: bar',
+      ]);
+      const res = await detectSemanticCommits(config);
+      expect(res.semanticCommits).toBe(true);
+    });
+  });
+});
diff --git a/test/workers/repository/onboarding.spec.js b/test/workers/repository/onboarding.spec.js
deleted file mode 100644
index d3f610634a33188c48c2f657564ff0a386ba8408..0000000000000000000000000000000000000000
--- a/test/workers/repository/onboarding.spec.js
+++ /dev/null
@@ -1,73 +0,0 @@
-const onboarding = require('../../../lib/workers/repository/onboarding');
-const manager = require('../../../lib/manager');
-const logger = require('../../_fixtures/logger');
-const defaultConfig = require('../../../lib/config/defaults').getConfig();
-
-describe('lib/workers/repository/onboarding', () => {
-  describe('getOnboardingStatus(config)', () => {
-    let config;
-    beforeEach(() => {
-      config = { ...defaultConfig };
-      jest.resetAllMocks();
-      config.api = {
-        commitFilesToBranch: jest.fn(),
-        createPr: jest.fn(() => ({ displayNumber: 1 })),
-        getFileList: jest.fn(() => []),
-        findPr: jest.fn(),
-        getFileContent: jest.fn(),
-        getFileJson: jest.fn(() => ({})),
-        getPr: jest.fn(() => {}),
-        getCommitMessages: jest.fn(),
-      };
-      config.foundIgnoredPaths = true;
-      config.logger = logger;
-      config.detectedPackageFiles = true;
-    });
-    it('returns true if onboarding is false', async () => {
-      config.onboarding = false;
-      const res = await onboarding.getOnboardingStatus(config);
-      expect(res.repoIsOnboarded).toEqual(true);
-      expect(config.api.findPr.mock.calls.length).toBe(0);
-      expect(config.api.commitFilesToBranch.mock.calls.length).toBe(0);
-    });
-    it('returns true if renovate config present', async () => {
-      config.renovateJsonPresent = true;
-      const res = await onboarding.getOnboardingStatus(config);
-      expect(res.repoIsOnboarded).toEqual(true);
-      expect(config.api.findPr.mock.calls.length).toBe(0);
-      expect(config.api.commitFilesToBranch.mock.calls.length).toBe(0);
-    });
-    it('returns true if pr and pr is closed', async () => {
-      config.api.findPr.mockReturnValueOnce({ isClosed: true });
-      const res = await onboarding.getOnboardingStatus(config);
-      expect(res.repoIsOnboarded).toEqual(true);
-      expect(config.api.findPr.mock.calls.length).toBe(1);
-      expect(config.api.commitFilesToBranch.mock.calls.length).toBe(0);
-    });
-    it('skips commit files and returns false if open pr', async () => {
-      config.api.findPr.mockReturnValueOnce({ isClosed: false });
-      const res = await onboarding.getOnboardingStatus(config);
-      expect(res.repoIsOnboarded).toEqual(false);
-      expect(config.api.findPr.mock.calls.length).toBe(1);
-      expect(config.api.commitFilesToBranch.mock.calls.length).toBe(0);
-    });
-    it('commits files and returns false if no pr', async () => {
-      config.api.getFileList.mockReturnValueOnce(['package.json']);
-      const res = await onboarding.getOnboardingStatus(config);
-      expect(res.repoIsOnboarded).toEqual(false);
-      expect(config.api.findPr.mock.calls.length).toBe(1);
-      expect(config.api.commitFilesToBranch.mock.calls.length).toBe(1);
-      expect(config.api.commitFilesToBranch.mock.calls[0]).toMatchSnapshot();
-    });
-    it('throws if no packageFiles', async () => {
-      manager.detectPackageFiles = jest.fn(() => []);
-      let e;
-      try {
-        await onboarding.getOnboardingStatus(config);
-      } catch (err) {
-        e = err;
-      }
-      expect(e).toMatchSnapshot();
-    });
-  });
-});
diff --git a/test/workers/repository/onboarding/branch/index.spec.js b/test/workers/repository/onboarding/branch/index.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..7efcef64417f46963f4412780fd92b1b7be4b228
--- /dev/null
+++ b/test/workers/repository/onboarding/branch/index.spec.js
@@ -0,0 +1,61 @@
+const logger = require('../../../../_fixtures/logger');
+const defaultConfig = require('../../../../../lib/config/defaults').getConfig();
+const {
+  checkOnboardingBranch,
+} = require('../../../../../lib/workers/repository/onboarding/branch');
+
+describe('workers/repository/onboarding/branch', () => {
+  describe('checkOnboardingBranch', () => {
+    let config;
+    beforeEach(() => {
+      jest.resetAllMocks();
+      config = {
+        ...defaultConfig,
+        logger,
+        api: {
+          commitFilesToBranch: jest.fn(),
+          findPr: jest.fn(),
+          getFileList: jest.fn(() => []),
+          setBaseBranch: jest.fn(),
+        },
+      };
+    });
+    it('throws if no package files', async () => {
+      let e;
+      try {
+        await checkOnboardingBranch(config);
+      } catch (err) {
+        e = err;
+      }
+      expect(e).toBeDefined();
+    });
+    it('throws if fork', async () => {
+      config.isFork = true;
+      let e;
+      try {
+        await checkOnboardingBranch(config);
+      } catch (err) {
+        e = err;
+      }
+      expect(e).toBeDefined();
+    });
+    it('detects repo is onboarded via file', async () => {
+      config.api.getFileList.mockReturnValueOnce(['renovate.json']);
+      const res = await checkOnboardingBranch(config);
+      expect(res.repoIsOnboarded).toBe(true);
+    });
+    it('detects repo is onboarded via PR', async () => {
+      config.api.findPr.mockReturnValue(true);
+      const res = await checkOnboardingBranch(config);
+      expect(res.repoIsOnboarded).toBe(true);
+    });
+    it('creates onboaring branch', async () => {
+      config.api.getFileList.mockReturnValue(['package.json']);
+      config.api.commitFilesToBranch = jest.fn();
+      const res = await checkOnboardingBranch(config);
+      expect(res.repoIsOnboarded).toBe(false);
+      expect(res.branchList).toEqual(['renovate/configure']);
+      expect(config.api.setBaseBranch.mock.calls).toHaveLength(1);
+    });
+  });
+});
diff --git a/test/workers/repository/updates/__snapshots__/branchify.spec.js.snap b/test/workers/repository/updates/__snapshots__/branchify.spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..1c0fc16ff3cc144f956926d4a10f7d8175ff92de
--- /dev/null
+++ b/test/workers/repository/updates/__snapshots__/branchify.spec.js.snap
@@ -0,0 +1,2949 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`workers/repository/updates/branchify branchifyUpgrades() does not group if different compiled branch names 1`] = `
+Object {
+  "api": Object {
+    "addAssignees": [Function],
+    "addComment": [Function],
+    "addReviewers": [Function],
+    "branchExists": [Function],
+    "commitFilesToBranch": [Function],
+    "createPr": [Function],
+    "deleteBranch": [Function],
+    "deleteComment": [Function],
+    "editComment": [Function],
+    "ensureComment": [Function],
+    "ensureCommentRemoval": [Function],
+    "findPr": [Function],
+    "getAllRenovateBranches": [Function],
+    "getBranchCommit": [Function],
+    "getBranchLastCommitTime": [Function],
+    "getBranchPr": [Function],
+    "getBranchStatus": [Function],
+    "getBranchStatusCheck": [Function],
+    "getComments": [Function],
+    "getCommitDetails": [Function],
+    "getCommitMessages": [Function],
+    "getFile": [Function],
+    "getFileContent": [Function],
+    "getFileJson": [Function],
+    "getFileList": [Function],
+    "getPr": [Function],
+    "getPrList": [Function],
+    "getRepos": [Function],
+    "getSubDirectories": [Function],
+    "initRepo": [Function],
+    "isBranchStale": [Function],
+    "mergeBranch": [Function],
+    "mergePr": [Function],
+    "setBaseBranch": [Function],
+    "setBranchStatus": [Function],
+    "updatePr": [Function],
+  },
+  "assignees": Array [],
+  "autodiscover": false,
+  "automerge": false,
+  "automergeType": "pr",
+  "baseBranch": null,
+  "branchList": undefined,
+  "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.x",
+  "branchPrefix": "renovate/",
+  "branches": Array [
+    Object {
+      "branchName": "foo-1.1.0",
+      "depName": "foo",
+      "logger": Object {
+        "child": [Function],
+        "debug": [Function],
+        "error": [Function],
+        "fatal": [Function],
+        "info": [Function],
+        "trace": [Function],
+        "warn": [Function],
+      },
+      "prTitle": "some-title",
+      "upgrades": Array [
+        Object {
+          "branchName": "foo-1.1.0",
+          "depName": "foo",
+          "prTitle": "some-title",
+          "version": "1.1.0",
+        },
+      ],
+      "version": "1.1.0",
+    },
+    Object {
+      "branchName": "foo-2.0.0",
+      "depName": "foo",
+      "logger": Object {
+        "child": [Function],
+        "debug": [Function],
+        "error": [Function],
+        "fatal": [Function],
+        "info": [Function],
+        "trace": [Function],
+        "warn": [Function],
+      },
+      "prTitle": "some-title",
+      "upgrades": Array [
+        Object {
+          "branchName": "foo-2.0.0",
+          "depName": "foo",
+          "prTitle": "some-title",
+          "version": "2.0.0",
+        },
+      ],
+      "version": "2.0.0",
+    },
+    Object {
+      "branchName": "bar-1.1.0",
+      "depName": "bar",
+      "logger": Object {
+        "child": [Function],
+        "debug": [Function],
+        "error": [Function],
+        "fatal": [Function],
+        "info": [Function],
+        "trace": [Function],
+        "warn": [Function],
+      },
+      "prTitle": "some-title",
+      "upgrades": Array [
+        Object {
+          "branchName": "bar-1.1.0",
+          "depName": "bar",
+          "prTitle": "some-title",
+          "version": "1.1.0",
+        },
+      ],
+      "version": "1.1.0",
+    },
+  ],
+  "commitMessage": "Update dependency {{depName}} to v{{newVersion}}",
+  "dependencies": Object {
+    "semanticPrefix": "fix(deps):",
+  },
+  "description": Array [],
+  "devDependencies": Object {
+    "pin": Object {
+      "group": Object {
+        "commitMessage": "Pin devDependencies",
+        "prTitle": "Pin devDependencies",
+      },
+    },
+  },
+  "digest": Object {
+    "semanticPrefix": "refactor(deps):",
+  },
+  "docker": Object {
+    "branchName": "{{branchPrefix}}docker-{{depNameSanitized}}-{{newVersionMajor}}.x",
+    "commitMessage": "Update {{depName}} to tag {{newTag}}",
+    "digest": Object {
+      "branchName": "{{branchPrefix}}docker-{{depNameSanitized}}-{{currentTag}}",
+      "commitMessage": "Update {{depName}}:{{currentTag}} digest",
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Docker base image \`{{depName}}@{{currentTag}}\` to the latest digest (\`{{newDigest}}\`).
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Update Dockerfile {{depName}} image {{currentTag}} digest ({{newDigestShort}})",
+    },
+    "enabled": true,
+    "group": Object {
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Dockerfiles to use image digests.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: \`{{upgrade.newDigest}}\`
+{{/each}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Update Docker {{groupName}} digests",
+    },
+    "major": Object {
+      "enabled": false,
+    },
+    "minor": Object {
+      "enabled": false,
+    },
+    "patch": Object {
+      "enabled": false,
+    },
+    "pin": Object {
+      "branchName": "{{branchPrefix}}docker-pin-{{depNameSanitized}}-{{currentTag}}",
+      "group": Object {
+        "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Dockerfiles to use image digests.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: \`{{upgrade.newDigest}}\`
+{{/each}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+        "prTitle": "Pin Docker digests",
+      },
+      "groupName": "Pin Docker Digests",
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Docker base image \`{{depName}}@{{currentTag}}\` to use a digest (\`{{newDigest}}\`).
+This digest will then be kept updated via Pull Requests whenever the image is updated on the Docker registry.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Pin Dockerfile {{depName}}@{{currentTag}} image digest",
+    },
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Docker base image \`{{depName}}\` from tag \`{{currentTag}}\` to new tag \`{{newTag}}\`.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Update {{depName}} Dockerfile tag to {{#if isMajor}}v{{newVersionMajor}}{{else}}v{{newTag}}{{/if}}",
+  },
+  "enabled": true,
+  "encrypted": null,
+  "endpoint": null,
+  "errors": Array [],
+  "excludePackageNames": Array [],
+  "excludePackagePatterns": Array [],
+  "extends": Array [],
+  "group": Object {
+    "branchName": "{{branchPrefix}}{{groupSlug}}",
+    "commitMessage": "Renovate {{groupName}} packages",
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request renovates the package group \\"{{groupName}}\\".
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: from \`{{upgrade.currentVersion}}\` to \`{{upgrade.newVersion}}\`
+{{/each}}
+
+{{#unless isPin}}
+### Commits
+
+{{#each upgrades as |upgrade|}}
+{{#if upgrade.releases.length}}
+<details>
+<summary>{{upgrade.githubName}}</summary>
+{{#each upgrade.releases as |release|}}
+
+#### {{release.version}}
+{{#each release.commits as |commit|}}
+-   [\`{{commit.shortSha}}\`]({{commit.url}}){{commit.message}}
+{{/each}}
+{{/each}}
+
+</details>
+{{/if}}
+{{/each}}
+{{/unless}}
+<br />
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Renovate {{groupName}} packages",
+    "recreateClosed": true,
+  },
+  "groupName": null,
+  "groupSlug": null,
+  "ignoreDeps": Array [],
+  "ignoreFuture": true,
+  "ignoreNpmrcFile": false,
+  "ignorePaths": Array [
+    "**/node_modules/**",
+  ],
+  "ignoreUnstable": true,
+  "labels": Array [],
+  "lazyGrouping": true,
+  "lockFileMaintenance": Object {
+    "branchName": "{{branchPrefix}}lock-file-maintenance",
+    "commitMessage": "Update lock file",
+    "enabled": false,
+    "groupName": null,
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates \`package.json\` lock files to use the latest dependency versions.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Lock file maintenance",
+    "recreateClosed": true,
+    "schedule": Array [
+      "before 5am on monday",
+    ],
+  },
+  "logFile": null,
+  "logFileLevel": "debug",
+  "logLevel": "info",
+  "logger": Object {
+    "child": [Function],
+    "debug": [Function],
+    "error": [Function],
+    "fatal": [Function],
+    "info": [Function],
+    "trace": [Function],
+    "warn": [Function],
+  },
+  "major": Object {},
+  "meteor": Object {
+    "enabled": true,
+  },
+  "minor": Object {},
+  "npm": Object {
+    "enabled": true,
+    "pin": Object {
+      "automerge": true,
+    },
+  },
+  "npmrc": null,
+  "onboarding": true,
+  "optionalDependencies": Object {},
+  "packageFiles": Array [],
+  "packageNames": Array [],
+  "packagePatterns": Array [],
+  "packageRules": Array [],
+  "patch": Object {
+    "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.{{newVersionMinor}}.x",
+  },
+  "peerDependencies": Object {
+    "enabled": false,
+  },
+  "pin": Object {
+    "group": Object {
+      "commitMessage": "Pin Dependencies",
+      "prTitle": "{{groupName}}",
+      "semanticPrefix": "refactor(deps):",
+    },
+    "groupName": "Pin Dependencies",
+    "rebaseStalePrs": true,
+    "recreateClosed": true,
+    "unpublishSafe": false,
+  },
+  "pinDigests": true,
+  "pinVersions": null,
+  "platform": "github",
+  "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request {{#if isRollback}}rolls back{{else}}updates{{/if}} dependency {{#if repositoryUrl}}[{{depName}}]({{repositoryUrl}}){{else}}\`{{depName}}\`{{/if}} from \`{{#unless isRange}}{{#unless isPin}}v{{/unless}}{{/unless}}{{currentVersion}}\` to \`{{#unless isRange}}v{{/unless}}{{newVersion}}\`{{#if isRollback}}. This is necessary and important because \`v{{currentVersion}}\` cannot be found in the npm registry - probably because of it being unpublished.{{/if}}
+{{#if releases.length}}
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+### Commits
+
+<details>
+<summary>{{githubName}}</summary>
+
+{{#each releases as |release|}}
+#### {{release.version}}
+{{#each release.commits as |commit|}}
+-   [\`{{commit.shortSha}}\`]({{commit.url}}) {{commit.message}}
+{{/each}}
+{{/each}}
+
+</details>
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+  "prCreation": "immediate",
+  "prNotPendingHours": 12,
+  "prTitle": "{{#if isPin}}Pin{{else}}{{#if isRollback}}Roll back{{else}}Update{{/if}}{{/if}} dependency {{depName}} to {{#if isRange}}{{newVersion}}{{else}}{{#if isMajor}}v{{newVersionMajor}}{{else}}v{{newVersion}}{{/if}}{{/if}}",
+  "privateKey": null,
+  "rebaseStalePrs": false,
+  "recreateClosed": false,
+  "renovateFork": false,
+  "repositories": Array [],
+  "requiredStatusChecks": Array [],
+  "respectLatest": true,
+  "reviewers": Array [],
+  "schedule": Array [],
+  "semanticCommits": null,
+  "semanticPrefix": "chore(deps):",
+  "separateMajorReleases": true,
+  "separatePatchReleases": false,
+  "timezone": null,
+  "token": null,
+  "unpublishSafe": false,
+  "updateNotScheduled": true,
+  "upgrades": null,
+  "warnings": Array [],
+  "yarnrc": null,
+}
+`;
+
+exports[`workers/repository/updates/branchify branchifyUpgrades() groups if same compiled branch names 1`] = `
+Object {
+  "api": Object {
+    "addAssignees": [Function],
+    "addComment": [Function],
+    "addReviewers": [Function],
+    "branchExists": [Function],
+    "commitFilesToBranch": [Function],
+    "createPr": [Function],
+    "deleteBranch": [Function],
+    "deleteComment": [Function],
+    "editComment": [Function],
+    "ensureComment": [Function],
+    "ensureCommentRemoval": [Function],
+    "findPr": [Function],
+    "getAllRenovateBranches": [Function],
+    "getBranchCommit": [Function],
+    "getBranchLastCommitTime": [Function],
+    "getBranchPr": [Function],
+    "getBranchStatus": [Function],
+    "getBranchStatusCheck": [Function],
+    "getComments": [Function],
+    "getCommitDetails": [Function],
+    "getCommitMessages": [Function],
+    "getFile": [Function],
+    "getFileContent": [Function],
+    "getFileJson": [Function],
+    "getFileList": [Function],
+    "getPr": [Function],
+    "getPrList": [Function],
+    "getRepos": [Function],
+    "getSubDirectories": [Function],
+    "initRepo": [Function],
+    "isBranchStale": [Function],
+    "mergeBranch": [Function],
+    "mergePr": [Function],
+    "setBaseBranch": [Function],
+    "setBranchStatus": [Function],
+    "updatePr": [Function],
+  },
+  "assignees": Array [],
+  "autodiscover": false,
+  "automerge": false,
+  "automergeType": "pr",
+  "baseBranch": null,
+  "branchList": undefined,
+  "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.x",
+  "branchPrefix": "renovate/",
+  "branches": Array [
+    Object {
+      "branchName": "foo",
+      "depName": "foo",
+      "logger": Object {
+        "child": [Function],
+        "debug": [Function],
+        "error": [Function],
+        "fatal": [Function],
+        "info": [Function],
+        "trace": [Function],
+        "warn": [Function],
+      },
+      "prTitle": "some-title",
+      "upgrades": Array [
+        Object {
+          "branchName": "foo",
+          "depName": "foo",
+          "prTitle": "some-title",
+          "version": "2.0.0",
+        },
+        Object {
+          "branchName": "foo",
+          "depName": "foo",
+          "prTitle": "some-title",
+          "version": "1.1.0",
+        },
+      ],
+      "version": "2.0.0",
+    },
+    Object {
+      "branchName": "bar-1.1.0",
+      "depName": "bar",
+      "logger": Object {
+        "child": [Function],
+        "debug": [Function],
+        "error": [Function],
+        "fatal": [Function],
+        "info": [Function],
+        "trace": [Function],
+        "warn": [Function],
+      },
+      "prTitle": "some-title",
+      "upgrades": Array [
+        Object {
+          "branchName": "bar-1.1.0",
+          "depName": "bar",
+          "prTitle": "some-title",
+          "version": "1.1.0",
+        },
+      ],
+      "version": "1.1.0",
+    },
+  ],
+  "commitMessage": "Update dependency {{depName}} to v{{newVersion}}",
+  "dependencies": Object {
+    "semanticPrefix": "fix(deps):",
+  },
+  "description": Array [],
+  "devDependencies": Object {
+    "pin": Object {
+      "group": Object {
+        "commitMessage": "Pin devDependencies",
+        "prTitle": "Pin devDependencies",
+      },
+    },
+  },
+  "digest": Object {
+    "semanticPrefix": "refactor(deps):",
+  },
+  "docker": Object {
+    "branchName": "{{branchPrefix}}docker-{{depNameSanitized}}-{{newVersionMajor}}.x",
+    "commitMessage": "Update {{depName}} to tag {{newTag}}",
+    "digest": Object {
+      "branchName": "{{branchPrefix}}docker-{{depNameSanitized}}-{{currentTag}}",
+      "commitMessage": "Update {{depName}}:{{currentTag}} digest",
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Docker base image \`{{depName}}@{{currentTag}}\` to the latest digest (\`{{newDigest}}\`).
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Update Dockerfile {{depName}} image {{currentTag}} digest ({{newDigestShort}})",
+    },
+    "enabled": true,
+    "group": Object {
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Dockerfiles to use image digests.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: \`{{upgrade.newDigest}}\`
+{{/each}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Update Docker {{groupName}} digests",
+    },
+    "major": Object {
+      "enabled": false,
+    },
+    "minor": Object {
+      "enabled": false,
+    },
+    "patch": Object {
+      "enabled": false,
+    },
+    "pin": Object {
+      "branchName": "{{branchPrefix}}docker-pin-{{depNameSanitized}}-{{currentTag}}",
+      "group": Object {
+        "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Dockerfiles to use image digests.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: \`{{upgrade.newDigest}}\`
+{{/each}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+        "prTitle": "Pin Docker digests",
+      },
+      "groupName": "Pin Docker Digests",
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Docker base image \`{{depName}}@{{currentTag}}\` to use a digest (\`{{newDigest}}\`).
+This digest will then be kept updated via Pull Requests whenever the image is updated on the Docker registry.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Pin Dockerfile {{depName}}@{{currentTag}} image digest",
+    },
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Docker base image \`{{depName}}\` from tag \`{{currentTag}}\` to new tag \`{{newTag}}\`.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Update {{depName}} Dockerfile tag to {{#if isMajor}}v{{newVersionMajor}}{{else}}v{{newTag}}{{/if}}",
+  },
+  "enabled": true,
+  "encrypted": null,
+  "endpoint": null,
+  "errors": Array [],
+  "excludePackageNames": Array [],
+  "excludePackagePatterns": Array [],
+  "extends": Array [],
+  "group": Object {
+    "branchName": "{{branchPrefix}}{{groupSlug}}",
+    "commitMessage": "Renovate {{groupName}} packages",
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request renovates the package group \\"{{groupName}}\\".
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: from \`{{upgrade.currentVersion}}\` to \`{{upgrade.newVersion}}\`
+{{/each}}
+
+{{#unless isPin}}
+### Commits
+
+{{#each upgrades as |upgrade|}}
+{{#if upgrade.releases.length}}
+<details>
+<summary>{{upgrade.githubName}}</summary>
+{{#each upgrade.releases as |release|}}
+
+#### {{release.version}}
+{{#each release.commits as |commit|}}
+-   [\`{{commit.shortSha}}\`]({{commit.url}}){{commit.message}}
+{{/each}}
+{{/each}}
+
+</details>
+{{/if}}
+{{/each}}
+{{/unless}}
+<br />
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Renovate {{groupName}} packages",
+    "recreateClosed": true,
+  },
+  "groupName": null,
+  "groupSlug": null,
+  "ignoreDeps": Array [],
+  "ignoreFuture": true,
+  "ignoreNpmrcFile": false,
+  "ignorePaths": Array [
+    "**/node_modules/**",
+  ],
+  "ignoreUnstable": true,
+  "labels": Array [],
+  "lazyGrouping": true,
+  "lockFileMaintenance": Object {
+    "branchName": "{{branchPrefix}}lock-file-maintenance",
+    "commitMessage": "Update lock file",
+    "enabled": false,
+    "groupName": null,
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates \`package.json\` lock files to use the latest dependency versions.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Lock file maintenance",
+    "recreateClosed": true,
+    "schedule": Array [
+      "before 5am on monday",
+    ],
+  },
+  "logFile": null,
+  "logFileLevel": "debug",
+  "logLevel": "info",
+  "logger": Object {
+    "child": [Function],
+    "debug": [Function],
+    "error": [Function],
+    "fatal": [Function],
+    "info": [Function],
+    "trace": [Function],
+    "warn": [Function],
+  },
+  "major": Object {},
+  "meteor": Object {
+    "enabled": true,
+  },
+  "minor": Object {},
+  "npm": Object {
+    "enabled": true,
+    "pin": Object {
+      "automerge": true,
+    },
+  },
+  "npmrc": null,
+  "onboarding": true,
+  "optionalDependencies": Object {},
+  "packageFiles": Array [],
+  "packageNames": Array [],
+  "packagePatterns": Array [],
+  "packageRules": Array [],
+  "patch": Object {
+    "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.{{newVersionMinor}}.x",
+  },
+  "peerDependencies": Object {
+    "enabled": false,
+  },
+  "pin": Object {
+    "group": Object {
+      "commitMessage": "Pin Dependencies",
+      "prTitle": "{{groupName}}",
+      "semanticPrefix": "refactor(deps):",
+    },
+    "groupName": "Pin Dependencies",
+    "rebaseStalePrs": true,
+    "recreateClosed": true,
+    "unpublishSafe": false,
+  },
+  "pinDigests": true,
+  "pinVersions": null,
+  "platform": "github",
+  "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request {{#if isRollback}}rolls back{{else}}updates{{/if}} dependency {{#if repositoryUrl}}[{{depName}}]({{repositoryUrl}}){{else}}\`{{depName}}\`{{/if}} from \`{{#unless isRange}}{{#unless isPin}}v{{/unless}}{{/unless}}{{currentVersion}}\` to \`{{#unless isRange}}v{{/unless}}{{newVersion}}\`{{#if isRollback}}. This is necessary and important because \`v{{currentVersion}}\` cannot be found in the npm registry - probably because of it being unpublished.{{/if}}
+{{#if releases.length}}
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+### Commits
+
+<details>
+<summary>{{githubName}}</summary>
+
+{{#each releases as |release|}}
+#### {{release.version}}
+{{#each release.commits as |commit|}}
+-   [\`{{commit.shortSha}}\`]({{commit.url}}) {{commit.message}}
+{{/each}}
+{{/each}}
+
+</details>
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+  "prCreation": "immediate",
+  "prNotPendingHours": 12,
+  "prTitle": "{{#if isPin}}Pin{{else}}{{#if isRollback}}Roll back{{else}}Update{{/if}}{{/if}} dependency {{depName}} to {{#if isRange}}{{newVersion}}{{else}}{{#if isMajor}}v{{newVersionMajor}}{{else}}v{{newVersion}}{{/if}}{{/if}}",
+  "privateKey": null,
+  "rebaseStalePrs": false,
+  "recreateClosed": false,
+  "renovateFork": false,
+  "repositories": Array [],
+  "requiredStatusChecks": Array [],
+  "respectLatest": true,
+  "reviewers": Array [],
+  "schedule": Array [],
+  "semanticCommits": null,
+  "semanticPrefix": "chore(deps):",
+  "separateMajorReleases": true,
+  "separatePatchReleases": false,
+  "timezone": null,
+  "token": null,
+  "unpublishSafe": false,
+  "updateNotScheduled": true,
+  "upgrades": null,
+  "warnings": Array [],
+  "yarnrc": null,
+}
+`;
+
+exports[`workers/repository/updates/branchify branchifyUpgrades() groups if same compiled group name 1`] = `
+Object {
+  "api": Object {
+    "addAssignees": [Function],
+    "addComment": [Function],
+    "addReviewers": [Function],
+    "branchExists": [Function],
+    "commitFilesToBranch": [Function],
+    "createPr": [Function],
+    "deleteBranch": [Function],
+    "deleteComment": [Function],
+    "editComment": [Function],
+    "ensureComment": [Function],
+    "ensureCommentRemoval": [Function],
+    "findPr": [Function],
+    "getAllRenovateBranches": [Function],
+    "getBranchCommit": [Function],
+    "getBranchLastCommitTime": [Function],
+    "getBranchPr": [Function],
+    "getBranchStatus": [Function],
+    "getBranchStatusCheck": [Function],
+    "getComments": [Function],
+    "getCommitDetails": [Function],
+    "getCommitMessages": [Function],
+    "getFile": [Function],
+    "getFileContent": [Function],
+    "getFileJson": [Function],
+    "getFileList": [Function],
+    "getPr": [Function],
+    "getPrList": [Function],
+    "getRepos": [Function],
+    "getSubDirectories": [Function],
+    "initRepo": [Function],
+    "isBranchStale": [Function],
+    "mergeBranch": [Function],
+    "mergePr": [Function],
+    "setBaseBranch": [Function],
+    "setBranchStatus": [Function],
+    "updatePr": [Function],
+  },
+  "assignees": Array [],
+  "autodiscover": false,
+  "automerge": false,
+  "automergeType": "pr",
+  "baseBranch": null,
+  "branchList": undefined,
+  "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.x",
+  "branchPrefix": "renovate/",
+  "branches": Array [
+    Object {
+      "branchName": "renovate/my-group",
+      "depName": "bar",
+      "groupName": "My Group",
+      "groupSlug": "my-group",
+      "logger": Object {
+        "child": [Function],
+        "debug": [Function],
+        "error": [Function],
+        "fatal": [Function],
+        "info": [Function],
+        "trace": [Function],
+        "warn": [Function],
+      },
+      "prTitle": "some-title",
+      "upgrades": Array [
+        Object {
+          "branchName": "renovate/my-group",
+          "depName": "bar",
+          "groupName": "My Group",
+          "groupSlug": "my-group",
+          "prTitle": "some-title",
+          "version": "1.1.0",
+        },
+        Object {
+          "branchName": "renovate/my-group",
+          "depName": "foo",
+          "groupName": "My Group",
+          "groupSlug": "my-group",
+          "prTitle": "some-title",
+          "version": "1.1.0",
+        },
+      ],
+      "version": "1.1.0",
+    },
+    Object {
+      "branchName": "foo",
+      "depName": "foo",
+      "logger": Object {
+        "child": [Function],
+        "debug": [Function],
+        "error": [Function],
+        "fatal": [Function],
+        "info": [Function],
+        "trace": [Function],
+        "warn": [Function],
+      },
+      "prTitle": "some-title",
+      "upgrades": Array [
+        Object {
+          "branchName": "foo",
+          "depName": "foo",
+          "prTitle": "some-title",
+          "version": "2.0.0",
+        },
+      ],
+      "version": "2.0.0",
+    },
+  ],
+  "commitMessage": "Update dependency {{depName}} to v{{newVersion}}",
+  "dependencies": Object {
+    "semanticPrefix": "fix(deps):",
+  },
+  "description": Array [],
+  "devDependencies": Object {
+    "pin": Object {
+      "group": Object {
+        "commitMessage": "Pin devDependencies",
+        "prTitle": "Pin devDependencies",
+      },
+    },
+  },
+  "digest": Object {
+    "semanticPrefix": "refactor(deps):",
+  },
+  "docker": Object {
+    "branchName": "{{branchPrefix}}docker-{{depNameSanitized}}-{{newVersionMajor}}.x",
+    "commitMessage": "Update {{depName}} to tag {{newTag}}",
+    "digest": Object {
+      "branchName": "{{branchPrefix}}docker-{{depNameSanitized}}-{{currentTag}}",
+      "commitMessage": "Update {{depName}}:{{currentTag}} digest",
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Docker base image \`{{depName}}@{{currentTag}}\` to the latest digest (\`{{newDigest}}\`).
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Update Dockerfile {{depName}} image {{currentTag}} digest ({{newDigestShort}})",
+    },
+    "enabled": true,
+    "group": Object {
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Dockerfiles to use image digests.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: \`{{upgrade.newDigest}}\`
+{{/each}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Update Docker {{groupName}} digests",
+    },
+    "major": Object {
+      "enabled": false,
+    },
+    "minor": Object {
+      "enabled": false,
+    },
+    "patch": Object {
+      "enabled": false,
+    },
+    "pin": Object {
+      "branchName": "{{branchPrefix}}docker-pin-{{depNameSanitized}}-{{currentTag}}",
+      "group": Object {
+        "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Dockerfiles to use image digests.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: \`{{upgrade.newDigest}}\`
+{{/each}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+        "prTitle": "Pin Docker digests",
+      },
+      "groupName": "Pin Docker Digests",
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Docker base image \`{{depName}}@{{currentTag}}\` to use a digest (\`{{newDigest}}\`).
+This digest will then be kept updated via Pull Requests whenever the image is updated on the Docker registry.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Pin Dockerfile {{depName}}@{{currentTag}} image digest",
+    },
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Docker base image \`{{depName}}\` from tag \`{{currentTag}}\` to new tag \`{{newTag}}\`.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Update {{depName}} Dockerfile tag to {{#if isMajor}}v{{newVersionMajor}}{{else}}v{{newTag}}{{/if}}",
+  },
+  "enabled": true,
+  "encrypted": null,
+  "endpoint": null,
+  "errors": Array [],
+  "excludePackageNames": Array [],
+  "excludePackagePatterns": Array [],
+  "extends": Array [],
+  "group": Object {
+    "branchName": "{{branchPrefix}}{{groupSlug}}",
+    "commitMessage": "Renovate {{groupName}} packages",
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request renovates the package group \\"{{groupName}}\\".
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: from \`{{upgrade.currentVersion}}\` to \`{{upgrade.newVersion}}\`
+{{/each}}
+
+{{#unless isPin}}
+### Commits
+
+{{#each upgrades as |upgrade|}}
+{{#if upgrade.releases.length}}
+<details>
+<summary>{{upgrade.githubName}}</summary>
+{{#each upgrade.releases as |release|}}
+
+#### {{release.version}}
+{{#each release.commits as |commit|}}
+-   [\`{{commit.shortSha}}\`]({{commit.url}}){{commit.message}}
+{{/each}}
+{{/each}}
+
+</details>
+{{/if}}
+{{/each}}
+{{/unless}}
+<br />
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Renovate {{groupName}} packages",
+    "recreateClosed": true,
+  },
+  "groupName": null,
+  "groupSlug": null,
+  "ignoreDeps": Array [],
+  "ignoreFuture": true,
+  "ignoreNpmrcFile": false,
+  "ignorePaths": Array [
+    "**/node_modules/**",
+  ],
+  "ignoreUnstable": true,
+  "labels": Array [],
+  "lazyGrouping": true,
+  "lockFileMaintenance": Object {
+    "branchName": "{{branchPrefix}}lock-file-maintenance",
+    "commitMessage": "Update lock file",
+    "enabled": false,
+    "groupName": null,
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates \`package.json\` lock files to use the latest dependency versions.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Lock file maintenance",
+    "recreateClosed": true,
+    "schedule": Array [
+      "before 5am on monday",
+    ],
+  },
+  "logFile": null,
+  "logFileLevel": "debug",
+  "logLevel": "info",
+  "logger": Object {
+    "child": [Function],
+    "debug": [Function],
+    "error": [Function],
+    "fatal": [Function],
+    "info": [Function],
+    "trace": [Function],
+    "warn": [Function],
+  },
+  "major": Object {},
+  "meteor": Object {
+    "enabled": true,
+  },
+  "minor": Object {},
+  "npm": Object {
+    "enabled": true,
+    "pin": Object {
+      "automerge": true,
+    },
+  },
+  "npmrc": null,
+  "onboarding": true,
+  "optionalDependencies": Object {},
+  "packageFiles": Array [],
+  "packageNames": Array [],
+  "packagePatterns": Array [],
+  "packageRules": Array [],
+  "patch": Object {
+    "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.{{newVersionMinor}}.x",
+  },
+  "peerDependencies": Object {
+    "enabled": false,
+  },
+  "pin": Object {
+    "group": Object {
+      "commitMessage": "Pin Dependencies",
+      "prTitle": "{{groupName}}",
+      "semanticPrefix": "refactor(deps):",
+    },
+    "groupName": "Pin Dependencies",
+    "rebaseStalePrs": true,
+    "recreateClosed": true,
+    "unpublishSafe": false,
+  },
+  "pinDigests": true,
+  "pinVersions": null,
+  "platform": "github",
+  "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request {{#if isRollback}}rolls back{{else}}updates{{/if}} dependency {{#if repositoryUrl}}[{{depName}}]({{repositoryUrl}}){{else}}\`{{depName}}\`{{/if}} from \`{{#unless isRange}}{{#unless isPin}}v{{/unless}}{{/unless}}{{currentVersion}}\` to \`{{#unless isRange}}v{{/unless}}{{newVersion}}\`{{#if isRollback}}. This is necessary and important because \`v{{currentVersion}}\` cannot be found in the npm registry - probably because of it being unpublished.{{/if}}
+{{#if releases.length}}
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+### Commits
+
+<details>
+<summary>{{githubName}}</summary>
+
+{{#each releases as |release|}}
+#### {{release.version}}
+{{#each release.commits as |commit|}}
+-   [\`{{commit.shortSha}}\`]({{commit.url}}) {{commit.message}}
+{{/each}}
+{{/each}}
+
+</details>
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+  "prCreation": "immediate",
+  "prNotPendingHours": 12,
+  "prTitle": "{{#if isPin}}Pin{{else}}{{#if isRollback}}Roll back{{else}}Update{{/if}}{{/if}} dependency {{depName}} to {{#if isRange}}{{newVersion}}{{else}}{{#if isMajor}}v{{newVersionMajor}}{{else}}v{{newVersion}}{{/if}}{{/if}}",
+  "privateKey": null,
+  "rebaseStalePrs": false,
+  "recreateClosed": false,
+  "renovateFork": false,
+  "repositories": Array [],
+  "requiredStatusChecks": Array [],
+  "respectLatest": true,
+  "reviewers": Array [],
+  "schedule": Array [],
+  "semanticCommits": null,
+  "semanticPrefix": "chore(deps):",
+  "separateMajorReleases": true,
+  "separatePatchReleases": false,
+  "timezone": null,
+  "token": null,
+  "unpublishSafe": false,
+  "updateNotScheduled": true,
+  "upgrades": null,
+  "warnings": Array [],
+  "yarnrc": null,
+}
+`;
+
+exports[`workers/repository/updates/branchify branchifyUpgrades() mixes errors and warnings 1`] = `
+Object {
+  "api": Object {
+    "addAssignees": [Function],
+    "addComment": [Function],
+    "addReviewers": [Function],
+    "branchExists": [Function],
+    "commitFilesToBranch": [Function],
+    "createPr": [Function],
+    "deleteBranch": [Function],
+    "deleteComment": [Function],
+    "editComment": [Function],
+    "ensureComment": [Function],
+    "ensureCommentRemoval": [Function],
+    "findPr": [Function],
+    "getAllRenovateBranches": [Function],
+    "getBranchCommit": [Function],
+    "getBranchLastCommitTime": [Function],
+    "getBranchPr": [Function],
+    "getBranchStatus": [Function],
+    "getBranchStatusCheck": [Function],
+    "getComments": [Function],
+    "getCommitDetails": [Function],
+    "getCommitMessages": [Function],
+    "getFile": [Function],
+    "getFileContent": [Function],
+    "getFileJson": [Function],
+    "getFileList": [Function],
+    "getPr": [Function],
+    "getPrList": [Function],
+    "getRepos": [Function],
+    "getSubDirectories": [Function],
+    "initRepo": [Function],
+    "isBranchStale": [Function],
+    "mergeBranch": [Function],
+    "mergePr": [Function],
+    "setBaseBranch": [Function],
+    "setBranchStatus": [Function],
+    "updatePr": [Function],
+  },
+  "assignees": Array [],
+  "autodiscover": false,
+  "automerge": false,
+  "automergeType": "pr",
+  "baseBranch": null,
+  "branchList": undefined,
+  "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.x",
+  "branchPrefix": "renovate/",
+  "branches": Array [
+    Object {
+      "branchName": "foo-1.1.0",
+      "logger": Object {
+        "child": [Function],
+        "debug": [Function],
+        "error": [Function],
+        "fatal": [Function],
+        "info": [Function],
+        "trace": [Function],
+        "warn": [Function],
+      },
+      "prTitle": "some-title",
+      "upgrades": Array [
+        Object {
+          "branchName": "foo-1.1.0",
+          "prTitle": "some-title",
+          "version": "1.1.0",
+        },
+      ],
+      "version": "1.1.0",
+    },
+    Object {
+      "branchName": "bar-1.1.0",
+      "logger": Object {
+        "child": [Function],
+        "debug": [Function],
+        "error": [Function],
+        "fatal": [Function],
+        "info": [Function],
+        "trace": [Function],
+        "warn": [Function],
+      },
+      "prTitle": "some-title",
+      "upgrades": Array [
+        Object {
+          "branchName": "bar-1.1.0",
+          "prTitle": "some-title",
+          "version": "1.1.0",
+        },
+      ],
+      "version": "1.1.0",
+    },
+  ],
+  "commitMessage": "Update dependency {{depName}} to v{{newVersion}}",
+  "dependencies": Object {
+    "semanticPrefix": "fix(deps):",
+  },
+  "description": Array [],
+  "devDependencies": Object {
+    "pin": Object {
+      "group": Object {
+        "commitMessage": "Pin devDependencies",
+        "prTitle": "Pin devDependencies",
+      },
+    },
+  },
+  "digest": Object {
+    "semanticPrefix": "refactor(deps):",
+  },
+  "docker": Object {
+    "branchName": "{{branchPrefix}}docker-{{depNameSanitized}}-{{newVersionMajor}}.x",
+    "commitMessage": "Update {{depName}} to tag {{newTag}}",
+    "digest": Object {
+      "branchName": "{{branchPrefix}}docker-{{depNameSanitized}}-{{currentTag}}",
+      "commitMessage": "Update {{depName}}:{{currentTag}} digest",
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Docker base image \`{{depName}}@{{currentTag}}\` to the latest digest (\`{{newDigest}}\`).
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Update Dockerfile {{depName}} image {{currentTag}} digest ({{newDigestShort}})",
+    },
+    "enabled": true,
+    "group": Object {
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Dockerfiles to use image digests.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: \`{{upgrade.newDigest}}\`
+{{/each}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Update Docker {{groupName}} digests",
+    },
+    "major": Object {
+      "enabled": false,
+    },
+    "minor": Object {
+      "enabled": false,
+    },
+    "patch": Object {
+      "enabled": false,
+    },
+    "pin": Object {
+      "branchName": "{{branchPrefix}}docker-pin-{{depNameSanitized}}-{{currentTag}}",
+      "group": Object {
+        "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Dockerfiles to use image digests.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: \`{{upgrade.newDigest}}\`
+{{/each}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+        "prTitle": "Pin Docker digests",
+      },
+      "groupName": "Pin Docker Digests",
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Docker base image \`{{depName}}@{{currentTag}}\` to use a digest (\`{{newDigest}}\`).
+This digest will then be kept updated via Pull Requests whenever the image is updated on the Docker registry.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Pin Dockerfile {{depName}}@{{currentTag}} image digest",
+    },
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Docker base image \`{{depName}}\` from tag \`{{currentTag}}\` to new tag \`{{newTag}}\`.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Update {{depName}} Dockerfile tag to {{#if isMajor}}v{{newVersionMajor}}{{else}}v{{newTag}}{{/if}}",
+  },
+  "enabled": true,
+  "encrypted": null,
+  "endpoint": null,
+  "errors": Array [
+    Object {
+      "type": "error",
+    },
+  ],
+  "excludePackageNames": Array [],
+  "excludePackagePatterns": Array [],
+  "extends": Array [],
+  "group": Object {
+    "branchName": "{{branchPrefix}}{{groupSlug}}",
+    "commitMessage": "Renovate {{groupName}} packages",
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request renovates the package group \\"{{groupName}}\\".
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: from \`{{upgrade.currentVersion}}\` to \`{{upgrade.newVersion}}\`
+{{/each}}
+
+{{#unless isPin}}
+### Commits
+
+{{#each upgrades as |upgrade|}}
+{{#if upgrade.releases.length}}
+<details>
+<summary>{{upgrade.githubName}}</summary>
+{{#each upgrade.releases as |release|}}
+
+#### {{release.version}}
+{{#each release.commits as |commit|}}
+-   [\`{{commit.shortSha}}\`]({{commit.url}}){{commit.message}}
+{{/each}}
+{{/each}}
+
+</details>
+{{/if}}
+{{/each}}
+{{/unless}}
+<br />
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Renovate {{groupName}} packages",
+    "recreateClosed": true,
+  },
+  "groupName": null,
+  "groupSlug": null,
+  "ignoreDeps": Array [],
+  "ignoreFuture": true,
+  "ignoreNpmrcFile": false,
+  "ignorePaths": Array [
+    "**/node_modules/**",
+  ],
+  "ignoreUnstable": true,
+  "labels": Array [],
+  "lazyGrouping": true,
+  "lockFileMaintenance": Object {
+    "branchName": "{{branchPrefix}}lock-file-maintenance",
+    "commitMessage": "Update lock file",
+    "enabled": false,
+    "groupName": null,
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates \`package.json\` lock files to use the latest dependency versions.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Lock file maintenance",
+    "recreateClosed": true,
+    "schedule": Array [
+      "before 5am on monday",
+    ],
+  },
+  "logFile": null,
+  "logFileLevel": "debug",
+  "logLevel": "info",
+  "logger": Object {
+    "child": [Function],
+    "debug": [Function],
+    "error": [Function],
+    "fatal": [Function],
+    "info": [Function],
+    "trace": [Function],
+    "warn": [Function],
+  },
+  "major": Object {},
+  "meteor": Object {
+    "enabled": true,
+  },
+  "minor": Object {},
+  "npm": Object {
+    "enabled": true,
+    "pin": Object {
+      "automerge": true,
+    },
+  },
+  "npmrc": null,
+  "onboarding": true,
+  "optionalDependencies": Object {},
+  "packageFiles": Array [],
+  "packageNames": Array [],
+  "packagePatterns": Array [],
+  "packageRules": Array [],
+  "patch": Object {
+    "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.{{newVersionMinor}}.x",
+  },
+  "peerDependencies": Object {
+    "enabled": false,
+  },
+  "pin": Object {
+    "group": Object {
+      "commitMessage": "Pin Dependencies",
+      "prTitle": "{{groupName}}",
+      "semanticPrefix": "refactor(deps):",
+    },
+    "groupName": "Pin Dependencies",
+    "rebaseStalePrs": true,
+    "recreateClosed": true,
+    "unpublishSafe": false,
+  },
+  "pinDigests": true,
+  "pinVersions": null,
+  "platform": "github",
+  "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request {{#if isRollback}}rolls back{{else}}updates{{/if}} dependency {{#if repositoryUrl}}[{{depName}}]({{repositoryUrl}}){{else}}\`{{depName}}\`{{/if}} from \`{{#unless isRange}}{{#unless isPin}}v{{/unless}}{{/unless}}{{currentVersion}}\` to \`{{#unless isRange}}v{{/unless}}{{newVersion}}\`{{#if isRollback}}. This is necessary and important because \`v{{currentVersion}}\` cannot be found in the npm registry - probably because of it being unpublished.{{/if}}
+{{#if releases.length}}
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+### Commits
+
+<details>
+<summary>{{githubName}}</summary>
+
+{{#each releases as |release|}}
+#### {{release.version}}
+{{#each release.commits as |commit|}}
+-   [\`{{commit.shortSha}}\`]({{commit.url}}) {{commit.message}}
+{{/each}}
+{{/each}}
+
+</details>
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+  "prCreation": "immediate",
+  "prNotPendingHours": 12,
+  "prTitle": "{{#if isPin}}Pin{{else}}{{#if isRollback}}Roll back{{else}}Update{{/if}}{{/if}} dependency {{depName}} to {{#if isRange}}{{newVersion}}{{else}}{{#if isMajor}}v{{newVersionMajor}}{{else}}v{{newVersion}}{{/if}}{{/if}}",
+  "privateKey": null,
+  "rebaseStalePrs": false,
+  "recreateClosed": false,
+  "renovateFork": false,
+  "repositories": Array [],
+  "requiredStatusChecks": Array [],
+  "respectLatest": true,
+  "reviewers": Array [],
+  "schedule": Array [],
+  "semanticCommits": null,
+  "semanticPrefix": "chore(deps):",
+  "separateMajorReleases": true,
+  "separatePatchReleases": false,
+  "timezone": null,
+  "token": null,
+  "unpublishSafe": false,
+  "updateNotScheduled": true,
+  "upgrades": null,
+  "warnings": Array [
+    Object {
+      "branchName": "foo-{{version}}",
+      "prTitle": "some-title",
+      "type": "warning",
+      "version": "2.0.0",
+    },
+  ],
+  "yarnrc": null,
+}
+`;
+
+exports[`workers/repository/updates/branchify branchifyUpgrades() returns one branch if one input 1`] = `
+Object {
+  "api": Object {
+    "addAssignees": [Function],
+    "addComment": [Function],
+    "addReviewers": [Function],
+    "branchExists": [Function],
+    "commitFilesToBranch": [Function],
+    "createPr": [Function],
+    "deleteBranch": [Function],
+    "deleteComment": [Function],
+    "editComment": [Function],
+    "ensureComment": [Function],
+    "ensureCommentRemoval": [Function],
+    "findPr": [Function],
+    "getAllRenovateBranches": [Function],
+    "getBranchCommit": [Function],
+    "getBranchLastCommitTime": [Function],
+    "getBranchPr": [Function],
+    "getBranchStatus": [Function],
+    "getBranchStatusCheck": [Function],
+    "getComments": [Function],
+    "getCommitDetails": [Function],
+    "getCommitMessages": [Function],
+    "getFile": [Function],
+    "getFileContent": [Function],
+    "getFileJson": [Function],
+    "getFileList": [Function],
+    "getPr": [Function],
+    "getPrList": [Function],
+    "getRepos": [Function],
+    "getSubDirectories": [Function],
+    "initRepo": [Function],
+    "isBranchStale": [Function],
+    "mergeBranch": [Function],
+    "mergePr": [Function],
+    "setBaseBranch": [Function],
+    "setBranchStatus": [Function],
+    "updatePr": [Function],
+  },
+  "assignees": Array [],
+  "autodiscover": false,
+  "automerge": false,
+  "automergeType": "pr",
+  "baseBranch": null,
+  "branchList": Array [
+    "foo-1.1.0",
+  ],
+  "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.x",
+  "branchPrefix": "renovate/",
+  "branches": Array [
+    Object {
+      "branchName": "foo-1.1.0",
+      "depName": "foo",
+      "logger": Object {
+        "child": [Function],
+        "debug": [Function],
+        "error": [Function],
+        "fatal": [Function],
+        "info": [Function],
+        "trace": [Function],
+        "warn": [Function],
+      },
+      "prTitle": "some-title",
+      "upgrades": Array [
+        Object {
+          "branchName": "foo-1.1.0",
+          "depName": "foo",
+          "prTitle": "some-title",
+          "version": "1.1.0",
+        },
+      ],
+      "version": "1.1.0",
+    },
+  ],
+  "commitMessage": "Update dependency {{depName}} to v{{newVersion}}",
+  "dependencies": Object {
+    "semanticPrefix": "fix(deps):",
+  },
+  "description": Array [],
+  "devDependencies": Object {
+    "pin": Object {
+      "group": Object {
+        "commitMessage": "Pin devDependencies",
+        "prTitle": "Pin devDependencies",
+      },
+    },
+  },
+  "digest": Object {
+    "semanticPrefix": "refactor(deps):",
+  },
+  "docker": Object {
+    "branchName": "{{branchPrefix}}docker-{{depNameSanitized}}-{{newVersionMajor}}.x",
+    "commitMessage": "Update {{depName}} to tag {{newTag}}",
+    "digest": Object {
+      "branchName": "{{branchPrefix}}docker-{{depNameSanitized}}-{{currentTag}}",
+      "commitMessage": "Update {{depName}}:{{currentTag}} digest",
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Docker base image \`{{depName}}@{{currentTag}}\` to the latest digest (\`{{newDigest}}\`).
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Update Dockerfile {{depName}} image {{currentTag}} digest ({{newDigestShort}})",
+    },
+    "enabled": true,
+    "group": Object {
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Dockerfiles to use image digests.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: \`{{upgrade.newDigest}}\`
+{{/each}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Update Docker {{groupName}} digests",
+    },
+    "major": Object {
+      "enabled": false,
+    },
+    "minor": Object {
+      "enabled": false,
+    },
+    "patch": Object {
+      "enabled": false,
+    },
+    "pin": Object {
+      "branchName": "{{branchPrefix}}docker-pin-{{depNameSanitized}}-{{currentTag}}",
+      "group": Object {
+        "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Dockerfiles to use image digests.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: \`{{upgrade.newDigest}}\`
+{{/each}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+        "prTitle": "Pin Docker digests",
+      },
+      "groupName": "Pin Docker Digests",
+      "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Docker base image \`{{depName}}@{{currentTag}}\` to use a digest (\`{{newDigest}}\`).
+This digest will then be kept updated via Pull Requests whenever the image is updated on the Docker registry.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+      "prTitle": "Pin Dockerfile {{depName}}@{{currentTag}} image digest",
+    },
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Docker base image \`{{depName}}\` from tag \`{{currentTag}}\` to new tag \`{{newTag}}\`.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Update {{depName}} Dockerfile tag to {{#if isMajor}}v{{newVersionMajor}}{{else}}v{{newTag}}{{/if}}",
+  },
+  "enabled": true,
+  "encrypted": null,
+  "endpoint": null,
+  "errors": Array [],
+  "excludePackageNames": Array [],
+  "excludePackagePatterns": Array [],
+  "extends": Array [],
+  "group": Object {
+    "branchName": "{{branchPrefix}}{{groupSlug}}",
+    "commitMessage": "Renovate {{groupName}} packages",
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request renovates the package group \\"{{groupName}}\\".
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#each upgrades as |upgrade|}}
+-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{depName}}\`{{/if}}: from \`{{upgrade.currentVersion}}\` to \`{{upgrade.newVersion}}\`
+{{/each}}
+
+{{#unless isPin}}
+### Commits
+
+{{#each upgrades as |upgrade|}}
+{{#if upgrade.releases.length}}
+<details>
+<summary>{{upgrade.githubName}}</summary>
+{{#each upgrade.releases as |release|}}
+
+#### {{release.version}}
+{{#each release.commits as |commit|}}
+-   [\`{{commit.shortSha}}\`]({{commit.url}}){{commit.message}}
+{{/each}}
+{{/each}}
+
+</details>
+{{/if}}
+{{/each}}
+{{/unless}}
+<br />
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Renovate {{groupName}} packages",
+    "recreateClosed": true,
+  },
+  "groupName": null,
+  "groupSlug": null,
+  "ignoreDeps": Array [],
+  "ignoreFuture": true,
+  "ignoreNpmrcFile": false,
+  "ignorePaths": Array [
+    "**/node_modules/**",
+  ],
+  "ignoreUnstable": true,
+  "labels": Array [],
+  "lazyGrouping": true,
+  "lockFileMaintenance": Object {
+    "branchName": "{{branchPrefix}}lock-file-maintenance",
+    "commitMessage": "Update lock file",
+    "enabled": false,
+    "groupName": null,
+    "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates \`package.json\` lock files to use the latest dependency versions.
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+    "prTitle": "Lock file maintenance",
+    "recreateClosed": true,
+    "schedule": Array [
+      "before 5am on monday",
+    ],
+  },
+  "logFile": null,
+  "logFileLevel": "debug",
+  "logLevel": "info",
+  "logger": Object {
+    "child": [Function],
+    "debug": [Function],
+    "error": [Function],
+    "fatal": [Function],
+    "info": [Function],
+    "trace": [Function],
+    "warn": [Function],
+  },
+  "major": Object {},
+  "meteor": Object {
+    "enabled": true,
+  },
+  "minor": Object {},
+  "npm": Object {
+    "enabled": true,
+    "pin": Object {
+      "automerge": true,
+    },
+  },
+  "npmrc": null,
+  "onboarding": true,
+  "optionalDependencies": Object {},
+  "packageFiles": Array [],
+  "packageNames": Array [],
+  "packagePatterns": Array [],
+  "packageRules": Array [],
+  "patch": Object {
+    "branchName": "{{branchPrefix}}{{depNameSanitized}}-{{newVersionMajor}}.{{newVersionMinor}}.x",
+  },
+  "peerDependencies": Object {
+    "enabled": false,
+  },
+  "pin": Object {
+    "group": Object {
+      "commitMessage": "Pin Dependencies",
+      "prTitle": "{{groupName}}",
+      "semanticPrefix": "refactor(deps):",
+    },
+    "groupName": "Pin Dependencies",
+    "rebaseStalePrs": true,
+    "recreateClosed": true,
+    "unpublishSafe": false,
+  },
+  "pinDigests": true,
+  "pinVersions": null,
+  "platform": "github",
+  "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request {{#if isRollback}}rolls back{{else}}updates{{/if}} dependency {{#if repositoryUrl}}[{{depName}}]({{repositoryUrl}}){{else}}\`{{depName}}\`{{/if}} from \`{{#unless isRange}}{{#unless isPin}}v{{/unless}}{{/unless}}{{currentVersion}}\` to \`{{#unless isRange}}v{{/unless}}{{newVersion}}\`{{#if isRollback}}. This is necessary and important because \`v{{currentVersion}}\` cannot be found in the npm registry - probably because of it being unpublished.{{/if}}
+{{#if releases.length}}
+
+{{#if schedule}}
+**Note**: This PR was created on a configured schedule (\\"{{schedule}}\\"{{#if timezone}} in timezone \`{{timezone}}\`{{/if}}) and will not receive updates outside those times.
+{{/if}}
+
+### Commits
+
+<details>
+<summary>{{githubName}}</summary>
+
+{{#each releases as |release|}}
+#### {{release.version}}
+{{#each release.commits as |commit|}}
+-   [\`{{commit.shortSha}}\`]({{commit.url}}) {{commit.message}}
+{{/each}}
+{{/each}}
+
+</details>
+{{/if}}
+
+{{#if hasErrors}}
+
+---
+
+### Errors
+
+Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.
+
+{{#each errors as |error|}}
+-   \`{{error.depName}}\`: {{error.message}}
+{{/each}}
+{{/if}}
+
+{{#if hasWarnings}}
+
+---
+
+### Warnings
+
+Please make sure the following warnings are safe to ignore:
+
+{{#each warnings as |warning|}}
+-   \`{{warning.depName}}\`: {{warning.message}}
+{{/each}}
+{{/if}}
+
+---
+
+This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).",
+  "prCreation": "immediate",
+  "prNotPendingHours": 12,
+  "prTitle": "{{#if isPin}}Pin{{else}}{{#if isRollback}}Roll back{{else}}Update{{/if}}{{/if}} dependency {{depName}} to {{#if isRange}}{{newVersion}}{{else}}{{#if isMajor}}v{{newVersionMajor}}{{else}}v{{newVersion}}{{/if}}{{/if}}",
+  "privateKey": null,
+  "rebaseStalePrs": false,
+  "recreateClosed": false,
+  "renovateFork": false,
+  "repoIsOnboarded": true,
+  "repositories": Array [],
+  "requiredStatusChecks": Array [],
+  "respectLatest": true,
+  "reviewers": Array [],
+  "schedule": Array [],
+  "semanticCommits": null,
+  "semanticPrefix": "chore(deps):",
+  "separateMajorReleases": true,
+  "separatePatchReleases": false,
+  "timezone": null,
+  "token": null,
+  "unpublishSafe": false,
+  "updateNotScheduled": true,
+  "upgrades": null,
+  "warnings": Array [],
+  "yarnrc": null,
+}
+`;
diff --git a/test/workers/repository/updates/__snapshots__/generate.spec.js.snap b/test/workers/repository/updates/__snapshots__/generate.spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..f9dddb3d209ebd243e2b8edbf7baab6e4d06eb57
--- /dev/null
+++ b/test/workers/repository/updates/__snapshots__/generate.spec.js.snap
@@ -0,0 +1,67 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`workers/repository/updates/generate generateBranchConfig() does not group single upgrade 1`] = `
+Object {
+  "branchName": "some-branch",
+  "depName": "some-dep",
+  "foo": 1,
+  "prTitle": "some-prefix: some-title",
+  "semanticCommits": true,
+  "semanticPrefix": "some-prefix:",
+  "upgrades": Array [
+    Object {
+      "branchName": "some-branch",
+      "depName": "some-dep",
+      "foo": 1,
+      "prTitle": "some-prefix: some-title",
+      "semanticCommits": true,
+      "semanticPrefix": "some-prefix:",
+    },
+  ],
+}
+`;
+
+exports[`workers/repository/updates/generate generateBranchConfig() groups multiple upgrades 1`] = `
+Object {
+  "branchName": "some-branch",
+  "depName": "some-dep",
+  "foo": 2,
+  "groupName": "some-group",
+  "prTitle": "some-title",
+  "upgrades": Array [
+    Object {
+      "branchName": "some-branch",
+      "depName": "some-dep",
+      "foo": 2,
+      "groupName": "some-group",
+      "prTitle": "some-title",
+    },
+    Object {
+      "branchName": "some-branch",
+      "depName": "some-other-dep",
+      "foo": 2,
+      "groupName": "some-group",
+      "prTitle": "some-title",
+    },
+  ],
+}
+`;
+
+exports[`workers/repository/updates/generate generateBranchConfig() groups single upgrade if not lazyGrouping 1`] = `
+Object {
+  "branchName": "some-branch",
+  "depName": "some-dep",
+  "foo": 2,
+  "groupName": "some-group",
+  "prTitle": "some-title",
+  "upgrades": Array [
+    Object {
+      "branchName": "some-branch",
+      "depName": "some-dep",
+      "foo": 2,
+      "groupName": "some-group",
+      "prTitle": "some-title",
+    },
+  ],
+}
+`;
diff --git a/test/workers/repository/updates/branchify.spec.js b/test/workers/repository/updates/branchify.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..5cedad618cda955ddb14a617b7595c0cf6999e5e
--- /dev/null
+++ b/test/workers/repository/updates/branchify.spec.js
@@ -0,0 +1,142 @@
+let config;
+beforeEach(() => {
+  jest.resetAllMocks();
+  config = { ...require('../../../_fixtures/config') };
+  config.errors = [];
+  config.warnings = [];
+});
+
+const {
+  branchifyUpgrades,
+} = require('../../../../lib/workers/repository/updates/branchify');
+
+describe('workers/repository/updates/branchify', () => {
+  describe('branchifyUpgrades()', () => {
+    it('returns empty', async () => {
+      config.upgrades = [];
+      const res = await branchifyUpgrades(config);
+      expect(res.branches).toEqual([]);
+    });
+    it('returns one branch if one input', async () => {
+      config.upgrades = [
+        {
+          depName: 'foo',
+          branchName: 'foo-{{version}}',
+          version: '1.1.0',
+          prTitle: 'some-title',
+        },
+      ];
+      config.repoIsOnboarded = true;
+      const res = await branchifyUpgrades(config);
+      expect(Object.keys(res.branches).length).toBe(1);
+      expect(res).toMatchSnapshot();
+    });
+    it('does not group if different compiled branch names', async () => {
+      config.upgrades = [
+        {
+          depName: 'foo',
+          branchName: 'foo-{{version}}',
+          version: '1.1.0',
+          prTitle: 'some-title',
+        },
+        {
+          depName: 'foo',
+          branchName: 'foo-{{version}}',
+          version: '2.0.0',
+          prTitle: 'some-title',
+        },
+        {
+          depName: 'bar',
+          branchName: 'bar-{{version}}',
+          version: '1.1.0',
+          prTitle: 'some-title',
+        },
+      ];
+      const res = await branchifyUpgrades(config);
+      expect(Object.keys(res.branches).length).toBe(3);
+      expect(res).toMatchSnapshot();
+    });
+    it('groups if same compiled branch names', async () => {
+      config.upgrades = [
+        {
+          depName: 'foo',
+          branchName: 'foo',
+          version: '1.1.0',
+          prTitle: 'some-title',
+        },
+        {
+          depName: 'foo',
+          branchName: 'foo',
+          version: '2.0.0',
+          prTitle: 'some-title',
+        },
+        {
+          depName: 'bar',
+          branchName: 'bar-{{version}}',
+          version: '1.1.0',
+          prTitle: 'some-title',
+        },
+      ];
+      const res = await branchifyUpgrades(config);
+      expect(Object.keys(res.branches).length).toBe(2);
+      expect(res).toMatchSnapshot();
+    });
+    it('groups if same compiled group name', async () => {
+      config.upgrades = [
+        {
+          depName: 'foo',
+          branchName: 'foo',
+          prTitle: 'some-title',
+          version: '1.1.0',
+          groupName: 'My Group',
+          group: { branchName: 'renovate/{{groupSlug}}' },
+        },
+        {
+          depName: 'foo',
+          branchName: 'foo',
+          prTitle: 'some-title',
+          version: '2.0.0',
+        },
+        {
+          depName: 'bar',
+          branchName: 'bar-{{version}}',
+          prTitle: 'some-title',
+          version: '1.1.0',
+          groupName: 'My Group',
+          group: { branchName: 'renovate/my-group' },
+        },
+      ];
+      const res = await branchifyUpgrades(config);
+      expect(Object.keys(res.branches).length).toBe(2);
+      expect(res).toMatchSnapshot();
+    });
+    it('mixes errors and warnings', async () => {
+      config.upgrades = [
+        {
+          type: 'error',
+        },
+        {
+          branchName: 'foo-{{version}}',
+          prTitle: 'some-title',
+          version: '1.1.0',
+        },
+        {
+          type: 'warning',
+          branchName: 'foo-{{version}}',
+          prTitle: 'some-title',
+          version: '2.0.0',
+        },
+        {
+          branchName: 'bar-{{version}}',
+          prTitle: 'some-title',
+          version: '1.1.0',
+        },
+      ];
+      const res = await branchifyUpgrades(config);
+      expect(Object.keys(res.branches).length).toBe(2);
+      expect(res.errors).toHaveLength(1);
+      expect(res.warnings).toHaveLength(1);
+      expect(res).toMatchSnapshot();
+    });
+  });
+});
diff --git a/test/workers/repository/updates/determine.spec.js b/test/workers/repository/updates/determine.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..6311d37432c49d010e6fc72175b9452faaa7242e
--- /dev/null
+++ b/test/workers/repository/updates/determine.spec.js
@@ -0,0 +1,56 @@
+jest.mock('../../../../lib/workers/package-file');
+
+const packageFileWorker = require('../../../../lib/workers/package-file');
+
+let config;
+beforeEach(() => {
+  jest.resetAllMocks();
+  config = require('../../../_fixtures/config');
+});
+
+const {
+  determineRepoUpgrades,
+} = require('../../../../lib/workers/repository/updates/determine');
+
+describe('workers/repository/updates/determine', () => {
+  describe('determineRepoUpgrades(config)', () => {
+    it('returns empty array if none found', async () => {
+      config.packageFiles = [
+        {
+          packageFile: 'package.json',
+        },
+        {
+          packageFile: 'backend/package.json',
+        },
+      ];
+      packageFileWorker.renovatePackageFile.mockReturnValue([]);
+      const res = await determineRepoUpgrades(config);
+      expect(res.upgrades).toHaveLength(0);
+    });
+    it('returns array if upgrades found', async () => {
+      config.packageFiles = [
+        {
+          packageFile: 'Dockerfile',
+        },
+        {
+          packageFile: 'backend/package.json',
+        },
+        {
+          packageFile: 'frontend/package.js',
+        },
+      ];
+      packageFileWorker.renovateDockerfile.mockReturnValueOnce([
+        { depName: 'a' },
+      ]);
+      packageFileWorker.renovatePackageFile.mockReturnValueOnce([
+        { depName: 'b' },
+        { depName: 'c' },
+      ]);
+      packageFileWorker.renovateMeteorPackageFile.mockReturnValueOnce([
+        { foo: 'd' },
+      ]);
+      const res = await determineRepoUpgrades(config);
+      expect(res.upgrades).toHaveLength(4);
+    });
+  });
+});
diff --git a/test/workers/repository/updates/generate.spec.js b/test/workers/repository/updates/generate.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..28b0d8aa8f2a4ee7b3d3abd62450ea7e50e10492
--- /dev/null
+++ b/test/workers/repository/updates/generate.spec.js
@@ -0,0 +1,113 @@
+let config;
+beforeEach(() => {
+  jest.resetAllMocks();
+  config = require('../../../_fixtures/config');
+});
+
+const {
+  generateBranchConfig,
+} = require('../../../../lib/workers/repository/updates/generate');
+
+describe('workers/repository/updates/generate', () => {
+  describe('generateBranchConfig()', () => {
+    it('does not group single upgrade', () => {
+      const branch = [
+        {
+          depName: 'some-dep',
+          groupName: 'some-group',
+          branchName: 'some-branch',
+          prTitle: 'some-title',
+          semanticCommits: true,
+          semanticPrefix: 'some-prefix:',
+          lazyGrouping: true,
+          foo: 1,
+          group: {
+            foo: 2,
+          },
+        },
+      ];
+      const res = generateBranchConfig(branch, config.logger);
+      expect(res.foo).toBe(1);
+      expect(res.groupName).toBeUndefined();
+      expect(res).toMatchSnapshot();
+    });
+    it('groups single upgrade if not lazyGrouping', () => {
+      const branch = [
+        {
+          depName: 'some-dep',
+          groupName: 'some-group',
+          branchName: 'some-branch',
+          prTitle: 'some-title',
+          lazyGrouping: false,
+          foo: 1,
+          group: {
+            foo: 2,
+          },
+        },
+      ];
+      const res = generateBranchConfig(branch, config.logger);
+      expect(res.foo).toBe(2);
+      expect(res.groupName).toBeDefined();
+      expect(res).toMatchSnapshot();
+    });
+    it('does not group same upgrades', () => {
+      const branch = [
+        {
+          depName: 'some-dep',
+          groupName: 'some-group',
+          branchName: 'some-branch',
+          prTitle: 'some-title',
+          lazyGrouping: true,
+          foo: 1,
+          group: {
+            foo: 2,
+          },
+        },
+        {
+          depName: 'some-dep',
+          groupName: 'some-group',
+          branchName: 'some-branch',
+          prTitle: 'some-title',
+          lazyGrouping: true,
+          foo: 1,
+          group: {
+            foo: 2,
+          },
+        },
+      ];
+      const res = generateBranchConfig(branch, config.logger);
+      expect(res.foo).toBe(1);
+      expect(res.groupName).toBeUndefined();
+    });
+    it('groups multiple upgrades', () => {
+      const branch = [
+        {
+          depName: 'some-dep',
+          groupName: 'some-group',
+          branchName: 'some-branch',
+          prTitle: 'some-title',
+          lazyGrouping: true,
+          foo: 1,
+          group: {
+            foo: 2,
+          },
+        },
+        {
+          depName: 'some-other-dep',
+          groupName: 'some-group',
+          branchName: 'some-branch',
+          prTitle: 'some-title',
+          lazyGrouping: true,
+          foo: 1,
+          group: {
+            foo: 2,
+          },
+        },
+      ];
+      const res = generateBranchConfig(branch, config.logger);
+      expect(res.foo).toBe(2);
+      expect(res.groupName).toBeDefined();
+      expect(res).toMatchSnapshot();
+    });
+  });
+});
diff --git a/test/workers/repository/updates/index.spec.js b/test/workers/repository/updates/index.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..f8c3a784296741cd1fd6b65eb85efebe44f7f5b8
--- /dev/null
+++ b/test/workers/repository/updates/index.spec.js
@@ -0,0 +1,20 @@
+jest.mock('../../../../lib/workers/repository/updates/determine');
+jest.mock('../../../../lib/workers/repository/updates/branchify');
+
+let config;
+beforeEach(() => {
+  jest.resetAllMocks();
+  config = require('../../../_fixtures/config');
+});
+
+const {
+  determineUpdates,
+} = require('../../../../lib/workers/repository/updates');
+
+describe('workers/repository/updates', () => {
+  describe('determineUpdates()', () => {
+    it('runs', async () => {
+      await determineUpdates(config, 'some-token');
+    });
+  });
+});
diff --git a/test/workers/repository/upgrades.spec.js b/test/workers/repository/upgrades.spec.js
deleted file mode 100644
index 90af11b79b1490cc460191482743766e6c22be72..0000000000000000000000000000000000000000
--- a/test/workers/repository/upgrades.spec.js
+++ /dev/null
@@ -1,306 +0,0 @@
-const upgrades = require('../../../lib/workers/repository/upgrades');
-const packageFileWorker = require('../../../lib/workers/package-file');
-const logger = require('../../_fixtures/logger');
-
-jest.mock('../../../lib/workers/package-file');
-
-describe('workers/repository/upgrades', () => {
-  describe('determineRepoUpgrades(config)', () => {
-    let config;
-    beforeEach(() => {
-      config = {
-        logger,
-      };
-    });
-    it('returns empty array if no packageFiles', async () => {
-      config.packageFiles = [];
-      const res = await upgrades.determineRepoUpgrades(config);
-      expect(res.length).toBe(0);
-    });
-    it('returns empty array if none found', async () => {
-      config.packageFiles = [
-        'package.json',
-        {
-          packageFile: 'backend/package.json',
-        },
-      ];
-      packageFileWorker.renovatePackageFile.mockReturnValue([]);
-      const res = await upgrades.determineRepoUpgrades(config);
-      expect(res.length).toBe(0);
-    });
-    it('returns array if upgrades found', async () => {
-      config.packageFiles = [
-        'Dockerfile',
-        {
-          packageFile: 'backend/package.json',
-        },
-        {
-          packageFile: 'frontend/package.js',
-        },
-      ];
-      packageFileWorker.renovateDockerfile.mockReturnValueOnce([
-        { depName: 'a' },
-      ]);
-      packageFileWorker.renovatePackageFile.mockReturnValueOnce([
-        { depName: 'b' },
-        { depName: 'c' },
-      ]);
-      packageFileWorker.renovateMeteorPackageFile.mockReturnValueOnce([
-        { foo: 'd' },
-      ]);
-      const res = await upgrades.determineRepoUpgrades(config);
-      expect(res).toHaveLength(4);
-    });
-  });
-  describe('generateConfig(branchUpgrades)', () => {
-    it('does not group single upgrade', () => {
-      const branchUpgrades = [
-        {
-          depName: 'some-dep',
-          groupName: 'some-group',
-          branchName: 'some-branch',
-          prTitle: 'some-title',
-          semanticCommits: true,
-          semanticPrefix: 'some-prefix:',
-          lazyGrouping: true,
-          foo: 1,
-          group: {
-            foo: 2,
-          },
-        },
-      ];
-      const res = upgrades.generateConfig(branchUpgrades);
-      expect(res.foo).toBe(1);
-      expect(res.groupName).toBeUndefined();
-      expect(res).toMatchSnapshot();
-    });
-    it('groups single upgrade if not lazyGrouping', () => {
-      const branchUpgrades = [
-        {
-          depName: 'some-dep',
-          groupName: 'some-group',
-          branchName: 'some-branch',
-          prTitle: 'some-title',
-          lazyGrouping: false,
-          foo: 1,
-          group: {
-            foo: 2,
-          },
-        },
-      ];
-      const res = upgrades.generateConfig(branchUpgrades);
-      expect(res.foo).toBe(2);
-      expect(res.groupName).toBeDefined();
-      expect(res).toMatchSnapshot();
-    });
-    it('does not group same upgrades', () => {
-      const branchUpgrades = [
-        {
-          depName: 'some-dep',
-          groupName: 'some-group',
-          branchName: 'some-branch',
-          prTitle: 'some-title',
-          lazyGrouping: true,
-          foo: 1,
-          group: {
-            foo: 2,
-          },
-        },
-        {
-          depName: 'some-dep',
-          groupName: 'some-group',
-          branchName: 'some-branch',
-          prTitle: 'some-title',
-          lazyGrouping: true,
-          foo: 1,
-          group: {
-            foo: 2,
-          },
-        },
-      ];
-      const res = upgrades.generateConfig(branchUpgrades);
-      expect(res.foo).toBe(1);
-      expect(res.groupName).toBeUndefined();
-    });
-    it('groups multiple upgrades', () => {
-      const branchUpgrades = [
-        {
-          depName: 'some-dep',
-          groupName: 'some-group',
-          branchName: 'some-branch',
-          prTitle: 'some-title',
-          lazyGrouping: true,
-          foo: 1,
-          group: {
-            foo: 2,
-          },
-        },
-        {
-          depName: 'some-other-dep',
-          groupName: 'some-group',
-          branchName: 'some-branch',
-          prTitle: 'some-title',
-          lazyGrouping: true,
-          foo: 1,
-          group: {
-            foo: 2,
-          },
-        },
-      ];
-      const res = upgrades.generateConfig(branchUpgrades);
-      expect(res.foo).toBe(2);
-      expect(res.groupName).toBeDefined();
-      expect(res).toMatchSnapshot();
-    });
-  });
-  describe('groupByBranch(upgrades)', () => {
-    it('returns empty object if no input array', async () => {
-      const res = await upgrades.groupByBranch([]);
-      expect(res).toMatchSnapshot();
-    });
-    it('returns one branch if one input', async () => {
-      const input = [
-        {
-          depName: 'foo',
-          branchName: 'foo-{{version}}',
-          version: '1.1.0',
-          prTitle: 'some-title',
-        },
-      ];
-      const res = await upgrades.groupByBranch(input);
-      expect(Object.keys(res.branchUpgrades).length).toBe(1);
-      expect(res).toMatchSnapshot();
-    });
-    it('does not group if different compiled branch names', async () => {
-      const input = [
-        {
-          depName: 'foo',
-          branchName: 'foo-{{version}}',
-          version: '1.1.0',
-          prTitle: 'some-title',
-        },
-        {
-          depName: 'foo',
-          branchName: 'foo-{{version}}',
-          version: '2.0.0',
-          prTitle: 'some-title',
-        },
-        {
-          depName: 'bar',
-          branchName: 'bar-{{version}}',
-          version: '1.1.0',
-          prTitle: 'some-title',
-        },
-      ];
-      const res = await upgrades.groupByBranch(input);
-      expect(Object.keys(res.branchUpgrades).length).toBe(3);
-      expect(res).toMatchSnapshot();
-    });
-    it('groups if same compiled branch names', async () => {
-      const input = [
-        {
-          depName: 'foo',
-          branchName: 'foo',
-          version: '1.1.0',
-          prTitle: 'some-title',
-        },
-        {
-          depName: 'foo',
-          branchName: 'foo',
-          version: '2.0.0',
-          prTitle: 'some-title',
-        },
-        {
-          depName: 'bar',
-          branchName: 'bar-{{version}}',
-          version: '1.1.0',
-          prTitle: 'some-title',
-        },
-      ];
-      const res = await upgrades.groupByBranch(input);
-      expect(Object.keys(res.branchUpgrades).length).toBe(2);
-      expect(res).toMatchSnapshot();
-    });
-    it('groups if same compiled group name', async () => {
-      const input = [
-        {
-          depName: 'foo',
-          branchName: 'foo',
-          prTitle: 'some-title',
-          version: '1.1.0',
-          groupName: 'My Group',
-          group: { branchName: 'renovate/{{groupSlug}}' },
-        },
-        {
-          depName: 'foo',
-          branchName: 'foo',
-          prTitle: 'some-title',
-          version: '2.0.0',
-        },
-        {
-          depName: 'bar',
-          branchName: 'bar-{{version}}',
-          prTitle: 'some-title',
-          version: '1.1.0',
-          groupName: 'My Group',
-          group: { branchName: 'renovate/my-group' },
-        },
-      ];
-      const res = await upgrades.groupByBranch(input);
-      expect(Object.keys(res.branchUpgrades).length).toBe(2);
-      expect(res).toMatchSnapshot();
-    });
-    it('mixes errors and warnings', async () => {
-      const input = [
-        {
-          type: 'error',
-        },
-        {
-          branchName: 'foo-{{version}}',
-          prTitle: 'some-title',
-          version: '1.1.0',
-        },
-        {
-          type: 'warning',
-          branchName: 'foo-{{version}}',
-          prTitle: 'some-title',
-          version: '2.0.0',
-        },
-        {
-          branchName: 'bar-{{version}}',
-          prTitle: 'some-title',
-          version: '1.1.0',
-        },
-      ];
-      const res = await upgrades.groupByBranch(input);
-      expect(Object.keys(res.branchUpgrades).length).toBe(2);
-      expect(res.errors).toHaveLength(1);
-      expect(res.warnings).toHaveLength(1);
-      expect(res).toMatchSnapshot();
-    });
-  });
-  describe('branchifyUpgrades(upgrades, parentLogger)', () => {
-    it('returns empty', async () => {
-      upgrades.groupByBranch = jest.fn(() => ({
-        branchUpgrades: {},
-        errors: [],
-        warnings: [],
-      }));
-      const res = await upgrades.branchifyUpgrades({});
-      expect(res.upgrades).toEqual([]);
-    });
-    it('processes multiple branches', async () => {
-      upgrades.groupByBranch = jest.fn(() => ({
-        branchUpgrades: {
-          a: [],
-          b: [],
-        },
-        errors: [],
-        warnings: [],
-      }));
-      upgrades.generateConfig = jest.fn(() => ({}));
-      const res = await upgrades.branchifyUpgrades({});
-      expect(res.upgrades).toHaveLength(2);
-    });
-  });
-});
diff --git a/test/workers/repository/write.spec.js b/test/workers/repository/write.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..6f8e3358118ff5e300845b932511bb45e846e567
--- /dev/null
+++ b/test/workers/repository/write.spec.js
@@ -0,0 +1,29 @@
+const { writeUpdates } = require('../../../lib/workers/repository/write');
+const branchWorker = require('../../../lib/workers/branch');
+
+branchWorker.processBranch = jest.fn();
+
+let config;
+beforeEach(() => {
+  jest.resetAllMocks();
+  config = { ...require('../../_fixtures/config') };
+});
+
+describe('workers/repository/write', () => {
+  describe('writeUpdates()', () => {
+    it('runs pins first', async () => {
+      config.branches = [{ isPin: true }, {}, {}];
+      const res = await writeUpdates(config);
+      expect(res).toEqual('done');
+      expect(branchWorker.processBranch.mock.calls).toHaveLength(1);
+    });
+    it('stops after automerge', async () => {
+      config.branches = [{}, {}, {}];
+      branchWorker.processBranch.mockReturnValueOnce('created');
+      branchWorker.processBranch.mockReturnValueOnce('automerged');
+      const res = await writeUpdates(config);
+      expect(res).toEqual('automerged');
+      expect(branchWorker.processBranch.mock.calls).toHaveLength(2);
+    });
+  });
+});