diff --git a/lib/config/index.js b/lib/config/index.js
index 56ae5e26229699df6640526da28fd52cf4ceaf57..80a08d43c10c73c6f957b48171d9430155ddc949 100644
--- a/lib/config/index.js
+++ b/lib/config/index.js
@@ -9,10 +9,27 @@ const envParser = require('./env');
 
 const { getPlatformApi } = require('../platform');
 const { resolveConfigPresets } = require('./presets');
+const { get, getLanguageList, getManagerList } = require('../manager');
 
 exports.parseConfigs = parseConfigs;
 exports.mergeChildConfig = mergeChildConfig;
 exports.filterConfig = filterConfig;
+exports.getManagerConfig = getManagerConfig;
+
+function getManagerConfig(config, manager) {
+  let managerConfig = config;
+  const language = get(manager, 'language');
+  if (language) {
+    managerConfig = mergeChildConfig(managerConfig, config[language]);
+  }
+  managerConfig = mergeChildConfig(managerConfig, config[manager]);
+  for (const i of getLanguageList().concat(getManagerList())) {
+    delete managerConfig[i];
+  }
+  managerConfig.language = language;
+  managerConfig.manager = manager;
+  return managerConfig;
+}
 
 async function parseConfigs(env, argv) {
   logger.debug('Parsing configs');
diff --git a/lib/manager/index.js b/lib/manager/index.js
index 9dcbd846c32bfa149cd8aa02218b94276f76ad32..517032dcc85fa5d60e3c5563e92ca933087e6865 100644
--- a/lib/manager/index.js
+++ b/lib/manager/index.js
@@ -1,9 +1,3 @@
-const minimatch = require('minimatch');
-const pAll = require('p-all');
-const { mergeChildConfig } = require('../config');
-const { checkMonorepos } = require('../manager/npm/monorepos');
-
-const managers = {};
 const managerList = [
   'bazel',
   'buildkite',
@@ -16,191 +10,37 @@ const managerList = [
   'pip_requirements',
   'travis',
 ];
+const managers = {};
 for (const manager of managerList) {
   // eslint-disable-next-line global-require,import/no-dynamic-require
   managers[manager] = require(`./${manager}`);
 }
 
-module.exports = {
-  detectPackageFiles,
-  extractDependencies,
-  getPackageUpdates,
-  getUpdatedPackageFiles,
-  resolvePackageFiles,
-};
+const languageList = ['node', 'python'];
 
-async function detectPackageFiles(config) {
-  logger.debug('detectPackageFiles()');
-  logger.trace({ config });
-  let packageFiles = [];
-  let fileList = await platform.getFileList();
-  if (config.includePaths && config.includePaths.length) {
-    fileList = fileList.filter(file =>
-      config.includePaths.some(
-        includePath => file === includePath || minimatch(file, includePath)
-      )
-    );
-  }
-  if (config.ignorePaths && config.ignorePaths.length) {
-    fileList = fileList.filter(
-      file =>
-        !config.ignorePaths.some(
-          ignorePath => file.includes(ignorePath) || minimatch(file, ignorePath)
-        )
-    );
-  }
-  for (const manager of managerList) {
-    logger.debug(`Detecting package files (${manager})`);
-    const { language } = managers[manager];
-    // Check if the user has a whitelist of managers
-    if (
-      config.enabledManagers &&
-      config.enabledManagers.length &&
-      !(
-        config.enabledManagers.includes(manager) ||
-        config.enabledManagers.includes(language)
-      )
-    ) {
-      logger.debug(manager + ' is not on the enabledManagers list');
-      continue; // eslint-disable-line no-continue
-    }
-    // Check if the manager is manually disabled
-    if (config[manager].enabled === false) {
-      logger.debug(manager + ' is disabled');
-      continue; // eslint-disable-line no-continue
-    }
-    // Check if the parent is manually disabled
-    if (language && config[language].enabled === false) {
-      logger.debug(manager + ' language is disabled');
-      continue; // eslint-disable-line no-continue
-    }
-    const files = [];
-    let allfiles = [];
-    for (const fileMatch of config[manager].fileMatch) {
-      logger.debug(`Using ${manager} file match: ${fileMatch}`);
-      allfiles = allfiles.concat(
-        fileList.filter(file => file.match(new RegExp(fileMatch)))
-      );
-    }
-    logger.debug(`Found ${allfiles.length} files`);
-    for (const file of allfiles) {
-      const { contentPattern } = managers[manager];
-      if (contentPattern) {
-        const content = await platform.getFile(file);
-        if (content && content.match(contentPattern)) {
-          files.push(file);
-        }
-      } else {
-        files.push(file);
-      }
-    }
-    if (files.length) {
-      logger.info({ manager, files }, `Detected package files`);
-      packageFiles = packageFiles.concat(
-        files.map(packageFile => ({ packageFile, manager }))
-      );
-    }
-  }
-  logger.trace({ packageFiles }, 'All detected package files');
-  return packageFiles;
-}
-
-function extractDependencies(packageContent, config) {
-  logger.debug('manager.extractDependencies()');
-  return managers[config.manager].extractDependencies(packageContent, config);
-}
+const get = (manager, name) => managers[manager][name];
+const getLanguageList = () => languageList;
+const getManagerList = () => managerList;
 
-function getPackageUpdates(config) {
-  logger.trace({ config }, 'manager.getPackageUpdates()');
-  const { manager } = config;
-  if (!managerList.includes(manager)) {
-    throw new Error('Unsupported package manager');
-  }
-  return managers[manager].getPackageUpdates(config);
-}
+module.exports = {
+  get,
+  getLanguageList,
+  getManagerList,
+};
 
-async function getUpdatedPackageFiles(config) {
-  logger.debug('manager.getUpdatedPackageFiles()');
-  logger.trace({ config });
-  const updatedPackageFiles = {};
+const managerFunctions = [
+  'extractDependencies',
+  'postExtract',
+  'getPackageUpdates',
+  'updateDependency',
+  'supportsLockFileMaintenance',
+];
 
-  for (const upgrade of config.upgrades) {
-    const { manager } = upgrade;
-    if (upgrade.type !== 'lockFileMaintenance') {
-      const existingContent =
-        updatedPackageFiles[upgrade.packageFile] ||
-        (await platform.getFile(upgrade.packageFile, config.parentBranch));
-      let newContent = existingContent;
-      newContent = await managers[manager].updateDependency(
-        existingContent,
-        upgrade
-      );
-      if (!newContent) {
-        if (config.parentBranch) {
-          logger.info('Rebasing branch after error updating content');
-          return getUpdatedPackageFiles({
-            ...config,
-            parentBranch: undefined,
-          });
-        }
-        throw new Error('Error updating branch content and cannot rebase');
-      }
-      if (newContent !== existingContent) {
-        if (config.parentBranch) {
-          // This ensure it's always 1 commit from Renovate
-          logger.info('Need to update package file so will rebase first');
-          return getUpdatedPackageFiles({
-            ...config,
-            parentBranch: undefined,
-          });
-        }
-        logger.debug('Updating packageFile content');
-        updatedPackageFiles[upgrade.packageFile] = newContent;
-      }
+for (const f of managerFunctions) {
+  module.exports[f] = (manager, ...params) => {
+    if (managers[manager][f]) {
+      return managers[manager][f](...params);
     }
-  }
-  return {
-    parentBranch: config.parentBranch, // Need to overwrite original config
-    updatedPackageFiles: Object.keys(updatedPackageFiles).map(packageFile => ({
-      name: packageFile,
-      contents: updatedPackageFiles[packageFile],
-    })),
+    return null;
   };
 }
-
-async function resolvePackageFiles(config) {
-  logger.debug('manager.resolvePackageFile()');
-  logger.trace({ config });
-  const allPackageFiles = await detectPackageFiles(config);
-  logger.debug({ allPackageFiles }, 'allPackageFiles');
-  async function resolvePackageFile(p) {
-    let packageFile = p;
-    const { manager } = packageFile;
-    if (managers[manager].resolvePackageFile) {
-      return managers[manager].resolvePackageFile(config, packageFile);
-    }
-    const { language } = managers[manager];
-    const languageConfig = language ? config[language] : {};
-    const managerConfig = mergeChildConfig(languageConfig, config[manager]);
-    packageFile = mergeChildConfig(managerConfig, packageFile);
-    logger.debug(
-      `Resolving packageFile ${JSON.stringify(packageFile.packageFile)}`
-    );
-    packageFile.content = await platform.getFile(packageFile.packageFile);
-    return packageFile;
-  }
-  let queue = allPackageFiles.map(p => () => resolvePackageFile(p));
-  // limit to 100 maximum package files if no global value set
-  const maxPackageFiles = config.global.maxPackageFiles || 100;
-  // istanbul ignore if
-  if (queue.length > maxPackageFiles) {
-    logger.warn(`packageFile queue length is ${queue.length}`);
-    queue = queue.slice(0, maxPackageFiles);
-  }
-  // retrieve with concurrency of 5
-  const packageFiles = (await pAll(queue, { concurrency: 5 })).filter(
-    p => p !== null
-  );
-  logger.debug('Checking against path rules');
-  return checkMonorepos({ ...config, packageFiles });
-}
diff --git a/lib/manager/npm/extract.js b/lib/manager/npm/extract.js
deleted file mode 100644
index fb93f555cbcca90e4b663220109d3b7f632d95a1..0000000000000000000000000000000000000000
--- a/lib/manager/npm/extract.js
+++ /dev/null
@@ -1,62 +0,0 @@
-module.exports = {
-  extractDependencies,
-};
-
-function extractDependencies(packageJson, config) {
-  const {
-    depType,
-    packageLockParsed,
-    npmShrinkwrapParsed,
-    yarnLockParsed,
-  } = config;
-  const depNames = packageJson[depType]
-    ? Object.keys(packageJson[depType])
-    : [];
-  const deps = depNames
-    .map(depName => {
-      const currentVersion = packageJson[depType][depName]
-        ? `${packageJson[depType][depName]}`.trim().replace(/^=/, '')
-        : undefined;
-      let lockedVersion;
-      try {
-        const lockFile = packageLockParsed || npmShrinkwrapParsed;
-        if (lockFile) {
-          if (lockFile.dependencies[depName]) {
-            lockedVersion = lockFile.dependencies[depName].version;
-            if (lockedVersion !== currentVersion) {
-              logger.debug(
-                { currentVersion, lockedVersion },
-                'Found locked version'
-              );
-            }
-          } else {
-            logger.debug({ currentVersion }, 'Found no locked version');
-          }
-        } else if (yarnLockParsed && yarnLockParsed.object) {
-          const key = `${depName}@${currentVersion}`;
-          const lockEntry = yarnLockParsed.object[key];
-          if (lockEntry) {
-            lockedVersion = lockEntry.version;
-            if (lockedVersion !== currentVersion) {
-              logger.debug(
-                { currentVersion, lockedVersion },
-                'Found locked version'
-              );
-            }
-          } else {
-            logger.debug({ currentVersion }, 'Found no locked version');
-          }
-        }
-      } catch (err) {
-        logger.debug({ currentVersion }, 'Could not find locked version');
-      }
-      return {
-        depType,
-        depName,
-        currentVersion,
-        lockedVersion,
-      };
-    })
-    .filter(dep => dep.currentVersion);
-  return { deps };
-}
diff --git a/lib/manager/npm/extract/index.js b/lib/manager/npm/extract/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..1a6441812362f21d6dde42899f38d1d8ef5d4698
--- /dev/null
+++ b/lib/manager/npm/extract/index.js
@@ -0,0 +1,108 @@
+const path = require('path');
+const upath = require('upath');
+const { getLockedVersions } = require('./locked-versions');
+const { detectMonorepos } = require('./monorepo');
+
+module.exports = {
+  extractDependencies,
+  postExtract,
+};
+
+async function extractDependencies(content, packageFile) {
+  logger.debug({ content, packageFile });
+  const deps = [];
+  let packageJson;
+  try {
+    packageJson = JSON.parse(content);
+  } catch (err) {
+    logger.info({ packageFile }, 'Invalid JSON');
+    return null;
+  }
+  const packageJsonName = packageJson.name;
+  const packageJsonVersion = packageJson.version;
+  const yarnWorkspacesPackages = packageJson.workspaces;
+
+  const lockFiles = {
+    yarnLock: 'yarn.lock',
+    packageLock: 'package-lock.json',
+    shrinkwrapJson: 'npm-shrinkwrap.json',
+    pnpmShrinkwrap: 'shrinkwrap.yaml',
+  };
+
+  for (const [key, val] of Object.entries(lockFiles)) {
+    const filePath = upath.join(path.dirname(packageFile), val);
+    if (await platform.getFile(filePath)) {
+      lockFiles[key] = filePath;
+    } else {
+      lockFiles[key] = undefined;
+    }
+  }
+  lockFiles.npmLock = lockFiles.packageLock || lockFiles.shrinkwrapJson;
+  delete lockFiles.packageLock;
+  delete lockFiles.shrinkwrapJson;
+
+  let npmrc = await platform.getFile(
+    upath.join(path.dirname(packageFile), '.npmrc')
+  );
+  if (!npmrc) {
+    npmrc = undefined;
+  }
+
+  let lernaDir;
+  let lernaPackages;
+  let lernaClient;
+  const lernaJson = JSON.parse(
+    await platform.getFile(upath.join(path.dirname(packageFile), 'lerna.json'))
+  );
+  if (lernaJson) {
+    lernaDir = path.dirname(packageFile);
+    lernaPackages = lernaJson.packages;
+    lernaClient = lernaJson.npmClient;
+  }
+
+  const depTypes = [
+    'dependencies',
+    'devDependencies',
+    'optionalDependencies',
+    'peerDependencies',
+    'engines',
+  ];
+  for (const depType of depTypes) {
+    if (packageJson[depType]) {
+      try {
+        for (const [depName, version] of Object.entries(packageJson[depType])) {
+          deps.push({
+            depName,
+            depType,
+            currentVersion: version.trim().replace(/^=/, ''),
+          });
+        }
+      } catch (err) /* istanbul ignore next */ {
+        logger.info(
+          { packageFile, depType, err, message: err.message },
+          'Error parsing package.json'
+        );
+        return null;
+      }
+    }
+  }
+  if (!(deps.length || lernaDir || yarnWorkspacesPackages)) {
+    return null;
+  }
+  return {
+    deps,
+    packageJsonName,
+    packageJsonVersion,
+    npmrc,
+    ...lockFiles,
+    lernaDir,
+    lernaClient,
+    lernaPackages,
+    yarnWorkspacesPackages,
+  };
+}
+
+async function postExtract(packageFiles) {
+  await detectMonorepos(packageFiles);
+  await getLockedVersions(packageFiles);
+}
diff --git a/lib/manager/npm/extract/locked-versions.js b/lib/manager/npm/extract/locked-versions.js
new file mode 100644
index 0000000000000000000000000000000000000000..6665492d2f17fdbf8fc02ad8acdcfad138b01062
--- /dev/null
+++ b/lib/manager/npm/extract/locked-versions.js
@@ -0,0 +1,36 @@
+const { getNpmLock } = require('./npm');
+const { getYarnLock } = require('./yarn');
+
+module.exports = {
+  getLockedVersions,
+};
+
+async function getLockedVersions(packageFiles) {
+  const lockFileCache = {};
+  logger.debug('Finding locked versions');
+  for (const packageFile of packageFiles) {
+    const { yarnLock, npmLock, pnpmShrinkwrap } = packageFile;
+    if (yarnLock) {
+      logger.debug('Found yarnLock');
+      if (!lockFileCache[yarnLock]) {
+        logger.debug('Retrieving/parsing ' + yarnLock);
+        lockFileCache[yarnLock] = await getYarnLock(yarnLock);
+      }
+      for (const dep of packageFile.deps) {
+        dep.lockedVersion =
+          lockFileCache[yarnLock][`${dep.depName}@${dep.currentVersion}`];
+      }
+    } else if (npmLock) {
+      logger.debug({ npmLock }, 'npm lockfile');
+      if (!lockFileCache[npmLock]) {
+        logger.debug('Retrieving/parsing ' + npmLock);
+        lockFileCache[npmLock] = await getNpmLock(npmLock);
+      }
+      for (const dep of packageFile.deps) {
+        dep.lockedVersion = lockFileCache[npmLock][dep.depName];
+      }
+    } else if (pnpmShrinkwrap) {
+      logger.info('TODO: implement shrinkwrap.yaml parsing of lockVersion');
+    }
+  }
+}
diff --git a/lib/manager/npm/extract/monorepo.js b/lib/manager/npm/extract/monorepo.js
new file mode 100644
index 0000000000000000000000000000000000000000..da1a4a81bb49f9d72f396bdb471588d09724a8ff
--- /dev/null
+++ b/lib/manager/npm/extract/monorepo.js
@@ -0,0 +1,56 @@
+const minimatch = require('minimatch');
+const path = require('path');
+const upath = require('upath');
+
+module.exports = {
+  detectMonorepos,
+};
+
+function matchesAnyPattern(val, patterns) {
+  return patterns.some(pattern => minimatch(val, pattern));
+}
+
+function detectMonorepos(packageFiles) {
+  logger.debug('Detecting Lerna and Yarn Workspaces');
+  for (const p of packageFiles) {
+    const {
+      packageFile,
+      npmLock,
+      yarnLock,
+      lernaDir,
+      lernaClient,
+      lernaPackages,
+      yarnWorkspacesPackages,
+    } = p;
+    const basePath = path.dirname(packageFile);
+    const packages =
+      lernaClient === 'yarn' && yarnWorkspacesPackages
+        ? yarnWorkspacesPackages
+        : lernaPackages;
+    if (packages && packages.length) {
+      logger.debug(
+        { packageFile },
+        'Found monorepo packages with base path ' + basePath
+      );
+      const subPackagePatterns = packages.map(pattern =>
+        upath.join(basePath, pattern)
+      );
+      const subPackages = packageFiles.filter(sp =>
+        matchesAnyPattern(path.dirname(sp.packageFile), subPackagePatterns)
+      );
+      const subPackageNames = subPackages
+        .map(sp => sp.packageJsonName)
+        .filter(Boolean);
+      // add all names to main package.json
+      packageFile.monorepoPackages = subPackageNames;
+      for (const subPackage of subPackages) {
+        subPackage.monorepoPackages = subPackageNames.filter(
+          name => name !== subPackage.packageJsonName
+        );
+        subPackage.lernaDir = lernaDir;
+        subPackage.yarnLock = subPackage.yarnLock || yarnLock;
+        subPackage.npmLock = subPackage.npmLock || npmLock;
+      }
+    }
+  }
+}
diff --git a/lib/manager/npm/extract/npm.js b/lib/manager/npm/extract/npm.js
new file mode 100644
index 0000000000000000000000000000000000000000..50f1d5e5d529c8b7e7f6dfbd549eea6df0616bf8
--- /dev/null
+++ b/lib/manager/npm/extract/npm.js
@@ -0,0 +1,22 @@
+module.exports = {
+  getNpmLock,
+};
+
+async function getNpmLock(filePath) {
+  const lockRaw = await platform.getFile(filePath);
+  try {
+    const lockParsed = JSON.parse(lockRaw);
+    const lockFile = {};
+    for (const [entry, val] of Object.entries(lockParsed.dependencies)) {
+      logger.trace({ entry, version: val.version });
+      lockFile[entry] = val.version;
+    }
+    return lockFile;
+  } catch (err) {
+    logger.info(
+      { filePath, err, message: err.message },
+      'Warning: Exception parsing npm lock file'
+    );
+    return {};
+  }
+}
diff --git a/lib/manager/npm/extract/yarn.js b/lib/manager/npm/extract/yarn.js
new file mode 100644
index 0000000000000000000000000000000000000000..8bf56b016242406798fab1489d3886a0d4fc3cff
--- /dev/null
+++ b/lib/manager/npm/extract/yarn.js
@@ -0,0 +1,32 @@
+const yarnLockParser = require('@yarnpkg/lockfile');
+
+module.exports = {
+  getYarnLock,
+};
+
+async function getYarnLock(filePath) {
+  const yarnLockRaw = await platform.getFile(filePath);
+  try {
+    const yarnLockParsed = yarnLockParser.parse(yarnLockRaw);
+    // istanbul ignore if
+    if (yarnLockParsed.type !== 'success') {
+      logger.info(
+        { filePath, parseType: yarnLockParsed.type },
+        'Error parsing yarn.lock - not success'
+      );
+      return {};
+    }
+    const lockFile = {};
+    for (const [entry, val] of Object.entries(yarnLockParsed.object)) {
+      logger.trace({ entry, version: val.version });
+      lockFile[entry] = val.version;
+    }
+    return lockFile;
+  } catch (err) {
+    logger.info(
+      { filePath, err, message: err.message },
+      'Warning: Exception parsing yarn.lock'
+    );
+    return {};
+  }
+}
diff --git a/lib/manager/npm/index.js b/lib/manager/npm/index.js
index fd6cd4040687e75cf3afdab1228293ac8aeefa4f..930494df38652a742980e7a7b477080cdf61e763 100644
--- a/lib/manager/npm/index.js
+++ b/lib/manager/npm/index.js
@@ -1,11 +1,11 @@
-const { extractDependencies } = require('./extract');
+const { extractDependencies, postExtract } = require('./extract');
 const { getPackageUpdates } = require('./package');
-const { resolvePackageFile } = require('./resolve');
 const { updateDependency } = require('./update');
 
 module.exports = {
   extractDependencies,
+  postExtract,
   getPackageUpdates,
-  resolvePackageFile,
   updateDependency,
+  supportsLockFileMaintenance: true,
 };
diff --git a/lib/manager/npm/monorepos.js b/lib/manager/npm/monorepos.js
deleted file mode 100644
index 29135456879540f0c487cfe76e337d3642aab214..0000000000000000000000000000000000000000
--- a/lib/manager/npm/monorepos.js
+++ /dev/null
@@ -1,90 +0,0 @@
-const minimatch = require('minimatch');
-const path = require('path');
-const upath = require('upath');
-
-module.exports = {
-  checkMonorepos,
-};
-
-async function checkMonorepos(config) {
-  const monorepoPackages = [];
-  logger.debug('checkMonorepos()');
-  logger.trace({ config });
-  // yarn workspaces
-  let foundWorkspaces = false;
-  for (const packageFile of config.packageFiles) {
-    if (
-      packageFile.packageFile &&
-      packageFile.packageFile.endsWith('package.json') &&
-      packageFile.content.workspaces
-    ) {
-      foundWorkspaces = true;
-      packageFile.workspaces = true;
-      const workspaceDir = path.dirname(packageFile.packageFile);
-      const { workspaces } = packageFile.content;
-      if (workspaces.length) {
-        logger.info(
-          { packageFile: packageFile.packageFile, workspaces },
-          'Found yarn workspaces'
-        );
-        for (const workspace of workspaces) {
-          const basePath = upath.join(workspaceDir, workspace);
-          logger.debug(`basePath=${basePath}`);
-          for (const innerPackageFile of config.packageFiles) {
-            if (
-              minimatch(path.dirname(innerPackageFile.packageFile), basePath)
-            ) {
-              logger.debug(`Matched ${innerPackageFile.packageFile}`);
-              const depName = innerPackageFile.content.name;
-              monorepoPackages.push(depName);
-              innerPackageFile.workspaceDir = workspaceDir;
-            }
-          }
-        }
-      }
-    }
-  }
-  if (foundWorkspaces) {
-    logger.debug('Ignoring any lerna and returning workspaces');
-    return { ...config, workspaces: true, monorepoPackages };
-  }
-  // lerna
-  let lernaJson;
-  try {
-    logger.debug('Checking for lerna.json');
-    lernaJson = JSON.parse(await platform.getFile('lerna.json'));
-  } catch (err) {
-    logger.info('Error parsing lerna.json');
-  }
-  if (!lernaJson) {
-    logger.debug('No lerna.json found');
-    return { ...config, monorepoPackages };
-  }
-  let lernaLockFile;
-  // istanbul ignore else
-  if (await platform.getFile('package-lock.json')) {
-    logger.debug('lerna has a package-lock.json');
-    lernaLockFile = 'npm';
-  } else if (
-    lernaJson.npmClient === 'yarn' &&
-    (await platform.getFile('yarn.lock'))
-  ) {
-    logger.debug('lerna has non-workspaces yarn');
-    lernaLockFile = 'yarn';
-  }
-  if (lernaJson && lernaJson.packages) {
-    logger.debug({ lernaJson }, 'Found lerna config');
-    for (const packageGlob of lernaJson.packages) {
-      for (const packageFile of config.packageFiles) {
-        if (minimatch(path.dirname(packageFile.packageFile), packageGlob)) {
-          const depName = packageFile.content.name;
-          if (!monorepoPackages.includes(depName)) {
-            monorepoPackages.push(depName);
-          }
-          packageFile.lerna = true;
-        }
-      }
-    }
-  }
-  return { ...config, lernaLockFile, monorepoPackages };
-}
diff --git a/lib/workers/branch/lock-files.js b/lib/manager/npm/post-update/index.js
similarity index 77%
rename from lib/workers/branch/lock-files.js
rename to lib/manager/npm/post-update/index.js
index bc8d2464e5afc9412ee9a6d1dca494b6f44de8be..2b02d520c3584be1fc9b5b5022782ae9f455c0a5 100644
--- a/lib/workers/branch/lock-files.js
+++ b/lib/manager/npm/post-update/index.js
@@ -7,173 +7,83 @@ const yarn = require('./yarn');
 const pnpm = require('./pnpm');
 
 module.exports = {
-  hasPackageLock,
-  hasNpmShrinkwrap,
-  hasYarnLock,
-  hasShrinkwrapYaml,
   determineLockFileDirs,
   writeExistingFiles,
   writeUpdatedPackageFiles,
-  getUpdatedLockFiles,
+  getAdditionalFiles,
 };
 
-function hasPackageLock(config, packageFile) {
-  logger.trace(
-    { packageFiles: config.packageFiles, packageFile },
-    'hasPackageLock'
-  );
-  for (const p of config.packageFiles) {
-    if (p.packageFile === packageFile) {
-      if (p.packageLock) {
-        return true;
-      }
-      return false;
-    }
-  }
-  throw new Error(`hasPackageLock cannot find ${packageFile}`);
-}
+// Strips empty values, deduplicates, and returns the directories from filenames
+// istanbul ignore next
+const getDirs = arr =>
+  Array.from(new Set(arr.filter(Boolean).map(path.dirname)));
 
-function hasNpmShrinkwrap(config, packageFile) {
-  logger.trace(
-    { packageFiles: config.packageFiles, packageFile },
-    'hasNpmShrinkwrap'
-  );
-  for (const p of config.packageFiles) {
-    if (p.packageFile === packageFile) {
-      if (p.npmShrinkwrap) {
-        return true;
-      }
-      return false;
-    }
-  }
-  throw new Error(`hasPackageLock cannot find ${packageFile}`);
-}
-
-function hasYarnLock(config, packageFile) {
-  logger.trace(
-    { packageFiles: config.packageFiles, packageFile },
-    'hasYarnLock'
-  );
-  for (const p of config.packageFiles) {
-    if (p.packageFile === packageFile) {
-      if (p.yarnLock) {
-        return true;
-      }
-      return false;
-    }
-  }
-  throw new Error(`hasYarnLock cannot find ${packageFile}`);
-}
-
-function hasShrinkwrapYaml(config, packageFile) {
-  logger.trace(
-    { packageFiles: config.packageFiles, packageFile },
-    'hasShrinkwrapYaml'
-  );
-  for (const p of config.packageFiles) {
-    if (p.packageFile === packageFile) {
-      if (p.shrinkwrapYaml) {
-        return true;
-      }
-      return false;
-    }
-  }
-  throw new Error(`hasShrinkwrapYaml cannot find ${packageFile}`);
-}
-
-function determineLockFileDirs(config) {
-  const packageLockFileDirs = [];
-  const npmShrinkwrapDirs = [];
-  const yarnLockFileDirs = [];
-  const shrinkwrapYamlDirs = [];
+// istanbul ignore next
+function determineLockFileDirs(config, packageFiles) {
+  const npmLockDirs = [];
+  const yarnLockDirs = [];
+  const pnpmShrinkwrapDirs = [];
   const lernaDirs = [];
 
   for (const upgrade of config.upgrades) {
     if (upgrade.type === 'lockFileMaintenance') {
-      // Return every direcotry that contains a lockfile
-      for (const packageFile of config.packageFiles) {
-        const dirname = path.dirname(packageFile.packageFile);
-        if (packageFile.yarnLock) {
-          yarnLockFileDirs.push(dirname);
-        }
-        if (packageFile.packageLock) {
-          packageLockFileDirs.push(dirname);
-        }
-        if (packageFile.npmShrinkwrap) {
-          npmShrinkwrapDirs.push(dirname);
-        }
-        if (packageFile.shrinkwrapYaml) {
-          shrinkwrapYamlDirs.push(dirname);
+      // TODO: support lerna
+      // Return every directory that contains a lockfile
+      for (const packageFile of packageFiles.npm) {
+        if (packageFile.lernaDir) {
+          lernaDirs.push(packageFile.lernaDir);
+        } else {
+          yarnLockDirs.push(packageFile.yarnLock);
+          npmLockDirs.push(packageFile.npmLock);
+          pnpmShrinkwrapDirs.push(packageFile.pnpmShrinkwrap);
         }
       }
       return {
-        packageLockFileDirs,
-        npmShrinkwrapDirs,
-        yarnLockFileDirs,
-        shrinkwrapYamlDirs,
+        yarnLockDirs: getDirs(yarnLockDirs),
+        npmLockDirs: getDirs(npmLockDirs),
+        pnpmShrinkwrapDirs: getDirs(pnpmShrinkwrapDirs),
+        lernaDirs: getDirs(lernaDirs),
       };
     }
   }
 
-  for (const packageFile of config.updatedPackageFiles) {
-    if (
-      module.exports.hasYarnLock(config, packageFile.name) &&
-      !config.lernaLockFile
-    ) {
-      yarnLockFileDirs.push(path.dirname(packageFile.name));
-    }
-    if (
-      module.exports.hasPackageLock(config, packageFile.name) &&
-      !config.lernaLockFile
-    ) {
-      packageLockFileDirs.push(path.dirname(packageFile.name));
-    }
-    if (
-      module.exports.hasNpmShrinkwrap(config, packageFile.name) &&
-      !config.lernaLockFile
-    ) {
-      npmShrinkwrapDirs.push(path.dirname(packageFile.name));
-    }
-    if (module.exports.hasShrinkwrapYaml(config, packageFile.name)) {
-      shrinkwrapYamlDirs.push(path.dirname(packageFile.name));
+  function getPackageFile(fileName) {
+    logger.trace('Looking for packageFile: ' + fileName);
+    for (const packageFile of packageFiles.npm) {
+      if (packageFile.packageFile === fileName) {
+        logger.trace({ packageFile }, 'Found packageFile');
+        return packageFile;
+      }
+      logger.trace('No match');
     }
+    return {};
   }
 
-  if (
-    config.updatedPackageFiles &&
-    config.updatedPackageFiles.length &&
-    config.lernaLockFile
-  ) {
-    lernaDirs.push('.');
-  }
-
-  // If yarn workspaces are in use, then we need to generate yarn.lock from the workspaces dir
-  if (
-    config.updatedPackageFiles &&
-    config.updatedPackageFiles.length &&
-    config.workspaceDir
-  ) {
-    const updatedPackageFileNames = config.updatedPackageFiles.map(p => p.name);
-    for (const packageFile of config.packageFiles) {
-      if (
-        updatedPackageFileNames.includes(packageFile.packageFile) &&
-        packageFile.workspaceDir &&
-        !yarnLockFileDirs.includes(packageFile.workspaceDir)
-      )
-        yarnLockFileDirs.push(packageFile.workspaceDir);
+  for (const p of config.updatedPackageFiles) {
+    logger.debug(`Checking ${p.name} for lock files`);
+    const packageFile = getPackageFile(p.name);
+    // lerna first
+    if (packageFile.lernaDir) {
+      logger.debug(`${packageFile.packageFile} has lerna lock file`);
+      lernaDirs.push(packageFile.lernaDir);
+    } else {
+      // push full lock file names and convert them later
+      yarnLockDirs.push(packageFile.yarnLock);
+      npmLockDirs.push(packageFile.npmLock);
+      pnpmShrinkwrapDirs.push(packageFile.pnpmShrinkwrap);
     }
   }
 
   return {
-    yarnLockFileDirs,
-    packageLockFileDirs,
-    npmShrinkwrapDirs,
-    shrinkwrapYamlDirs,
-    lernaDirs,
+    yarnLockDirs: getDirs(yarnLockDirs),
+    npmLockDirs: getDirs(npmLockDirs),
+    pnpmShrinkwrapDirs: getDirs(pnpmShrinkwrapDirs),
+    lernaDirs: getDirs(lernaDirs),
   };
 }
 
-async function writeExistingFiles(config) {
+// istanbul ignore next
+async function writeExistingFiles(config, packageFiles) {
   const lernaJson = await platform.getFile('lerna.json');
   if (lernaJson) {
     logger.debug(`Writing repo lerna.json (${config.tmpDir.path})`);
@@ -193,12 +103,10 @@ async function writeExistingFiles(config) {
       config.yarnrc
     );
   }
-  if (!config.packageFiles) {
+  if (!packageFiles.npm) {
     return;
   }
-  const npmFiles = config.packageFiles.filter(p =>
-    p.packageFile.endsWith('package.json')
-  );
+  const npmFiles = packageFiles.npm;
   logger.debug(
     { packageFiles: npmFiles.map(n => n.packageFile) },
     'Writing package.json files'
@@ -210,7 +118,9 @@ async function writeExistingFiles(config) {
     );
     logger.trace(`Writing package.json to ${basedir}`);
     // Massage the file to eliminate yarn errors
-    const massagedFile = { ...packageFile.content };
+    const massagedFile = JSON.parse(
+      await platform.getFile(packageFile.packageFile)
+    );
     if (massagedFile.name) {
       massagedFile.name = massagedFile.name.replace(/[{}]/g, '');
     }
@@ -309,7 +219,10 @@ async function writeExistingFiles(config) {
     if (packageFile.yarnLock && config.type !== 'lockFileMaintenance') {
       logger.debug(`Writing yarn.lock to ${basedir}`);
       const yarnLock = await platform.getFile(packageFile.yarnLock);
-      await fs.outputFile(upath.join(basedir, 'yarn.lock'), yarnLock);
+      await fs.outputFile(
+        upath.join(config.tmpDir.path, packageFile.yarnLock),
+        yarnLock
+      );
     } else {
       logger.trace(`Removing ${basedir}/yarn.lock`);
       await fs.remove(upath.join(basedir, 'yarn.lock'));
@@ -318,12 +231,12 @@ async function writeExistingFiles(config) {
     const pnpmBug992 = true;
     // istanbul ignore next
     if (
-      packageFile.shrinkwrapYaml &&
+      packageFile.pnpmShrinkwrap &&
       config.type !== 'lockFileMaintenance' &&
       !pnpmBug992
     ) {
       logger.debug(`Writing shrinkwrap.yaml to ${basedir}`);
-      const shrinkwrap = await platform.getFile(packageFile.shrinkwrapYaml);
+      const shrinkwrap = await platform.getFile(packageFile.pnpmShrinkwrap);
       await fs.outputFile(upath.join(basedir, 'shrinkwrap.yaml'), shrinkwrap);
     } else {
       await fs.remove(upath.join(basedir, 'shrinkwrap.yaml'));
@@ -331,6 +244,7 @@ async function writeExistingFiles(config) {
   }
 }
 
+// istanbul ignore next
 function listLocalLibs(dependencies) {
   logger.trace(`listLocalLibs (${dependencies})`);
   const toCopy = [];
@@ -350,6 +264,7 @@ function listLocalLibs(dependencies) {
   return toCopy;
 }
 
+// istanbul ignore next
 async function writeUpdatedPackageFiles(config) {
   logger.trace({ config }, 'writeUpdatedPackageFiles');
   logger.debug('Writing any updated package files');
@@ -375,8 +290,9 @@ async function writeUpdatedPackageFiles(config) {
   }
 }
 
-async function getUpdatedLockFiles(config) {
-  logger.trace({ config }, 'getUpdatedLockFiles');
+// istanbul ignore next
+async function getAdditionalFiles(config, packageFiles) {
+  logger.trace({ config }, 'getAdditionalFiles');
   logger.debug('Getting updated lock files');
   const lockFileErrors = [];
   const updatedLockFiles = [];
@@ -392,10 +308,10 @@ async function getUpdatedLockFiles(config) {
     logger.debug('Skipping lockFileMaintenance update');
     return { lockFileErrors, updatedLockFiles };
   }
-  const dirs = module.exports.determineLockFileDirs(config);
+  const dirs = module.exports.determineLockFileDirs(config, packageFiles);
   logger.debug({ dirs }, 'lock file dirs');
-  await module.exports.writeExistingFiles(config);
-  await module.exports.writeUpdatedPackageFiles(config);
+  await module.exports.writeExistingFiles(config, packageFiles);
+  await module.exports.writeUpdatedPackageFiles(config, packageFiles);
 
   const env =
     config.global && config.global.exposeEnv
@@ -403,7 +319,7 @@ async function getUpdatedLockFiles(config) {
       : { HOME: process.env.HOME, PATH: process.env.PATH };
   env.NODE_ENV = 'dev';
 
-  for (const lockFileDir of dirs.packageLockFileDirs) {
+  for (const lockFileDir of dirs.npmLockDirs) {
     logger.debug(`Generating package-lock.json for ${lockFileDir}`);
     const lockFileName = upath.join(lockFileDir, 'package-lock.json');
     const res = await npm.generateLockFile(
@@ -455,7 +371,7 @@ async function getUpdatedLockFiles(config) {
   }
 
   // istanbul ignore next
-  for (const lockFileDir of dirs.npmShrinkwrapDirs) {
+  for (const lockFileDir of dirs.pnpmShrinkwrapDirs) {
     logger.debug(`Generating npm-shrinkwrap.json for ${lockFileDir}`);
     const lockFileName = upath.join(lockFileDir, 'npm-shrinkwrap.json');
     const res = await npm.generateLockFile(
@@ -506,7 +422,7 @@ async function getUpdatedLockFiles(config) {
     }
   }
 
-  for (const lockFileDir of dirs.yarnLockFileDirs) {
+  for (const lockFileDir of dirs.yarnLockDirs) {
     logger.debug(`Generating yarn.lock for ${lockFileDir}`);
     const lockFileName = upath.join(lockFileDir, 'yarn.lock');
     const res = await yarn.generateLockFile(
@@ -558,7 +474,7 @@ async function getUpdatedLockFiles(config) {
     }
   }
 
-  for (const lockFileDir of dirs.shrinkwrapYamlDirs) {
+  for (const lockFileDir of dirs.pnpmShrinkwrapDirs) {
     logger.debug(`Generating shrinkwrap.yaml for ${lockFileDir}`);
     const lockFileName = upath.join(lockFileDir, 'shrinkwrap.yaml');
     const res = await pnpm.generateLockFile(
@@ -639,7 +555,7 @@ async function getUpdatedLockFiles(config) {
         stderr: res.stderr,
       });
     } else {
-      for (const packageFile of config.packageFiles) {
+      for (const packageFile of packageFiles.npm) {
         const baseDir = path.dirname(packageFile.packageFile);
         const filename = upath.join(baseDir, lockFile);
         logger.debug('Checking for ' + filename);
diff --git a/lib/workers/branch/lerna.js b/lib/manager/npm/post-update/lerna.js
similarity index 100%
rename from lib/workers/branch/lerna.js
rename to lib/manager/npm/post-update/lerna.js
diff --git a/lib/workers/branch/npm.js b/lib/manager/npm/post-update/npm.js
similarity index 100%
rename from lib/workers/branch/npm.js
rename to lib/manager/npm/post-update/npm.js
diff --git a/lib/workers/branch/pnpm.js b/lib/manager/npm/post-update/pnpm.js
similarity index 100%
rename from lib/workers/branch/pnpm.js
rename to lib/manager/npm/post-update/pnpm.js
diff --git a/lib/workers/branch/yarn.js b/lib/manager/npm/post-update/yarn.js
similarity index 100%
rename from lib/workers/branch/yarn.js
rename to lib/manager/npm/post-update/yarn.js
diff --git a/lib/manager/npm/resolve.js b/lib/manager/npm/resolve.js
deleted file mode 100644
index fe2dd9512948aa41360184bc63cda99d01f21e9d..0000000000000000000000000000000000000000
--- a/lib/manager/npm/resolve.js
+++ /dev/null
@@ -1,137 +0,0 @@
-const path = require('path');
-const upath = require('upath');
-const configParser = require('../../config');
-
-module.exports = {
-  resolvePackageFile,
-};
-
-async function resolvePackageFile(config, inputFile) {
-  const packageFile = configParser.mergeChildConfig(config.npm, inputFile);
-  logger.debug(
-    `Resolving packageFile ${JSON.stringify(packageFile.packageFile)}`
-  );
-
-  const pFileRaw = await platform.getFile(packageFile.packageFile);
-  // istanbul ignore if
-  if (!pFileRaw) {
-    logger.info(
-      { packageFile: packageFile.packageFile },
-      'Cannot find package.json'
-    );
-    config.errors.push({
-      depName: packageFile.packageFile,
-      message: 'Cannot find package.json',
-    });
-    return null;
-  }
-  try {
-    packageFile.content = JSON.parse(pFileRaw);
-  } catch (err) {
-    logger.info(
-      { packageFile: packageFile.packageFile },
-      'Cannot parse package.json'
-    );
-    if (config.repoIsOnboarded) {
-      const error = new Error('config-validation');
-      error.configFile = packageFile.packageFile;
-      error.validationError = 'Cannot parse package.json';
-      error.validationMessage =
-        'This package.json contains invalid JSON and cannot be parsed. Please fix it, or add it to your "ignorePaths" array in your renovate config so that Renovate can continue.';
-      throw error;
-    }
-    config.errors.push({
-      depName: packageFile.packageFile,
-      message:
-        "Cannot parse package.json (invalid JSON). Please fix the contents or add the file/path to the `ignorePaths` array in Renovate's config",
-    });
-    return null;
-  }
-
-  if (
-    inputFile.packageFile.includes('package.json') &&
-    inputFile.packageFile !== 'package.json' &&
-    packageFile.content.renovate !== undefined
-  ) {
-    const error = new Error('config-validation');
-    error.configFile = packageFile.packageFile;
-    error.validationError = 'package.json configuration error';
-    error.validationMessage =
-      'Nested package.json must not contain renovate configuration';
-    throw error;
-  }
-
-  if (!config.ignoreNpmrcFile) {
-    packageFile.npmrc = await platform.getFile(
-      upath.join(path.dirname(packageFile.packageFile), '.npmrc')
-    );
-  }
-  if (packageFile.npmrc) {
-    logger.info({ packageFile: packageFile.packageFile }, 'Found .npmrc');
-    if (packageFile.npmrc.match(/\${NPM_TOKEN}/) && !config.global.exposeEnv) {
-      logger.info('Stripping NPM_TOKEN from .npmrc');
-      packageFile.npmrc = packageFile.npmrc
-        .replace(/(^|\n).*?\${NPM_TOKEN}.*?(\n|$)/g, '')
-        .trim();
-      if (packageFile.npmrc === '') {
-        logger.info('Removing empty .npmrc');
-        delete packageFile.npmrc;
-      }
-    }
-  } else {
-    delete packageFile.npmrc;
-  }
-  packageFile.yarnrc = await platform.getFile(
-    upath.join(path.dirname(packageFile.packageFile), '.yarnrc')
-  );
-  if (packageFile.yarnrc) {
-    logger.info({ packageFile: packageFile.packageFile }, 'Found .yarnrc');
-  } else {
-    delete packageFile.yarnrc;
-  }
-  // Detect if lock files are used
-  const yarnLockFileName = upath.join(
-    path.dirname(packageFile.packageFile),
-    'yarn.lock'
-  );
-  const fileList = await platform.getFileList();
-  if (fileList.includes(yarnLockFileName)) {
-    logger.debug({ packageFile: packageFile.packageFile }, 'Found yarn.lock');
-    packageFile.yarnLock = yarnLockFileName;
-  }
-  const packageLockFileName = upath.join(
-    path.dirname(packageFile.packageFile),
-    'package-lock.json'
-  );
-  if (fileList.includes(packageLockFileName)) {
-    logger.debug(
-      { packageFile: packageFile.packageFile },
-      'Found package-lock.json'
-    );
-    packageFile.packageLock = packageLockFileName;
-  }
-  const npmShrinkwrapFileName = upath.join(
-    path.dirname(packageFile.packageFile),
-    'npm-shrinkwrap.json'
-  );
-  if (fileList.includes(npmShrinkwrapFileName)) {
-    logger.info(
-      { packageFile: packageFile.packageFile },
-      'Found npm-shrinkwrap.json'
-    );
-    packageFile.npmShrinkwrap = npmShrinkwrapFileName;
-  }
-  const shrinkwrapFileName = upath.join(
-    path.dirname(packageFile.packageFile),
-    'shrinkwrap.yaml'
-  );
-  if (fileList.includes(shrinkwrapFileName)) {
-    logger.debug(
-      { packageFile: packageFile.packageFile },
-      'Found shrinkwrap.yaml'
-    );
-    packageFile.shrinkwrapYaml = shrinkwrapFileName;
-  }
-  packageFile.currentPackageJsonVersion = packageFile.content.version;
-  return packageFile;
-}
diff --git a/lib/platform/github/index.js b/lib/platform/github/index.js
index 5eaeb2617264d7071c7403e1d53ba2d30dbc574c..cc05c428fe6456a8bb87d9c39d4e1e0d3458ad49 100644
--- a/lib/platform/github/index.js
+++ b/lib/platform/github/index.js
@@ -1273,10 +1273,14 @@ async function createBlob(fileContents) {
 
 // Return the commit SHA for a branch
 async function getBranchCommit(branchName) {
-  const res = await get(
-    `repos/${config.repository}/git/refs/heads/${branchName}`
-  );
-  return res.body.object.sha;
+  try {
+    const res = await get(
+      `repos/${config.repository}/git/refs/heads/${branchName}`
+    );
+    return res.body.object.sha;
+  } catch (err) /* istanbul ignore next */ {
+    return null;
+  }
 }
 
 async function getCommitDetails(commit) {
diff --git a/lib/workers/branch/get-updated.js b/lib/workers/branch/get-updated.js
new file mode 100644
index 0000000000000000000000000000000000000000..a028ac86ea80fcf4af89184803743e23c1859edc
--- /dev/null
+++ b/lib/workers/branch/get-updated.js
@@ -0,0 +1,52 @@
+const { get } = require('../../manager');
+
+module.exports = {
+  getUpdatedPackageFiles,
+};
+
+async function getUpdatedPackageFiles(config) {
+  logger.debug('manager.getUpdatedPackageFiles()');
+  logger.trace({ config });
+  const updatedPackageFiles = {};
+
+  for (const upgrade of config.upgrades) {
+    const { manager } = upgrade;
+    if (upgrade.type !== 'lockFileMaintenance') {
+      const existingContent =
+        updatedPackageFiles[upgrade.packageFile] ||
+        (await platform.getFile(upgrade.packageFile, config.parentBranch));
+      let newContent = existingContent;
+      const updateDependency = get(manager, 'updateDependency');
+      newContent = await updateDependency(existingContent, upgrade);
+      if (!newContent) {
+        if (config.parentBranch) {
+          logger.info('Rebasing branch after error updating content');
+          return getUpdatedPackageFiles({
+            ...config,
+            parentBranch: undefined,
+          });
+        }
+        throw new Error('Error updating branch content and cannot rebase');
+      }
+      if (newContent !== existingContent) {
+        if (config.parentBranch) {
+          // This ensure it's always 1 commit from Renovate
+          logger.info('Need to update package file so will rebase first');
+          return getUpdatedPackageFiles({
+            ...config,
+            parentBranch: undefined,
+          });
+        }
+        logger.debug('Updating packageFile content');
+        updatedPackageFiles[upgrade.packageFile] = newContent;
+      }
+    }
+  }
+  return {
+    parentBranch: config.parentBranch, // Need to overwrite original config
+    updatedPackageFiles: Object.keys(updatedPackageFiles).map(packageFile => ({
+      name: packageFile,
+      contents: updatedPackageFiles[packageFile],
+    })),
+  };
+}
diff --git a/lib/workers/branch/index.js b/lib/workers/branch/index.js
index 9563c77a6f17cd08636d2986f9ff8ceef72c3ce0..b5445e113eefad3fd3806b280bd01cb800665fbe 100644
--- a/lib/workers/branch/index.js
+++ b/lib/workers/branch/index.js
@@ -1,6 +1,6 @@
 const schedule = require('./schedule');
-const { getUpdatedPackageFiles } = require('../../manager');
-const { getUpdatedLockFiles } = require('./lock-files');
+const { getUpdatedPackageFiles } = require('./get-updated');
+const { getAdditionalFiles } = require('../../manager/npm/post-update');
 const { commitFilesToBranch } = require('./commit');
 const { getParentBranch } = require('./parent');
 const { tryBranchAutomerge } = require('./automerge');
@@ -14,7 +14,8 @@ module.exports = {
   processBranch,
 };
 
-async function processBranch(branchConfig) {
+async function processBranch(branchConfig, packageFiles) {
+  logger.debug(`processBranch with ${branchConfig.upgrades.length} upgrades`);
   const config = { ...branchConfig };
   const dependencies = config.upgrades
     .map(upgrade => upgrade.depName)
@@ -142,7 +143,7 @@ async function processBranch(branchConfig) {
     } else {
       logger.debug('No package files need updating');
     }
-    Object.assign(config, await getUpdatedLockFiles(config));
+    Object.assign(config, await getAdditionalFiles(config, packageFiles));
     if (config.updatedLockFiles && config.updatedLockFiles.length) {
       logger.debug(
         { updatedLockFiles: config.updatedLockFiles.map(f => f.name) },
diff --git a/lib/workers/package-file/dep-type.js b/lib/workers/package-file/dep-type.js
deleted file mode 100644
index 4758e1a172f18df8ff1887ad2e1d6013346a8ade..0000000000000000000000000000000000000000
--- a/lib/workers/package-file/dep-type.js
+++ /dev/null
@@ -1,63 +0,0 @@
-const configParser = require('../../config');
-const pkgWorker = require('./package');
-const { extractDependencies } = require('../../manager');
-const { applyPackageRules } = require('../../util/package-rules');
-
-module.exports = {
-  renovateDepType,
-  getDepConfig,
-};
-
-async function renovateDepType(packageContent, config) {
-  logger.setMeta({
-    repository: config.repository,
-    packageFile: config.packageFile,
-    depType: config.depType,
-  });
-  logger.debug('renovateDepType()');
-  logger.trace({ config });
-  if (config.enabled === false) {
-    logger.debug('depType is disabled');
-    return [];
-  }
-  const res = await extractDependencies(packageContent, config);
-  let deps;
-  if (res) {
-    ({ deps } = res);
-  } else {
-    deps = [];
-  }
-  if (config.lerna || config.workspaces || config.workspaceDir) {
-    deps = deps.filter(
-      dependency => config.monorepoPackages.indexOf(dependency.depName) === -1
-    );
-  }
-  deps = deps.filter(
-    dependency => config.ignoreDeps.indexOf(dependency.depName) === -1
-  );
-  logger.debug(`filtered deps length is ${deps.length}`);
-  logger.debug({ deps }, `filtered deps`);
-  // Obtain full config for each dependency
-  const depConfigs = deps.map(dep => module.exports.getDepConfig(config, dep));
-  logger.trace({ config: depConfigs }, `depConfigs`);
-  // renovateDepType can return more than one upgrade each
-  const pkgWorkers = depConfigs.map(depConfig =>
-    pkgWorker.renovatePackage(depConfig)
-  );
-  // Use Promise.all to execute npm queries in parallel
-  const allUpgrades = await Promise.all(pkgWorkers);
-  logger.trace({ config: allUpgrades }, `allUpgrades`);
-  // Squash arrays into one
-  const combinedUpgrades = [].concat(...allUpgrades);
-  logger.trace({ config: combinedUpgrades }, `combinedUpgrades`);
-  return combinedUpgrades;
-}
-
-function getDepConfig(depTypeConfig, dep) {
-  let depConfig = configParser.mergeChildConfig(depTypeConfig, dep);
-  // Apply any matching package rules
-  if (depConfig.packageRules) {
-    depConfig = applyPackageRules(depConfig);
-  }
-  return configParser.filterConfig(depConfig, 'package');
-}
diff --git a/lib/workers/package-file/index.js b/lib/workers/package-file/index.js
deleted file mode 100644
index 1c4b4521fe000daa81e7368ae8de4bd426464541..0000000000000000000000000000000000000000
--- a/lib/workers/package-file/index.js
+++ /dev/null
@@ -1,159 +0,0 @@
-const yarnLockParser = require('@yarnpkg/lockfile');
-const configParser = require('../../config');
-const depTypeWorker = require('./dep-type');
-const npmApi = require('../../datasource/npm');
-const upath = require('upath');
-
-module.exports = {
-  mightBeABrowserLibrary,
-  renovatePackageFile,
-  renovatePackageJson,
-};
-
-function mightBeABrowserLibrary(packageJson) {
-  // return true unless we're sure it's not a browser library
-  if (packageJson.private === true) {
-    // it's not published
-    return false;
-  }
-  if (packageJson.main === undefined) {
-    // it can't be required
-    return false;
-  }
-  // TODO: how can we know if it's a node.js library only, and not browser?
-  // Otherwise play it safe and return true
-  return true;
-}
-
-async function renovatePackageFile(config) {
-  logger.setMeta({
-    repository: config.repository,
-    packageFile: config.packageFile,
-  });
-  logger.debug('renovatePackageFile()');
-  const { manager } = config;
-  if (config.enabled === false) {
-    logger.info('packageFile is disabled');
-    return [];
-  }
-  if (manager === 'npm') {
-    return renovatePackageJson(config);
-  }
-  const content = await platform.getFile(config.packageFile);
-  return depTypeWorker.renovateDepType(content, config);
-}
-
-async function renovatePackageJson(input) {
-  const config = { ...input };
-  if (config.npmrc) {
-    logger.debug('Setting .npmrc');
-    npmApi.setNpmrc(
-      config.npmrc,
-      config.global ? config.global.exposeEnv : false
-    );
-  }
-  let upgrades = [];
-  logger.info(`Processing package file`);
-
-  let { yarnLock } = config;
-  if (!yarnLock && config.workspaceDir) {
-    yarnLock = upath.join(config.workspaceDir, 'yarn.lock');
-    if (await platform.getFile(yarnLock)) {
-      logger.debug({ yarnLock }, 'Using workspaces yarn.lock');
-    } else {
-      logger.debug('Yarn workspaces has no yarn.lock');
-      yarnLock = undefined;
-    }
-  }
-  if (yarnLock) {
-    try {
-      config.yarnLockParsed = yarnLockParser.parse(
-        await platform.getFile(yarnLock)
-      );
-      if (config.yarnLockParsed.type !== 'success') {
-        logger.info(
-          { type: config.yarnLockParsed.type },
-          'Error parsing yarn.lock - not success'
-        );
-        delete config.yarnLockParsed;
-      }
-      logger.trace({ yarnLockParsed: config.yarnLockParsed });
-    } catch (err) {
-      logger.info({ yarnLock }, 'Warning: Exception parsing yarn.lock');
-    }
-  } else if (config.packageLock) {
-    try {
-      config.packageLockParsed = JSON.parse(
-        await platform.getFile(config.packageLock)
-      );
-      logger.trace({ packageLockParsed: config.packageLockParsed });
-    } catch (err) {
-      logger.warn(
-        { packageLock: config.packageLock },
-        'Could not parse package-lock.json'
-      );
-    }
-  } else if (config.npmShrinkwrap) {
-    try {
-      config.npmShrinkwrapParsed = JSON.parse(
-        await platform.getFile(config.npmShrinkwrap)
-      );
-      logger.trace({ npmShrinkwrapParsed: config.npmShrinkwrapParsed });
-    } catch (err) {
-      logger.warn(
-        { npmShrinkwrap: config.npmShrinkwrap },
-        'Could not parse npm-shrinkwrap.json'
-      );
-    }
-  }
-
-  const depTypes = [
-    'dependencies',
-    'devDependencies',
-    'optionalDependencies',
-    'peerDependencies',
-    'engines',
-  ];
-  const depTypeConfigs = depTypes.map(depType => {
-    const depTypeConfig = { ...config, depType };
-    // Always pin devDependencies
-    // Pin dependencies if we're pretty sure it's not a browser library
-    if (
-      depTypeConfig.pinVersions === null &&
-      !depTypeConfig.upgradeInRange &&
-      (depType === 'devDependencies' ||
-        (depType === 'dependencies' && !mightBeABrowserLibrary(config.content)))
-    ) {
-      logger.debug({ depType }, 'Autodetecting pinVersions = true');
-      depTypeConfig.pinVersions = true;
-    }
-    logger.trace({ config: depTypeConfig }, 'depTypeConfig');
-    return configParser.filterConfig(depTypeConfig, 'depType');
-  });
-  logger.trace({ config: depTypeConfigs }, `depTypeConfigs`);
-  for (const depTypeConfig of depTypeConfigs) {
-    upgrades = upgrades.concat(
-      await depTypeWorker.renovateDepType(config.content, depTypeConfig)
-    );
-  }
-  if (
-    config.lockFileMaintenance.enabled &&
-    (config.yarnLock || config.packageLock)
-  ) {
-    logger.debug('lockFileMaintenance enabled');
-    // Maintain lock files
-    const lockFileMaintenanceConf = configParser.mergeChildConfig(
-      config,
-      config.lockFileMaintenance
-    );
-    lockFileMaintenanceConf.type = 'lockFileMaintenance';
-    logger.trace(
-      { config: lockFileMaintenanceConf },
-      `lockFileMaintenanceConf`
-    );
-    upgrades.push(configParser.filterConfig(lockFileMaintenanceConf, 'branch'));
-  }
-
-  logger.info('Finished processing package file');
-  return upgrades;
-}
diff --git a/lib/workers/package-file/package.js b/lib/workers/package-file/package.js
deleted file mode 100644
index caf5939ea2dc5db645b4925d669d4e469bed5e0a..0000000000000000000000000000000000000000
--- a/lib/workers/package-file/package.js
+++ /dev/null
@@ -1,38 +0,0 @@
-const configParser = require('../../config');
-const { getPackageUpdates } = require('../../manager');
-
-module.exports = {
-  renovatePackage,
-};
-
-// Returns all results for a given dependency config
-async function renovatePackage(config) {
-  // These are done in parallel so we don't setMeta to avoid conflicts
-  logger.trace(
-    { dependency: config.depName, config },
-    `renovatePackage(${config.depName})`
-  );
-  if (config.enabled === false) {
-    logger.debug('package is disabled');
-    return [];
-  }
-  const results = await getPackageUpdates(config);
-  if (results.length) {
-    logger.debug(
-      { dependency: config.depName, results },
-      `${config.depName} lookup results`
-    );
-  }
-  // Flatten the result on top of config, add repositoryUrl
-  return (
-    results
-      // combine upgrade fields with existing config
-      .map(res => configParser.mergeChildConfig(config, res))
-      // type can be major, minor, patch, pin, digest
-      .map(res => configParser.mergeChildConfig(res, res[res.type]))
-      // allow types to be disabled
-      .filter(res => res.enabled)
-      // strip unnecessary fields for next stage
-      .map(res => configParser.filterConfig(res, 'branch'))
-  );
-}
diff --git a/lib/workers/repository/extract/file-match.js b/lib/workers/repository/extract/file-match.js
new file mode 100644
index 0000000000000000000000000000000000000000..1b9540bcc7add08300e073e3a5cc2c466a9fb4f0
--- /dev/null
+++ b/lib/workers/repository/extract/file-match.js
@@ -0,0 +1,41 @@
+const minimatch = require('minimatch');
+
+module.exports = {
+  getIncludedFiles,
+  filterIgnoredFiles,
+  getMatchingFiles,
+};
+
+function getIncludedFiles(fileList, includePaths) {
+  if (!(includePaths && includePaths.length)) {
+    return fileList;
+  }
+  return fileList.filter(file =>
+    includePaths.some(
+      includePath => file === includePath || minimatch(file, includePath)
+    )
+  );
+}
+
+function filterIgnoredFiles(fileList, ignorePaths) {
+  if (!(ignorePaths && ignorePaths.length)) {
+    return fileList;
+  }
+  return fileList.filter(
+    file =>
+      !ignorePaths.some(
+        ignorePath => file.includes(ignorePath) || minimatch(file, ignorePath)
+      )
+  );
+}
+
+function getMatchingFiles(fileList, manager, fileMatch) {
+  let matchedFiles = [];
+  for (const match of fileMatch) {
+    logger.debug(`Using file match: ${match} for manager ${manager}`);
+    matchedFiles = matchedFiles.concat(
+      fileList.filter(file => file.match(new RegExp(match)))
+    );
+  }
+  return matchedFiles;
+}
diff --git a/lib/workers/repository/extract/index.js b/lib/workers/repository/extract/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..ce52ab69f389dc917d7b1fb3e41c68728da3354d
--- /dev/null
+++ b/lib/workers/repository/extract/index.js
@@ -0,0 +1,23 @@
+const { getManagerList } = require('../../../manager');
+const { getManagerConfig } = require('../../../config');
+const { getManagerPackageFiles } = require('./manager-files');
+
+module.exports = {
+  extractAllDependencies,
+};
+
+async function extractAllDependencies(config) {
+  const extractions = {};
+  let fileCount = 0;
+  for (const manager of getManagerList()) {
+    const managerConfig = getManagerConfig(config, manager);
+    managerConfig.manager = manager;
+    const packageFiles = await getManagerPackageFiles(config, managerConfig);
+    if (packageFiles.length) {
+      fileCount += packageFiles.length;
+      extractions[manager] = packageFiles;
+    }
+  }
+  logger.debug(`Found ${fileCount.length} package file(s)`);
+  return extractions;
+}
diff --git a/lib/workers/repository/extract/manager-files.js b/lib/workers/repository/extract/manager-files.js
new file mode 100644
index 0000000000000000000000000000000000000000..4f118298eff7e8c0e2819b783a647e99d1a4901a
--- /dev/null
+++ b/lib/workers/repository/extract/manager-files.js
@@ -0,0 +1,55 @@
+module.exports = {
+  getManagerPackageFiles,
+};
+
+const { extractDependencies, postExtract } = require('../../../manager');
+
+const {
+  getIncludedFiles,
+  filterIgnoredFiles,
+  getMatchingFiles,
+} = require('./file-match');
+
+async function getManagerPackageFiles(config, managerConfig) {
+  const { manager, enabled, includePaths, ignorePaths } = managerConfig;
+  logger.debug(`getPackageFiles(${manager})`);
+  if (!enabled) {
+    logger.debug(`${manager} is disabled`);
+    return [];
+  }
+  if (
+    config.enabledManagers.length &&
+    !config.enabledManagers.includes(manager)
+  ) {
+    logger.debug(`${manager} is not in enabledManagers list`);
+    return [];
+  }
+  let fileList = await platform.getFileList();
+  fileList = getIncludedFiles(fileList, includePaths);
+  fileList = filterIgnoredFiles(fileList, ignorePaths);
+  const matchedFiles = getMatchingFiles(
+    fileList,
+    manager,
+    config[manager].fileMatch
+  );
+  if (matchedFiles.length) {
+    logger.debug(
+      { matchedFiles },
+      `Matched ${matchedFiles.length} file(s) for manager ${manager}`
+    );
+  }
+  const packageFiles = [];
+  for (const packageFile of matchedFiles) {
+    const content = await platform.getFile(packageFile);
+    const res = await extractDependencies(manager, content, packageFile);
+    if (res) {
+      packageFiles.push({
+        packageFile,
+        manager,
+        ...res,
+      });
+    }
+  }
+  await postExtract(manager, packageFiles);
+  return packageFiles;
+}
diff --git a/lib/workers/repository/index.js b/lib/workers/repository/index.js
index aa3035a0f97739ce87d4165bd2d45e1813e6e166..0fc8dd743abd843b141dfd711c186906a42df82a 100644
--- a/lib/workers/repository/index.js
+++ b/lib/workers/repository/index.js
@@ -9,21 +9,18 @@ module.exports = {
   renovateRepository,
 };
 
+// istanbul ignore next
 async function renovateRepository(repoConfig) {
   let config = { ...repoConfig };
   logger.setMeta({ repository: config.repository });
   logger.info('Renovating repository');
-  logger.trace({ config }, 'renovateRepository()');
+  logger.trace({ config });
   try {
     config = await initRepo(config);
-    let res;
-    let branches;
-    let branchList;
-    let packageFiles;
-    ({ res, branches, branchList, packageFiles } = await processRepo(config)); // eslint-disable-line prefer-const
-    if (!config.repoIsOnboarded) {
-      res = await ensureOnboardingPr(config, packageFiles, branches);
-    }
+    const { res, branches, branchList, packageFiles } = await processRepo(
+      config
+    );
+    await ensureOnboardingPr(config, packageFiles, branches);
     await finaliseRepo(config, branchList);
     return processResult(config, res);
   } catch (err) /* istanbul ignore next */ {
diff --git a/lib/workers/repository/onboarding/branch/index.js b/lib/workers/repository/onboarding/branch/index.js
index 6a8ae60bc2db4ab0ff4d6277c6d1ad9455b3e798..dca69c9e73060dcd25359427cc5ffb7541d81ce5 100644
--- a/lib/workers/repository/onboarding/branch/index.js
+++ b/lib/workers/repository/onboarding/branch/index.js
@@ -1,4 +1,4 @@
-const { detectPackageFiles } = require('../../../../manager');
+const { extractAllDependencies } = require('../../extract');
 const { createOnboardingBranch } = require('./create');
 const { rebaseOnboardingBranch } = require('./rebase');
 const { isOnboarded, onboardingPrExists } = require('./check');
@@ -20,7 +20,7 @@ async function checkOnboardingBranch(config) {
     await rebaseOnboardingBranch(config);
   } else {
     logger.debug('Onboarding PR does not exist');
-    if ((await detectPackageFiles(config)).length === 0) {
+    if (Object.entries(await extractAllDependencies(config)).length === 0) {
       throw new Error('no-package-files');
     }
     logger.info('Need to create onboarding PR');
diff --git a/lib/workers/repository/onboarding/pr/index.js b/lib/workers/repository/onboarding/pr/index.js
index 7f1a7ae251b3944411bb49949f14908f5242c438..ffe72f2cae02c19aaab4389b1a6abff089c7f594 100644
--- a/lib/workers/repository/onboarding/pr/index.js
+++ b/lib/workers/repository/onboarding/pr/index.js
@@ -4,6 +4,9 @@ const { getBaseBranchDesc } = require('./base-branch');
 const { getPrList } = require('./pr-list');
 
 async function ensureOnboardingPr(config, packageFiles, branches) {
+  if (config.repoIsOnboarded) {
+    return;
+  }
   logger.debug('ensureOnboardingPr()');
   logger.trace({ config });
   const onboardingBranch = `renovate/configure`;
@@ -27,14 +30,17 @@ You can post questions in [our Config Help repository](https://github.com/renova
 ---
 `;
   let prBody = prTemplate;
-  if (packageFiles && packageFiles.length) {
+  if (packageFiles && Object.entries(packageFiles).length) {
+    let files = [];
+    for (const [manager, managerFiles] of Object.entries(packageFiles)) {
+      files = files.concat(
+        managerFiles.map(file => ` * \`${file.packageFile}\` (${manager})`)
+      );
+    }
     prBody =
       prBody.replace(
         '{{PACKAGE FILES}}',
-        '## Detected Package Files\n\n' +
-          packageFiles
-            .map(packageFile => ` * \`${packageFile.packageFile}\``)
-            .join('\n')
+        '## Detected Package Files\n\n' + files.join('\n')
       ) + '\n';
   } else {
     prBody = prBody.replace('{{PACKAGE FILES}}\n', '');
@@ -62,12 +68,12 @@ You can post questions in [our Config Help repository](https://github.com/renova
     // Check if existing PR needs updating
     if (existingPr.title === onboardingPrTitle && existingPr.body === prBody) {
       logger.info(`${existingPr.displayNumber} does not need updating`);
-      return 'onboarding';
+      return;
     }
     // PR must need updating
     await platform.updatePr(existingPr.number, onboardingPrTitle, prBody);
     logger.info(`Updated ${existingPr.displayNumber}`);
-    return 'onboarding';
+    return;
   }
   logger.info('Creating onboarding PR');
   const labels = [];
@@ -80,7 +86,6 @@ You can post questions in [our Config Help repository](https://github.com/renova
     useDefaultBranch
   );
   logger.info({ pr: pr.displayNumber }, 'Created onboarding PR');
-  return 'onboarding';
 }
 
 module.exports = {
diff --git a/lib/workers/repository/process/extract-update.js b/lib/workers/repository/process/extract-update.js
index c5d2d6e7198aa12d55de9c4a55f7666a3ba8d536..c1724e270b52bd4ac922e7a01761fb656da9e95b 100644
--- a/lib/workers/repository/process/extract-update.js
+++ b/lib/workers/repository/process/extract-update.js
@@ -1,21 +1,24 @@
-const { determineUpdates } = require('../updates');
 const { writeUpdates } = require('./write');
 const { sortBranches } = require('./sort');
-const { resolvePackageFiles } = require('../../../manager');
+const { fetchUpdates } = require('./fetch');
+const { branchifyUpgrades } = require('../updates/branchify');
+const { extractAllDependencies } = require('../extract');
 
 module.exports = {
   extractAndUpdate,
 };
 
-async function extractAndUpdate(input) {
-  let config = await resolvePackageFiles(input);
-  config = await determineUpdates(config);
-  const { branches, branchList, packageFiles } = config;
+async function extractAndUpdate(config) {
+  logger.debug('extractAndUpdate()');
+  const packageFiles = await extractAllDependencies(config);
+  logger.debug({ packageFiles }, 'packageFiles');
+  await fetchUpdates(config, packageFiles);
+  logger.debug({ packageFiles }, 'packageFiles with updates');
+  const { branches, branchList } = branchifyUpgrades(config, packageFiles);
   sortBranches(branches);
   let res;
   if (config.repoIsOnboarded) {
-    res = await writeUpdates(config);
+    res = await writeUpdates(config, packageFiles, branches);
   }
-  logger.setMeta({ repository: config.repository });
   return { res, branches, branchList, packageFiles };
 }
diff --git a/lib/workers/repository/process/fetch.js b/lib/workers/repository/process/fetch.js
new file mode 100644
index 0000000000000000000000000000000000000000..ae7f5edc29bea0248ebddcdda872ab0bee2abc68
--- /dev/null
+++ b/lib/workers/repository/process/fetch.js
@@ -0,0 +1,69 @@
+const pAll = require('p-all');
+
+const { getPackageUpdates } = require('../../../manager');
+const { mergeChildConfig } = require('../../../config');
+const { applyPackageRules } = require('../../../util/package-rules');
+const { getManagerConfig } = require('../../../config');
+
+module.exports = {
+  fetchUpdates,
+};
+
+async function fetchDepUpdates(packageFileConfig, dep) {
+  /* eslint-disable no-param-reassign */
+  const { manager, packageFile } = packageFileConfig;
+  const { depName, currentVersion } = dep;
+  let depConfig = mergeChildConfig(packageFileConfig, dep);
+  depConfig = applyPackageRules(depConfig);
+  dep.updates = [];
+  if (depConfig.ignoreDeps.includes(depName)) {
+    logger.debug({ depName: dep.depName }, 'Dependency is ignored');
+    dep.skipReason = 'ignored';
+  } else if (
+    depConfig.monorepoPackages &&
+    depConfig.monorepoPackages.includes(depName)
+  ) {
+    logger.debug(
+      { depName: dep.depName },
+      'Dependency is ignored as part of monorepo'
+    );
+    dep.skipReason = 'monorepo';
+  } else if (depConfig.enabled === false) {
+    logger.debug({ depName: dep.depName }, 'Dependency is disabled');
+    dep.skipReason = 'disabled';
+  } else {
+    dep.updates = await getPackageUpdates(manager, depConfig);
+    logger.debug({
+      packageFile,
+      manager,
+      depName,
+      currentVersion,
+      updates: dep.updates,
+    });
+  }
+  /* eslint-enable no-param-reassign */
+}
+
+async function fetchManagerPackagerFileUpdates(config, managerConfig, pFile) {
+  const packageFileConfig = mergeChildConfig(managerConfig, pFile);
+  const queue = pFile.deps.map(dep => () =>
+    fetchDepUpdates(packageFileConfig, dep)
+  );
+  await pAll(queue, { concurrency: 10 });
+}
+
+async function fetchManagerUpdates(config, packageFiles, manager) {
+  const managerConfig = getManagerConfig(config, manager);
+  const queue = packageFiles[manager].map(pFile => () =>
+    fetchManagerPackagerFileUpdates(config, managerConfig, pFile)
+  );
+  await pAll(queue, { concurrency: 5 });
+}
+
+async function fetchUpdates(config, packageFiles) {
+  logger.debug(`manager.fetchUpdates()`);
+  const allManagerJobs = Object.keys(packageFiles).map(manager =>
+    fetchManagerUpdates(config, packageFiles, manager)
+  );
+  await Promise.all(allManagerJobs);
+}
diff --git a/lib/workers/repository/process/index.js b/lib/workers/repository/process/index.js
index 61de6660b06d35e558284bed88eca7f608439c8c..1e58037ca265fc599e5444be03b51f05124ce950 100644
--- a/lib/workers/repository/process/index.js
+++ b/lib/workers/repository/process/index.js
@@ -6,6 +6,7 @@ module.exports = {
 };
 
 async function processRepo(config) {
+  logger.debug('processRepo()');
   if (config.baseBranches && config.baseBranches.length) {
     logger.info({ baseBranches: config.baseBranches }, 'baseBranches');
     let res;
@@ -24,5 +25,6 @@ async function processRepo(config) {
     }
     return { res, branches, branchList };
   }
+  logger.debug('No baseBranches');
   return extractAndUpdate(config);
 }
diff --git a/lib/workers/repository/process/limits.js b/lib/workers/repository/process/limits.js
new file mode 100644
index 0000000000000000000000000000000000000000..1ca9a18db2ee500ab77bd80f8dd45444781e39c3
--- /dev/null
+++ b/lib/workers/repository/process/limits.js
@@ -0,0 +1,57 @@
+const moment = require('moment');
+
+module.exports = {
+  getPrHourlyRemaining,
+  getConcurrentPrsRemaining,
+  getPrsRemaining,
+};
+
+async function getPrHourlyRemaining(config) {
+  if (config.prHourlyLimit) {
+    const prList = await platform.getPrList();
+    const currentHourStart = moment({
+      hour: moment().hour(),
+    });
+    try {
+      const soFarThisHour = prList.filter(
+        pr =>
+          pr.branchName !== 'renovate/configure' &&
+          moment(pr.createdAt).isAfter(currentHourStart)
+      ).length;
+      const prsRemaining = config.prHourlyLimit - soFarThisHour;
+      logger.info(`PR hourly limit remaining: ${prsRemaining}`);
+      return prsRemaining;
+    } catch (err) {
+      logger.error('Error checking PRs created per hour');
+    }
+  }
+  return 99;
+}
+
+async function getConcurrentPrsRemaining(config, branches) {
+  if (config.prConcurrentLimit) {
+    logger.debug(`Enforcing prConcurrentLimit (${config.prConcurrentLimit})`);
+    let currentlyOpen = 0;
+    for (const branch of branches) {
+      if (await platform.branchExists(branch.branchName)) {
+        currentlyOpen += 1;
+      }
+    }
+    logger.debug(`${currentlyOpen} PRs are currently open`);
+    const concurrentRemaining = config.prConcurrentLimit - currentlyOpen;
+    logger.info(`PR concurrent limit remaining: ${concurrentRemaining}`);
+    return concurrentRemaining;
+  }
+  return 99;
+}
+
+async function getPrsRemaining(config, branches) {
+  const hourlyRemaining = await module.exports.getPrHourlyRemaining(config);
+  const concurrentRemaining = await module.exports.getConcurrentPrsRemaining(
+    config,
+    branches
+  );
+  return hourlyRemaining < concurrentRemaining
+    ? hourlyRemaining
+    : concurrentRemaining;
+}
diff --git a/lib/workers/repository/process/write.js b/lib/workers/repository/process/write.js
index 56b7c87b3007cba781130aedc0e86d089fdcfd4c..97a6ca97dd824815bc16161b41964785cb0f30da 100644
--- a/lib/workers/repository/process/write.js
+++ b/lib/workers/repository/process/write.js
@@ -1,61 +1,32 @@
-const moment = require('moment');
 const tmp = require('tmp-promise');
 
 const branchWorker = require('../../branch');
+const { getPrsRemaining } = require('./limits');
 
 module.exports = {
   writeUpdates,
 };
 
-async function writeUpdates(config) {
-  let { branches } = config;
+async function writeUpdates(config, packageFiles, allBranches) {
+  let branches = allBranches;
   logger.info(`Processing ${branches.length} branch(es)`);
   if (!config.mirrorMode && 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 });
-  let prsRemaining = 99;
-  if (config.prHourlyLimit) {
-    const prList = await platform.getPrList();
-    const currentHourStart = moment({
-      hour: moment().hour(),
-    });
-    try {
-      prsRemaining =
-        config.prHourlyLimit -
-        prList.filter(
-          pr =>
-            pr.branchName !== 'renovate/configure' &&
-            moment(pr.createdAt).isAfter(currentHourStart)
-        ).length;
-      logger.info(`PR hourly limit remaining: ${prsRemaining}`);
-    } catch (err) {
-      logger.error('Error checking PRs created per hour');
-    }
-  }
-  if (config.prConcurrentLimit) {
-    logger.debug(`Enforcing prConcurrentLimit (${config.prConcurrentLimit})`);
-    let currentlyOpen = 0;
-    for (const branch of branches) {
-      if (await platform.branchExists(branch.branchName)) {
-        currentlyOpen += 1;
-      }
-    }
-    logger.debug(`${currentlyOpen} PRs are currently open`);
-    const concurrentRemaining = config.prConcurrentLimit - currentlyOpen;
-    logger.info(`PR concurrent limit remaining: ${concurrentRemaining}`);
-    prsRemaining =
-      prsRemaining < concurrentRemaining ? prsRemaining : concurrentRemaining;
-  }
+  let prsRemaining = await getPrsRemaining(config, branches);
   try {
     // eslint-disable-next-line no-param-reassign
     for (const branch of branches) {
-      const res = await branchWorker.processBranch({
-        ...branch,
-        tmpDir,
-        prHourlyLimitReached: prsRemaining <= 0,
-      });
+      const res = await branchWorker.processBranch(
+        {
+          ...branch,
+          tmpDir,
+          prHourlyLimitReached: prsRemaining <= 0,
+        },
+        packageFiles
+      );
       if (res === 'pr-closed' || res === 'automerged') {
         // Stop procesing other branches because base branch has been changed
         return res;
diff --git a/lib/workers/repository/result.js b/lib/workers/repository/result.js
index ee98917c872ac824b74eca9d38cc838efc38da2e..57b63b1af92daa50181d48191ee837c1036469b6 100644
--- a/lib/workers/repository/result.js
+++ b/lib/workers/repository/result.js
@@ -31,11 +31,7 @@ function processResult(config, result) {
     status = 'enabled';
   } else {
     status = 'onboarding';
-    if (result === 'onboarding') {
-      res = 'done';
-    } else {
-      res = result;
-    }
+    res = 'done';
   }
   return { res, status };
 }
diff --git a/lib/workers/repository/updates/branchify.js b/lib/workers/repository/updates/branchify.js
index 093276014fbf891198df7f7798f5f6f32834ca90..9118050ce4d35f5ba06fd0a9817ca33dc1d83a41 100644
--- a/lib/workers/repository/updates/branchify.js
+++ b/lib/workers/repository/updates/branchify.js
@@ -3,6 +3,7 @@ const slugify = require('slugify');
 const cleanGitRef = require('clean-git-ref').clean;
 
 const { generateBranchConfig } = require('./generate');
+const { flattenUpdates } = require('./flatten');
 
 /**
  * Clean git branch name
@@ -19,15 +20,18 @@ function cleanBranchName(branchName) {
     .replace(/\s/g, ''); // whitespace
 }
 
-function branchifyUpgrades(config) {
+function branchifyUpgrades(config, packageFiles) {
   logger.debug('branchifyUpgrades');
-  logger.trace({ config });
+  const updates = flattenUpdates(config, packageFiles);
+  logger.debug(`${updates.length} updates found`);
+  logger.debug({ updates });
+  logger.debug({ upgradeNames: updates.map(u => u.depName) });
   const errors = [];
   const warnings = [];
   const branchUpgrades = {};
   const branches = [];
-  for (const upg of config.upgrades) {
-    const update = { ...upg };
+  for (const u of updates) {
+    const update = { ...u };
     // Split out errors and warnings first
     if (update.type === 'error') {
       errors.push(update);
@@ -81,12 +85,10 @@ function branchifyUpgrades(config) {
     ? branches.map(upgrade => upgrade.branchName)
     : config.branchList;
   return {
-    ...config,
     errors: config.errors.concat(errors),
     warnings: config.warnings.concat(warnings),
     branches,
     branchList,
-    upgrades: null,
   };
 }
 
diff --git a/lib/workers/repository/updates/determine.js b/lib/workers/repository/updates/determine.js
deleted file mode 100644
index 058864e760ddbc03c13186d080dc1da71c3c3116..0000000000000000000000000000000000000000
--- a/lib/workers/repository/updates/determine.js
+++ /dev/null
@@ -1,46 +0,0 @@
-const packageFileWorker = require('../../package-file');
-const { mergeChildConfig, filterConfig } = require('../../../config');
-const { detectSemanticCommits } = require('./semantic');
-
-async function determineRepoUpgrades(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.setMeta({
-      repository: config.repository,
-      packageFile: packageFile.packageFile,
-    });
-    logger.debug('Getting packageFile config');
-    logger.trace({ fullPackageFile: packageFile });
-    let packageFileConfig = mergeChildConfig(config, packageFile);
-    packageFileConfig = filterConfig(packageFileConfig, 'packageFile');
-    upgrades = upgrades.concat(
-      await packageFileWorker.renovatePackageFile(packageFileConfig)
-    );
-  }
-  let semanticCommits;
-  if (upgrades.length) {
-    semanticCommits = await detectSemanticCommits(config);
-  }
-  // Sanitize depNames
-  upgrades = upgrades.map(upgrade => ({
-    ...upgrade,
-    semanticCommits,
-    depNameSanitized: upgrade.depName
-      ? upgrade.depName
-          .replace('@types/', '')
-          .replace('@', '')
-          .replace('/', '-')
-          .replace(/\s+/g, '-')
-          .toLowerCase()
-      : undefined,
-  }));
-
-  logger.debug('returning upgrades');
-  return { ...config, upgrades };
-}
-
-module.exports = { determineRepoUpgrades };
diff --git a/lib/workers/repository/updates/flatten.js b/lib/workers/repository/updates/flatten.js
new file mode 100644
index 0000000000000000000000000000000000000000..2792dca51d2ae692cbab25b4bc0221314142cfea
--- /dev/null
+++ b/lib/workers/repository/updates/flatten.js
@@ -0,0 +1,68 @@
+const {
+  getManagerConfig,
+  mergeChildConfig,
+  filterConfig,
+} = require('../../../config');
+const { applyPackageRules } = require('../../../util/package-rules');
+const { get } = require('../../../manager');
+
+module.exports = {
+  flattenUpdates,
+};
+
+function flattenUpdates(config, packageFiles) {
+  const updates = [];
+  for (const [manager, files] of Object.entries(packageFiles)) {
+    logger.debug(`flatten manager=${manager}`);
+    const managerConfig = getManagerConfig(config, manager);
+    logger.debug('Got manager config');
+    for (const packageFile of files) {
+      logger.debug('packageFile');
+      const packageFileConfig = mergeChildConfig(managerConfig, packageFile);
+      for (const dep of packageFile.deps) {
+        logger.debug('dep ' + dep.depName);
+        let depConfig = mergeChildConfig(packageFileConfig, dep);
+        logger.debug('got depConfig');
+        delete depConfig.deps;
+        depConfig = applyPackageRules(depConfig);
+        logger.debug('got depConfig with rules');
+        for (const update of dep.updates) {
+          logger.debug('update');
+          let updateConfig = mergeChildConfig(depConfig, update);
+          delete updateConfig.updates;
+          // apply major/minor/patch/pin/digest
+          updateConfig = mergeChildConfig(
+            updateConfig,
+            updateConfig[updateConfig.type]
+          );
+          updateConfig.depNameSanitized = updateConfig.depName
+            ? updateConfig.depName
+                .replace('@types/', '')
+                .replace('@', '')
+                .replace('/', '-')
+                .replace(/\s+/g, '-')
+                .toLowerCase()
+            : undefined;
+          delete updateConfig.repoIsOnboarded;
+          delete updateConfig.renovateJsonPresent;
+          updates.push(updateConfig);
+        }
+        logger.debug('Done dep');
+      }
+      logger.debug('Done packageFile');
+    }
+    logger.debug({ managerConfig });
+    if (
+      get(manager, 'supportsLockFileMaintenance') &&
+      managerConfig.lockFileMaintenance.enabled
+    ) {
+      const lockFileConfig = mergeChildConfig(
+        managerConfig,
+        managerConfig.lockFileMaintenance
+      );
+      lockFileConfig.type = 'lockFileMaintenance';
+      updates.push(lockFileConfig);
+    }
+  }
+  return updates.map(update => filterConfig(update, 'branch'));
+}
diff --git a/lib/workers/repository/updates/index.js b/lib/workers/repository/updates/index.js
deleted file mode 100644
index ceffa63e499571b1086a8bc2774bb464d8cbe50a..0000000000000000000000000000000000000000
--- a/lib/workers/repository/updates/index.js
+++ /dev/null
@@ -1,19 +0,0 @@
-const { determineRepoUpgrades } = require('./determine');
-const { branchifyUpgrades } = require('./branchify');
-
-module.exports = {
-  determineUpdates,
-};
-
-async function determineUpdates(input) {
-  let config = { ...input };
-  logger.debug('determineUpdates()');
-  logger.trace({ config });
-  config = await determineRepoUpgrades(config);
-  await platform.ensureIssueClosing(
-    'Action Required: Fix Renovate Configuration'
-  );
-  config = branchifyUpgrades(config);
-  logger.debug('Finished determining upgrades');
-  return config;
-}
diff --git a/test/_fixtures/npm/plocktest1/package-lock.json b/test/_fixtures/npm/plocktest1/package-lock.json
new file mode 100644
index 0000000000000000000000000000000000000000..a92861fada5af82fe90f2b8a1d5d41a6415aa131
--- /dev/null
+++ b/test/_fixtures/npm/plocktest1/package-lock.json
@@ -0,0 +1,57 @@
+{
+  "name": "plocktest1",
+  "version": "1.0.0",
+  "lockfileVersion": 1,
+  "requires": true,
+  "dependencies": {
+    "ansi-styles": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+      "requires": {
+        "color-convert": "1.9.1"
+      }
+    },
+    "chalk": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
+      "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
+      "requires": {
+        "ansi-styles": "3.2.1",
+        "escape-string-regexp": "1.0.5",
+        "supports-color": "5.4.0"
+      }
+    },
+    "color-convert": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz",
+      "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==",
+      "requires": {
+        "color-name": "1.1.3"
+      }
+    },
+    "color-name": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
+    },
+    "escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
+    },
+    "has-flag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+      "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
+    },
+    "supports-color": {
+      "version": "5.4.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
+      "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
+      "requires": {
+        "has-flag": "3.0.0"
+      }
+    }
+  }
+}
diff --git a/test/_fixtures/npm/plocktest1/package.json b/test/_fixtures/npm/plocktest1/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..0598aa4e1c215bc5e64d37b019dcd31be638c9d9
--- /dev/null
+++ b/test/_fixtures/npm/plocktest1/package.json
@@ -0,0 +1,15 @@
+{
+  "name": "plocktest1",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "chalk": "^2.4.1"
+  }
+}
diff --git a/test/_fixtures/npm/plocktest1/yarn.lock b/test/_fixtures/npm/plocktest1/yarn.lock
new file mode 100644
index 0000000000000000000000000000000000000000..f345998e02d0580f13d9d89c11806fbee19b2cae
--- /dev/null
+++ b/test/_fixtures/npm/plocktest1/yarn.lock
@@ -0,0 +1,41 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+ansi-styles@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+  dependencies:
+    color-convert "^1.9.0"
+
+chalk@^2.4.1:
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
+  dependencies:
+    ansi-styles "^3.2.1"
+    escape-string-regexp "^1.0.5"
+    supports-color "^5.3.0"
+
+color-convert@^1.9.0:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed"
+  dependencies:
+    color-name "^1.1.1"
+
+color-name@^1.1.1:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+
+escape-string-regexp@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+
+has-flag@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+
+supports-color@^5.3.0:
+  version "5.4.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54"
+  dependencies:
+    has-flag "^3.0.0"
diff --git a/test/manager/__snapshots__/index.spec.js.snap b/test/manager/__snapshots__/index.spec.js.snap
deleted file mode 100644
index 38ee9dcdec0086d82d0910e141cdce5b5f00ed53..0000000000000000000000000000000000000000
--- a/test/manager/__snapshots__/index.spec.js.snap
+++ /dev/null
@@ -1,95 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`manager detectPackageFiles(config) adds package files to object 1`] = `
-Array [
-  Object {
-    "manager": "npm",
-    "packageFile": "package.json",
-  },
-  Object {
-    "manager": "npm",
-    "packageFile": "backend/package.json",
-  },
-]
-`;
-
-exports[`manager detectPackageFiles(config) finds .nvmrc files 1`] = `
-Array [
-  Object {
-    "manager": "nvm",
-    "packageFile": ".nvmrc",
-  },
-]
-`;
-
-exports[`manager detectPackageFiles(config) finds .travis.yml files 1`] = `
-Array [
-  Object {
-    "manager": "travis",
-    "packageFile": ".travis.yml",
-  },
-]
-`;
-
-exports[`manager detectPackageFiles(config) finds Dockerfiles 1`] = `
-Array [
-  Object {
-    "manager": "docker",
-    "packageFile": "Dockerfile",
-  },
-  Object {
-    "manager": "docker",
-    "packageFile": "other/Dockerfile",
-  },
-  Object {
-    "manager": "docker",
-    "packageFile": "another/Dockerfile",
-  },
-]
-`;
-
-exports[`manager detectPackageFiles(config) finds WORKSPACE files 1`] = `
-Array [
-  Object {
-    "manager": "bazel",
-    "packageFile": "WORKSPACE",
-  },
-  Object {
-    "manager": "bazel",
-    "packageFile": "other/WORKSPACE",
-  },
-]
-`;
-
-exports[`manager detectPackageFiles(config) finds meteor package files 1`] = `
-Array [
-  Object {
-    "manager": "meteor",
-    "packageFile": "modules/something/package.js",
-  },
-]
-`;
-
-exports[`manager detectPackageFiles(config) ignores node modules 1`] = `
-Array [
-  Object {
-    "manager": "npm",
-    "packageFile": "package.json",
-  },
-]
-`;
-
-exports[`manager detectPackageFiles(config) ignores node modules 2`] = `undefined`;
-
-exports[`manager detectPackageFiles(config) ignores node modules 3`] = `undefined`;
-
-exports[`manager detectPackageFiles(config) skips meteor package files with no json 1`] = `Array []`;
-
-exports[`manager detectPackageFiles(config) uses includePaths 1`] = `
-Array [
-  Object {
-    "manager": "npm",
-    "packageFile": "package.json",
-  },
-]
-`;
diff --git a/test/manager/__snapshots__/resolve.spec.js.snap b/test/manager/__snapshots__/resolve.spec.js.snap
deleted file mode 100644
index 9157937dece08963cb16ca19d1cc049b8a849bbb..0000000000000000000000000000000000000000
--- a/test/manager/__snapshots__/resolve.spec.js.snap
+++ /dev/null
@@ -1,123 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`manager/resolve resolvePackageFiles() clears npmrc and yarnrc fields 1`] = `
-Array [
-  Object {
-    "content": Object {
-      "name": "something",
-      "renovate": Object {
-        "automerge": true,
-      },
-      "version": "1.0.0",
-    },
-    "currentPackageJsonVersion": "1.0.0",
-    "fileMatch": Array [
-      "(^|/)package.json$",
-    ],
-    "manager": "npm",
-    "packageFile": "package.json",
-  },
-]
-`;
-
-exports[`manager/resolve resolvePackageFiles() detect package.json and adds error if cannot parse (onboarding) 1`] = `Array []`;
-
-exports[`manager/resolve resolvePackageFiles() detect package.json and throws error if cannot parse (onboarded) 1`] = `[Error: config-validation]`;
-
-exports[`manager/resolve resolvePackageFiles() detects accompanying files 1`] = `
-Array [
-  Object {
-    "content": Object {
-      "name": "package.json",
-      "version": "0.0.1",
-    },
-    "currentPackageJsonVersion": "0.0.1",
-    "fileMatch": Array [
-      "(^|/)package.json$",
-    ],
-    "manager": "npm",
-    "npmShrinkwrap": "npm-shrinkwrap.json",
-    "npmrc": "npmrc",
-    "packageFile": "package.json",
-    "packageLock": "package-lock.json",
-    "shrinkwrapYaml": "shrinkwrap.yaml",
-    "yarnLock": "yarn.lock",
-    "yarnrc": "yarnrc",
-  },
-]
-`;
-
-exports[`manager/resolve resolvePackageFiles() resolves docker 1`] = `
-Array [
-  Object {
-    "commitMessageTopic": "{{{depName}}} Docker tag",
-    "content": "# comment
-FROM node:8
-",
-    "digest": Object {
-      "branchTopic": "{{{depNameSanitized}}}-{{{currentTag}}}",
-      "commitMessageExtra": "to {{newDigestShort}}",
-      "commitMessageTopic": "{{{depName}}}:{{{currentTag}}} Docker digest",
-      "group": Object {
-        "commitMessageTopic": "{{{groupName}}}",
-        "prBody": "This Pull Request updates Dockerfiles to the latest image digests. For details on Renovate's Docker support, please visit https://renovatebot.com/docs/language-support/docker\\n\\n{{#if schedule}}\\n**Note**: This PR was created on a configured schedule (\\"{{{schedule}}}\\"{{#if timezone}} in timezone \`{{{timezone}}}\`{{/if}}) and will not receive updates outside those times.\\n{{/if}}\\n\\n{{#each upgrades as |upgrade|}}\\n-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{{depName}}}\`{{/if}}: \`{{upgrade.newDigest}}\`\\n{{/each}}\\n\\n**Important**: Renovate will wait until you have merged this Pin request before creating PRs for any *upgrades*. If you do not wish to pin anything, please update your config accordingly instead of leaving this PR open.\\n\\n{{#if hasErrors}}\\n\\n---\\n\\n# Errors\\n\\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\\n\\n{{#each errors as |error|}}\\n-   \`{{error.depName}}\`: {{error.message}}\\n{{/each}}\\n{{/if}}\\n\\n{{#if hasWarnings}}\\n\\n---\\n\\n# Warnings\\n\\nPlease make sure the following warnings are safe to ignore:\\n\\n{{#each warnings as |warning|}}\\n-   \`{{warning.depName}}\`: {{warning.message}}\\n{{/each}}\\n{{/if}}",
-      },
-      "prBody": "This Pull Request updates Docker base image \`{{{depName}}}:{{{currentTag}}}\` to the latest digest (\`{{{newDigest}}}\`). For details on Renovate's Docker support, please visit https://renovatebot.com/docs/language-support/docker\\n\\n{{#if schedule}}\\n**Note**: This PR was created on a configured schedule (\\"{{{schedule}}}\\"{{#if timezone}} in timezone \`{{{timezone}}}\`{{/if}}) and will not receive updates outside those times.\\n{{/if}}\\n\\n{{#if hasErrors}}\\n\\n---\\n\\n# Errors\\n\\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\\n\\n{{#each errors as |error|}}\\n-   \`{{error.depName}}\`: {{error.message}}\\n{{/each}}\\n{{/if}}\\n\\n{{#if hasWarnings}}\\n\\n---\\n\\n# Warnings\\n\\nPlease make sure the following warnings are safe to ignore:\\n\\n{{#each warnings as |warning|}}\\n-   \`{{warning.depName}}\`: {{warning.message}}\\n{{/each}}\\n{{/if}}",
-    },
-    "fileMatch": Array [
-      "(^|/)Dockerfile$",
-    ],
-    "group": Object {
-      "commitMessageTopic": "{{{groupName}}} Docker tags",
-      "prBody": "This Pull Request updates Dockerfiles to use image digests.\\n\\n{{#if schedule}}\\n**Note**: This PR was created on a configured schedule (\\"{{{schedule}}}\\"{{#if timezone}} in timezone \`{{{timezone}}}\`{{/if}}) and will not receive updates outside those times.\\n{{/if}}\\n\\n{{#each upgrades as |upgrade|}}\\n-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{{depName}}}\`{{/if}}: \`{{upgrade.newDigest}}\`\\n{{/each}}\\n\\n{{#if hasErrors}}\\n\\n---\\n\\n# Errors\\n\\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\\n\\n{{#each errors as |error|}}\\n-   \`{{error.depName}}\`: {{error.message}}\\n{{/each}}\\n{{/if}}\\n\\n{{#if hasWarnings}}\\n\\n---\\n\\n# Warnings\\n\\nPlease make sure the following warnings are safe to ignore:\\n\\n{{#each warnings as |warning|}}\\n-   \`{{warning.depName}}\`: {{warning.message}}\\n{{/each}}\\n{{/if}}",
-    },
-    "major": Object {
-      "enabled": false,
-    },
-    "manager": "docker",
-    "managerBranchPrefix": "docker-",
-    "packageFile": "Dockerfile",
-    "pin": Object {
-      "commitMessageExtra": "",
-      "group": Object {
-        "branchTopic": "digests-pin",
-        "commitMessageTopic": "{{{groupName}}}",
-        "prBody": "This Pull Request pins Dockerfiles to use image digests. For details on Renovate's Docker support, please visit https://renovatebot.com/docs/language-support/docker\\n\\n{{#if schedule}}\\n**Note**: This PR was created on a configured schedule (\\"{{{schedule}}}\\"{{#if timezone}} in timezone \`{{{timezone}}}\`{{/if}}) and will not receive updates outside those times.\\n{{/if}}\\n\\n{{#each upgrades as |upgrade|}}\\n-   {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}\`{{{depName}}}\`{{/if}}: \`{{upgrade.newDigest}}\`\\n{{/each}}\\n\\n**Important**: Renovate will wait until you have merged this Pin request before creating PRs for any *upgrades*. If you do not wish to pin anything, please update your config accordingly instead of leaving this PR open.\\n\\n{{#if hasErrors}}\\n\\n---\\n\\n# Errors\\n\\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\\n\\n{{#each errors as |error|}}\\n-   \`{{error.depName}}\`: {{error.message}}\\n{{/each}}\\n{{/if}}\\n\\n{{#if hasWarnings}}\\n\\n---\\n\\n# Warnings\\n\\nPlease make sure the following warnings are safe to ignore:\\n\\n{{#each warnings as |warning|}}\\n-   \`{{warning.depName}}\`: {{warning.message}}\\n{{/each}}\\n{{/if}}",
-      },
-      "groupName": "Docker digests",
-      "prBody": "This Pull Request pins Docker base image \`{{{depName}}}:{{{currentTag}}}\` to use a digest (\`{{{newDigest}}}\`).\\nThis digest will then be kept updated via Pull Requests whenever the image is updated on the Docker registry. For details on Renovate's Docker support, please visit https://renovatebot.com/docs/language-support/docker\\n\\n{{#if schedule}}\\n**Note**: This PR was created on a configured schedule (\\"{{{schedule}}}\\"{{#if timezone}} in timezone \`{{{timezone}}}\`{{/if}}) and will not receive updates outside those times.\\n{{/if}}\\n\\n**Important**: Renovate will wait until you have merged this Pin request before creating PRs for any *upgrades*. If you do not wish to pin anything, please update your config accordingly instead of leaving this PR open.\\n\\n{{#if hasErrors}}\\n\\n---\\n\\n# Errors\\n\\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\\n\\n{{#each errors as |error|}}\\n-   \`{{error.depName}}\`: {{error.message}}\\n{{/each}}\\n{{/if}}\\n\\n{{#if hasWarnings}}\\n\\n---\\n\\n# Warnings\\n\\nPlease make sure the following warnings are safe to ignore:\\n\\n{{#each warnings as |warning|}}\\n-   \`{{warning.depName}}\`: {{warning.message}}\\n{{/each}}\\n{{/if}}",
-    },
-    "prBody": "This Pull Request updates Docker base image \`{{{depName}}}\` from tag \`{{{currentTag}}}\` to new tag \`{{{newTag}}}\`. For details on Renovate's Docker support, please visit https://renovatebot.com/docs/language-support/docker\\n\\n{{#if schedule}}\\n**Note**: This PR was created on a configured schedule (\\"{{{schedule}}}\\"{{#if timezone}} in timezone \`{{{timezone}}}\`{{/if}}) and will not receive updates outside those times.\\n{{/if}}\\n\\n{{#if hasErrors}}\\n\\n---\\n\\n# Errors\\n\\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\\n\\n{{#each errors as |error|}}\\n-   \`{{error.depName}}\`: {{error.message}}\\n{{/each}}\\n{{/if}}\\n\\n{{#if hasWarnings}}\\n\\n---\\n\\n# Warnings\\n\\nPlease make sure the following warnings are safe to ignore:\\n\\n{{#each warnings as |warning|}}\\n-   \`{{warning.depName}}\`: {{warning.message}}\\n{{/each}}\\n{{/if}}",
-  },
-]
-`;
-
-exports[`manager/resolve resolvePackageFiles() resolves package files without own resolve 1`] = `
-Array [
-  Object {
-    "content": "git_repository(\\n",
-    "fileMatch": Array [
-      "(^|/)WORKSPACE$",
-    ],
-    "manager": "bazel",
-    "packageFile": "WORKSPACE",
-  },
-]
-`;
-
-exports[`manager/resolve resolvePackageFiles() strips npmrc with NPM_TOKEN 1`] = `
-Array [
-  Object {
-    "content": Object {
-      "name": "package.json",
-      "version": "0.0.1",
-    },
-    "currentPackageJsonVersion": "0.0.1",
-    "fileMatch": Array [
-      "(^|/)package.json$",
-    ],
-    "manager": "npm",
-    "packageFile": "package.json",
-  },
-]
-`;
diff --git a/test/manager/docker/extract.spec.js b/test/manager/docker/extract.spec.js
index 77d4e48aa5233d610c3da1c7e17a1e9eacb2e24f..bdf06f9a519e783836b4c795e908474a72119c95 100644
--- a/test/manager/docker/extract.spec.js
+++ b/test/manager/docker/extract.spec.js
@@ -6,6 +6,10 @@ describe('lib/manager/docker/extract', () => {
     beforeEach(() => {
       config = {};
     });
+    it('handles no FROM', () => {
+      const res = extractDependencies('no from!', config);
+      expect(res).toBe(null);
+    });
     it('handles naked dep', () => {
       const res = extractDependencies('FROM node\n', config).deps;
       expect(res).toMatchSnapshot();
diff --git a/test/manager/index.spec.js b/test/manager/index.spec.js
index 9f2b3f56c828e1f865e9ecc013526cab957fad2b..6e67b20a617ef8fdcfbe0e1eff10b1a61426d0e3 100644
--- a/test/manager/index.spec.js
+++ b/test/manager/index.spec.js
@@ -1,202 +1,27 @@
-const defaultConfig = require('../../lib/config/defaults').getConfig();
 const manager = require('../../lib/manager');
-const npm = require('../../lib/manager/npm');
-const meteor = require('../../lib/manager/meteor');
-const docker = require('../../lib/manager/docker');
-const node = require('../../lib/manager/travis');
-const bazel = require('../../lib/manager/bazel');
-
-const path = require('path');
-const fs = require('fs-extra');
-
-const { getUpdatedPackageFiles } = manager;
 
 describe('manager', () => {
-  describe('detectPackageFiles(config)', () => {
-    let config;
-    beforeEach(() => {
-      config = {
-        ...JSON.parse(JSON.stringify(defaultConfig)),
-        warnings: [],
-      };
-    });
-    it('skips if not in enabledManagers list', async () => {
-      platform.getFileList.mockReturnValueOnce([
-        'package.json',
-        'backend/package.json',
-      ]);
-      config.enabledManagers = ['docker'];
-      const res = await manager.detectPackageFiles(config);
-      expect(res).toHaveLength(0);
-    });
-    it('skips if language is disabled', async () => {
-      platform.getFileList.mockReturnValueOnce([
-        'package.json',
-        '.circleci/config.yml',
-      ]);
-      config.docker.enabled = false;
-      const res = await manager.detectPackageFiles(config);
-      expect(res).toHaveLength(1);
-    });
-    it('adds package files to object', async () => {
-      platform.getFileList.mockReturnValueOnce([
-        'package.json',
-        'backend/package.json',
-      ]);
-      const res = await manager.detectPackageFiles(config);
-      expect(res).toMatchSnapshot();
-      expect(res).toHaveLength(2);
-    });
-    it('finds meteor package files', async () => {
-      config.meteor.enabled = true;
-      platform.getFileList.mockReturnValueOnce([
-        'modules/something/package.js',
-      ]); // meteor
-      platform.getFile.mockReturnValueOnce('Npm.depends( {} )');
-      const res = await manager.detectPackageFiles(config);
-      expect(res).toMatchSnapshot();
-      expect(res).toHaveLength(1);
-    });
-    it('skips meteor package files with no json', async () => {
-      config.meteor.enabled = true;
-      platform.getFileList.mockReturnValueOnce([
-        'modules/something/package.js',
-      ]); // meteor
-      platform.getFile.mockReturnValueOnce('Npm.depends(packages)');
-      const res = await manager.detectPackageFiles(config);
-      expect(res).toMatchSnapshot();
-      expect(res).toHaveLength(0);
-    });
-    it('finds Dockerfiles', async () => {
-      platform.getFileList.mockReturnValueOnce([
-        'Dockerfile',
-        'other/Dockerfile',
-        'another/Dockerfile',
-      ]);
-      const res = await manager.detectPackageFiles(config);
-      expect(res).toMatchSnapshot();
-      expect(res).toHaveLength(3);
-    });
-    it('finds .travis.yml files', async () => {
-      config.travis.enabled = true;
-      platform.getFileList.mockReturnValueOnce([
-        '.travis.yml',
-        'other/.travis.yml',
-      ]);
-      platform.getFile.mockReturnValueOnce('sudo: true\nnode_js:\n  -8\n');
-      const res = await manager.detectPackageFiles(config);
-      expect(res).toMatchSnapshot();
-      expect(res).toHaveLength(1);
-    });
-    it('finds .nvmrc files', async () => {
-      config.travis.enabled = true;
-      platform.getFileList.mockReturnValueOnce(['.nvmrc', 'other/.nvmrc']);
-      const res = await manager.detectPackageFiles(config);
-      expect(res).toMatchSnapshot();
-      expect(res).toHaveLength(1);
-    });
-    it('finds WORKSPACE files', async () => {
-      config.bazel.enabled = true;
-      platform.getFileList.mockReturnValueOnce([
-        'WORKSPACE',
-        'other/WORKSPACE',
-        'empty/WORKSPACE',
-      ]);
-      platform.getFile.mockReturnValueOnce('\n\ngit_repository(\n\n)\n');
-      platform.getFile.mockReturnValueOnce(
-        await fs.readFile(
-          path.resolve('test/_fixtures/bazel/WORKSPACE1'),
-          'utf8'
-        )
-      );
-      platform.getFile.mockReturnValueOnce('foo');
-      const res = await manager.detectPackageFiles(config);
-      expect(res).toMatchSnapshot();
-      expect(res).toHaveLength(2);
-    });
-    it('ignores node modules', async () => {
-      platform.getFileList.mockReturnValueOnce([
-        'package.json',
-        'node_modules/backend/package.json',
-      ]);
-      const res = await manager.detectPackageFiles(config);
-      expect(res).toMatchSnapshot();
-      expect(res).toHaveLength(1);
-      expect(res.foundIgnoredPaths).toMatchSnapshot();
-      expect(res.warnings).toMatchSnapshot();
-    });
-    it('uses includePaths', async () => {
-      platform.getFileList.mockReturnValueOnce([
-        'package.json',
-        'backend/package.json',
-      ]);
-      config.includePaths = ['package.json'];
-      const res = await manager.detectPackageFiles(config);
-      expect(res).toMatchSnapshot();
-      expect(res).toHaveLength(1);
+  describe('get()', () => {
+    it('gets something', () => {
+      expect(manager.get('docker', 'extractDependencies')).not.toBe(null);
     });
   });
-  describe('getUpdatedPackageFiles', () => {
-    let config;
-    beforeEach(() => {
-      config = {
-        ...defaultConfig,
-        parentBranch: 'some-branch',
-      };
-      npm.updateDependency = jest.fn();
-      docker.updateDependency = jest.fn();
-      meteor.updateDependency = jest.fn();
-      node.updateDependency = jest.fn();
-      bazel.updateDependency = jest.fn();
-    });
-    it('returns empty if lock file maintenance', async () => {
-      config.upgrades = [{ type: 'lockFileMaintenance' }];
-      const res = await getUpdatedPackageFiles(config);
-      expect(res.updatedPackageFiles).toHaveLength(0);
+  describe('getLanguageList()', () => {
+    it('gets', () => {
+      expect(manager.getLanguageList()).not.toBe(null);
     });
-    it('recurses if updateDependency error', async () => {
-      config.parentBranch = 'some-branch';
-      config.canRebase = true;
-      config.upgrades = [{ packageFile: 'package.json', manager: 'npm' }];
-      npm.updateDependency.mockReturnValueOnce(null);
-      npm.updateDependency.mockReturnValueOnce('some content');
-      const res = await getUpdatedPackageFiles(config);
-      expect(res.updatedPackageFiles).toHaveLength(1);
+  });
+  describe('getManagerList()', () => {
+    it('gets', () => {
+      expect(manager.getManagerList()).not.toBe(null);
     });
-    it('errors if cannot rebase', async () => {
-      config.upgrades = [{ packageFile: 'package.json', manager: 'npm' }];
-      let e;
-      try {
-        await getUpdatedPackageFiles(config);
-      } catch (err) {
-        e = err;
-      }
-      expect(e).toBeDefined();
+  });
+  describe('postExtract()', () => {
+    it('returns null', () => {
+      expect(manager.postExtract('docker', [])).toBe(null);
     });
-    it('returns updated files', async () => {
-      config.parentBranch = 'some-branch';
-      config.canRebase = true;
-      config.upgrades = [
-        { packageFile: 'package.json', manager: 'npm' },
-        { packageFile: 'Dockerfile', manager: 'docker' },
-        { packageFile: 'packages/foo/package.js', manager: 'meteor' },
-        { packageFile: '.travis.yml', manager: 'travis' },
-        { packageFile: 'WORKSPACE', manager: 'bazel' },
-      ];
-      platform.getFile.mockReturnValueOnce('old content 1');
-      platform.getFile.mockReturnValueOnce('old content 1');
-      platform.getFile.mockReturnValueOnce('old content 2');
-      platform.getFile.mockReturnValueOnce('old content 3');
-      platform.getFile.mockReturnValueOnce('old travis');
-      platform.getFile.mockReturnValueOnce('old WORKSPACE');
-      npm.updateDependency.mockReturnValueOnce('new content 1');
-      npm.updateDependency.mockReturnValueOnce('new content 1+');
-      docker.updateDependency.mockReturnValueOnce('new content 2');
-      meteor.updateDependency.mockReturnValueOnce('old content 3');
-      node.updateDependency.mockReturnValueOnce('old travis');
-      bazel.updateDependency.mockReturnValueOnce('old WORKSPACE');
-      const res = await getUpdatedPackageFiles(config);
-      expect(res.updatedPackageFiles).toHaveLength(2);
+    it('returns postExtract', () => {
+      expect(manager.postExtract('npm', [])).not.toBe(null);
     });
   });
 });
diff --git a/test/manager/meteor/__snapshots__/extract.spec.js.snap b/test/manager/meteor/__snapshots__/extract.spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..1b0358fd19ecf95755771d56905d8448d61f7f9b
--- /dev/null
+++ b/test/manager/meteor/__snapshots__/extract.spec.js.snap
@@ -0,0 +1,32 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`lib/manager/meteor/extract extractDependencies() returns results 1`] = `
+Object {
+  "deps": Array [
+    Object {
+      "currentVersion": "0.2.0",
+      "depName": "xml2js",
+    },
+    Object {
+      "currentVersion": "0.6.0",
+      "depName": "xml-crypto",
+    },
+    Object {
+      "currentVersion": "0.1.19",
+      "depName": "xmldom",
+    },
+    Object {
+      "currentVersion": "2.7.10",
+      "depName": "connect",
+    },
+    Object {
+      "currentVersion": "2.6.4",
+      "depName": "xmlbuilder",
+    },
+    Object {
+      "currentVersion": "0.2.0",
+      "depName": "querystring",
+    },
+  ],
+}
+`;
diff --git a/test/manager/meteor/extract.spec.js b/test/manager/meteor/extract.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..3bd3b66a9c2c7348ca1bfb175ca3cb07ce35f3db
--- /dev/null
+++ b/test/manager/meteor/extract.spec.js
@@ -0,0 +1,30 @@
+const fs = require('fs');
+const path = require('path');
+const { extractDependencies } = require('../../../lib/manager/meteor/extract');
+
+function readFixture(fixture) {
+  return fs.readFileSync(
+    path.resolve(__dirname, `../../_fixtures/meteor/${fixture}`),
+    'utf8'
+  );
+}
+
+const input01Content = readFixture('package-1.js');
+
+describe('lib/manager/meteor/extract', () => {
+  describe('extractDependencies()', () => {
+    let config;
+    beforeEach(() => {
+      config = {};
+    });
+    it('returns empty if fails to parse', () => {
+      const res = extractDependencies('blahhhhh:foo:@what\n', config);
+      expect(res).toBe(null);
+    });
+    it('returns results', () => {
+      const res = extractDependencies(input01Content, config);
+      expect(res).toMatchSnapshot();
+      expect(res.deps).toHaveLength(6);
+    });
+  });
+});
diff --git a/test/manager/npm/__snapshots__/extract.spec.js.snap b/test/manager/npm/__snapshots__/extract.spec.js.snap
deleted file mode 100644
index 62d37b3953692ea147d68973872f993bfb935573..0000000000000000000000000000000000000000
--- a/test/manager/npm/__snapshots__/extract.spec.js.snap
+++ /dev/null
@@ -1,42 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`manager/npm/extract .extractDependencies(npmExtract, depType) each element contains non-null depType, depName, currentVersion 1`] = `
-Array [
-  Object {
-    "currentVersion": "6.5.0",
-    "depName": "autoprefixer",
-    "depType": "dependencies",
-    "lockedVersion": undefined,
-  },
-  Object {
-    "currentVersion": "~1.6.0",
-    "depName": "bower",
-    "depType": "dependencies",
-    "lockedVersion": undefined,
-  },
-  Object {
-    "currentVersion": "13.1.0",
-    "depName": "browserify",
-    "depType": "dependencies",
-    "lockedVersion": undefined,
-  },
-  Object {
-    "currentVersion": "0.9.2",
-    "depName": "browserify-css",
-    "depType": "dependencies",
-    "lockedVersion": undefined,
-  },
-  Object {
-    "currentVersion": "0.22.0",
-    "depName": "cheerio",
-    "depType": "dependencies",
-    "lockedVersion": undefined,
-  },
-  Object {
-    "currentVersion": "1.21.0",
-    "depName": "config",
-    "depType": "dependencies",
-    "lockedVersion": undefined,
-  },
-]
-`;
diff --git a/test/manager/npm/__snapshots__/monorepo.spec.js.snap b/test/manager/npm/__snapshots__/monorepo.spec.js.snap
deleted file mode 100644
index 95ba2aea0242af8c7c3309d7635e3839f329d1e5..0000000000000000000000000000000000000000
--- a/test/manager/npm/__snapshots__/monorepo.spec.js.snap
+++ /dev/null
@@ -1,24 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`manager/npm/monorepo checkMonorepos adds lerna packages 1`] = `
-Array [
-  "@a/b",
-  "@a/c",
-]
-`;
-
-exports[`manager/npm/monorepo checkMonorepos adds nested yarn workspaces 1`] = `
-Array [
-  "@a/b",
-  "@a/c",
-]
-`;
-
-exports[`manager/npm/monorepo checkMonorepos adds yarn workspaces 1`] = `
-Array [
-  "@a/b",
-  "@a/c",
-]
-`;
-
-exports[`manager/npm/monorepo checkMonorepos skips if no lerna packages 1`] = `Array []`;
diff --git a/test/manager/npm/extract.spec.js b/test/manager/npm/extract.spec.js
deleted file mode 100644
index a0514565cffb720d9102c6d2aca663db069e4316..0000000000000000000000000000000000000000
--- a/test/manager/npm/extract.spec.js
+++ /dev/null
@@ -1,123 +0,0 @@
-const fs = require('fs');
-const path = require('path');
-const npmExtract = require('../../../lib/manager/npm/extract');
-
-function readFixture(fixture) {
-  return fs.readFileSync(
-    path.resolve(__dirname, `../../_fixtures/package-json/${fixture}`),
-    'utf8'
-  );
-}
-
-const input01Content = readFixture('inputs/01.json');
-const input02Content = readFixture('inputs/02.json');
-
-describe('manager/npm/extract', () => {
-  describe('.extractDependencies(npmExtract, depType)', () => {
-    it('returns an array of correct length (dependencies)', () => {
-      const config = {
-        depType: 'dependencies',
-      };
-      const extractedDependencies = npmExtract.extractDependencies(
-        JSON.parse(input01Content),
-        config
-      ).deps;
-      extractedDependencies.should.be.instanceof(Array);
-      extractedDependencies.should.have.length(6);
-    });
-    it('returns an array of correct length (devDependencies)', () => {
-      const config = {
-        depType: 'devDependencies',
-      };
-      const extractedDependencies = npmExtract.extractDependencies(
-        JSON.parse(input01Content),
-        config
-      ).deps;
-      extractedDependencies.should.be.instanceof(Array);
-      extractedDependencies.should.have.length(4);
-    });
-    it('each element contains non-null depType, depName, currentVersion', () => {
-      const config = {
-        depType: 'dependencies',
-      };
-      const extractedDependencies = npmExtract.extractDependencies(
-        JSON.parse(input01Content),
-        config
-      ).deps;
-      expect(extractedDependencies).toMatchSnapshot();
-      extractedDependencies
-        .every(dep => dep.depType && dep.depName && dep.currentVersion)
-        .should.eql(true);
-    });
-    it('supports null devDependencies indirect', () => {
-      const config = {
-        depType: 'dependencies',
-      };
-      const extractedDependencies = npmExtract.extractDependencies(
-        JSON.parse(input02Content),
-        config
-      ).deps;
-      extractedDependencies.should.be.instanceof(Array);
-      extractedDependencies.should.have.length(6);
-    });
-    it('supports null', () => {
-      const config = {
-        depType: 'fooDpendencies',
-      };
-      const extractedDependencies = npmExtract.extractDependencies(
-        JSON.parse(input02Content),
-        config
-      ).deps;
-      extractedDependencies.should.be.instanceof(Array);
-      extractedDependencies.should.have.length(0);
-    });
-    it('finds a locked version in package-lock.json', () => {
-      const packageLockParsed = {
-        dependencies: { chalk: { version: '2.0.1' } },
-      };
-      const config = {
-        depType: 'dependencies',
-        packageLockParsed,
-      };
-      const extractedDependencies = npmExtract.extractDependencies(
-        { dependencies: { chalk: '^2.0.0', foo: '^1.0.0' } },
-        config
-      ).deps;
-      extractedDependencies.should.be.instanceof(Array);
-      extractedDependencies.should.have.length(2);
-      expect(extractedDependencies[0].lockedVersion).toBeDefined();
-      expect(extractedDependencies[1].lockedVersion).toBeUndefined();
-    });
-    it('finds a locked version in yarn.lock', () => {
-      const yarnLockParsed = {
-        object: { 'chalk@^2.0.0': { version: '2.0.1' } },
-      };
-      const config = {
-        depType: 'dependencies',
-        yarnLockParsed,
-      };
-      const extractedDependencies = npmExtract.extractDependencies(
-        { dependencies: { chalk: '^2.0.0', foo: '^1.0.0' } },
-        config
-      ).deps;
-      extractedDependencies.should.be.instanceof(Array);
-      extractedDependencies.should.have.length(2);
-      expect(extractedDependencies[0].lockedVersion).toBeDefined();
-      expect(extractedDependencies[1].lockedVersion).toBeUndefined();
-    });
-    it('handles lock error', () => {
-      const config = {
-        depType: 'dependencies',
-        packageLockParsed: true,
-      };
-      const extractedDependencies = npmExtract.extractDependencies(
-        { dependencies: { chalk: '^2.0.0', foo: '^1.0.0' } },
-        config
-      ).deps;
-      extractedDependencies.should.be.instanceof(Array);
-      extractedDependencies.should.have.length(2);
-      expect(extractedDependencies[0].lockedVersion).toBeUndefined();
-      expect(extractedDependencies[1].lockedVersion).toBeUndefined();
-    });
-  });
-});
diff --git a/test/manager/npm/extract/__snapshots__/index.spec.js.snap b/test/manager/npm/extract/__snapshots__/index.spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..4d6546f2c014dbc462d549f4d78f3fad20b48a1c
--- /dev/null
+++ b/test/manager/npm/extract/__snapshots__/index.spec.js.snap
@@ -0,0 +1,202 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`manager/npm/extract .extractDependencies() finds a lock file 1`] = `
+Object {
+  "deps": Array [
+    Object {
+      "currentVersion": "6.5.0",
+      "depName": "autoprefixer",
+      "depType": "dependencies",
+    },
+    Object {
+      "currentVersion": "~1.6.0",
+      "depName": "bower",
+      "depType": "dependencies",
+    },
+    Object {
+      "currentVersion": "13.1.0",
+      "depName": "browserify",
+      "depType": "dependencies",
+    },
+    Object {
+      "currentVersion": "0.9.2",
+      "depName": "browserify-css",
+      "depType": "dependencies",
+    },
+    Object {
+      "currentVersion": "0.22.0",
+      "depName": "cheerio",
+      "depType": "dependencies",
+    },
+    Object {
+      "currentVersion": "1.21.0",
+      "depName": "config",
+      "depType": "dependencies",
+    },
+    Object {
+      "currentVersion": "^1.5.8",
+      "depName": "angular",
+      "depType": "devDependencies",
+    },
+    Object {
+      "currentVersion": "1.5.8",
+      "depName": "angular-touch",
+      "depType": "devDependencies",
+    },
+    Object {
+      "currentVersion": "1.5.8",
+      "depName": "angular-sanitize",
+      "depType": "devDependencies",
+    },
+    Object {
+      "currentVersion": "4.0.0-beta.1",
+      "depName": "@angular/core",
+      "depType": "devDependencies",
+    },
+  ],
+  "lernaClient": undefined,
+  "lernaDir": undefined,
+  "lernaPackages": undefined,
+  "npmLock": undefined,
+  "npmrc": undefined,
+  "packageJsonName": "renovate",
+  "packageJsonVersion": "1.0.0",
+  "pnpmShrinkwrap": undefined,
+  "yarnLock": "yarn.lock",
+  "yarnWorkspacesPackages": undefined,
+}
+`;
+
+exports[`manager/npm/extract .extractDependencies() finds lerna 1`] = `
+Object {
+  "deps": Array [
+    Object {
+      "currentVersion": "6.5.0",
+      "depName": "autoprefixer",
+      "depType": "dependencies",
+    },
+    Object {
+      "currentVersion": "~1.6.0",
+      "depName": "bower",
+      "depType": "dependencies",
+    },
+    Object {
+      "currentVersion": "13.1.0",
+      "depName": "browserify",
+      "depType": "dependencies",
+    },
+    Object {
+      "currentVersion": "0.9.2",
+      "depName": "browserify-css",
+      "depType": "dependencies",
+    },
+    Object {
+      "currentVersion": "0.22.0",
+      "depName": "cheerio",
+      "depType": "dependencies",
+    },
+    Object {
+      "currentVersion": "1.21.0",
+      "depName": "config",
+      "depType": "dependencies",
+    },
+    Object {
+      "currentVersion": "^1.5.8",
+      "depName": "angular",
+      "depType": "devDependencies",
+    },
+    Object {
+      "currentVersion": "1.5.8",
+      "depName": "angular-touch",
+      "depType": "devDependencies",
+    },
+    Object {
+      "currentVersion": "1.5.8",
+      "depName": "angular-sanitize",
+      "depType": "devDependencies",
+    },
+    Object {
+      "currentVersion": "4.0.0-beta.1",
+      "depName": "@angular/core",
+      "depType": "devDependencies",
+    },
+  ],
+  "lernaClient": undefined,
+  "lernaDir": ".",
+  "lernaPackages": undefined,
+  "npmLock": undefined,
+  "npmrc": undefined,
+  "packageJsonName": "renovate",
+  "packageJsonVersion": "1.0.0",
+  "pnpmShrinkwrap": undefined,
+  "yarnLock": undefined,
+  "yarnWorkspacesPackages": undefined,
+}
+`;
+
+exports[`manager/npm/extract .extractDependencies() returns an array of dependencies 1`] = `
+Object {
+  "deps": Array [
+    Object {
+      "currentVersion": "6.5.0",
+      "depName": "autoprefixer",
+      "depType": "dependencies",
+    },
+    Object {
+      "currentVersion": "~1.6.0",
+      "depName": "bower",
+      "depType": "dependencies",
+    },
+    Object {
+      "currentVersion": "13.1.0",
+      "depName": "browserify",
+      "depType": "dependencies",
+    },
+    Object {
+      "currentVersion": "0.9.2",
+      "depName": "browserify-css",
+      "depType": "dependencies",
+    },
+    Object {
+      "currentVersion": "0.22.0",
+      "depName": "cheerio",
+      "depType": "dependencies",
+    },
+    Object {
+      "currentVersion": "1.21.0",
+      "depName": "config",
+      "depType": "dependencies",
+    },
+    Object {
+      "currentVersion": "^1.5.8",
+      "depName": "angular",
+      "depType": "devDependencies",
+    },
+    Object {
+      "currentVersion": "1.5.8",
+      "depName": "angular-touch",
+      "depType": "devDependencies",
+    },
+    Object {
+      "currentVersion": "1.5.8",
+      "depName": "angular-sanitize",
+      "depType": "devDependencies",
+    },
+    Object {
+      "currentVersion": "4.0.0-beta.1",
+      "depName": "@angular/core",
+      "depType": "devDependencies",
+    },
+  ],
+  "lernaClient": undefined,
+  "lernaDir": undefined,
+  "lernaPackages": undefined,
+  "npmLock": undefined,
+  "npmrc": undefined,
+  "packageJsonName": "renovate",
+  "packageJsonVersion": "1.0.0",
+  "pnpmShrinkwrap": undefined,
+  "yarnLock": undefined,
+  "yarnWorkspacesPackages": undefined,
+}
+`;
diff --git a/test/manager/npm/extract/__snapshots__/locked-versions.spec.js.snap b/test/manager/npm/extract/__snapshots__/locked-versions.spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..66c457d6d20f69e1dd577265dc2426922bff4828
--- /dev/null
+++ b/test/manager/npm/extract/__snapshots__/locked-versions.spec.js.snap
@@ -0,0 +1,60 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`manager/npm/extract/locked-versions .getLockedVersions() ignores pnpm 1`] = `
+Array [
+  Object {
+    "deps": Array [
+      Object {
+        "currentVersion": "1.0.0",
+        "depName": "a",
+      },
+      Object {
+        "currentVersion": "2.0.0",
+        "depName": "b",
+      },
+    ],
+    "pnpmShrinkwrap": "shrinkwrap.yaml",
+  },
+]
+`;
+
+exports[`manager/npm/extract/locked-versions .getLockedVersions() uses package-lock.json 1`] = `
+Array [
+  Object {
+    "deps": Array [
+      Object {
+        "currentVersion": "1.0.0",
+        "depName": "a",
+        "lockedVersion": "1.0.0",
+      },
+      Object {
+        "currentVersion": "2.0.0",
+        "depName": "b",
+        "lockedVersion": "2.0.0",
+      },
+    ],
+    "npmLock": "package-lock.json",
+  },
+]
+`;
+
+exports[`manager/npm/extract/locked-versions .getLockedVersions() uses yarn.lock 1`] = `
+Array [
+  Object {
+    "deps": Array [
+      Object {
+        "currentVersion": "1.0.0",
+        "depName": "a",
+        "lockedVersion": "1.0.0",
+      },
+      Object {
+        "currentVersion": "2.0.0",
+        "depName": "b",
+        "lockedVersion": "2.0.0",
+      },
+    ],
+    "npmLock": "package-lock.json",
+    "yarnLock": "yarn.lock",
+  },
+]
+`;
diff --git a/test/manager/npm/extract/__snapshots__/monorepo.spec.js.snap b/test/manager/npm/extract/__snapshots__/monorepo.spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..afa5ecca68b86445bf210f3f7fc975441a6b16ac
--- /dev/null
+++ b/test/manager/npm/extract/__snapshots__/monorepo.spec.js.snap
@@ -0,0 +1,69 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`manager/npm/extract .extractDependencies() uses lerna package settings 1`] = `
+Array [
+  Object {
+    "lernaDir": ".",
+    "lernaPackages": Array [
+      "packages/*",
+    ],
+    "packageFile": "package.json",
+  },
+  Object {
+    "lernaDir": ".",
+    "monorepoPackages": Array [
+      "@org/b",
+    ],
+    "npmLock": undefined,
+    "packageFile": "packages/a/package.json",
+    "packageJsonName": "@org/a",
+    "yarnLock": undefined,
+  },
+  Object {
+    "lernaDir": ".",
+    "monorepoPackages": Array [
+      "@org/a",
+    ],
+    "npmLock": undefined,
+    "packageFile": "packages/b/package.json",
+    "packageJsonName": "@org/b",
+    "yarnLock": undefined,
+  },
+]
+`;
+
+exports[`manager/npm/extract .extractDependencies() uses yarn workspaces package settings 1`] = `
+Array [
+  Object {
+    "lernaClient": "yarn",
+    "lernaDir": ".",
+    "lernaPackages": Array [
+      "oldpackages/*",
+    ],
+    "packageFile": "package.json",
+    "yarnWorkspacesPackages": Array [
+      "packages/*",
+    ],
+  },
+  Object {
+    "lernaDir": ".",
+    "monorepoPackages": Array [
+      "@org/b",
+    ],
+    "npmLock": undefined,
+    "packageFile": "packages/a/package.json",
+    "packageJsonName": "@org/a",
+    "yarnLock": undefined,
+  },
+  Object {
+    "lernaDir": ".",
+    "monorepoPackages": Array [
+      "@org/a",
+    ],
+    "npmLock": undefined,
+    "packageFile": "packages/b/package.json",
+    "packageJsonName": "@org/b",
+    "yarnLock": undefined,
+  },
+]
+`;
diff --git a/test/manager/npm/extract/__snapshots__/npm.spec.js.snap b/test/manager/npm/extract/__snapshots__/npm.spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..38581285147b250ace8bdf4ee910768907639635
--- /dev/null
+++ b/test/manager/npm/extract/__snapshots__/npm.spec.js.snap
@@ -0,0 +1,13 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`manager/npm/extract/npm .getNpmLock() extracts 1`] = `
+Object {
+  "ansi-styles": "3.2.1",
+  "chalk": "2.4.1",
+  "color-convert": "1.9.1",
+  "color-name": "1.1.3",
+  "escape-string-regexp": "1.0.5",
+  "has-flag": "3.0.0",
+  "supports-color": "5.4.0",
+}
+`;
diff --git a/test/manager/npm/extract/__snapshots__/yarn.spec.js.snap b/test/manager/npm/extract/__snapshots__/yarn.spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..22e0d707fc31695366270015c8c6f8d149269de8
--- /dev/null
+++ b/test/manager/npm/extract/__snapshots__/yarn.spec.js.snap
@@ -0,0 +1,13 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`manager/npm/extract/yarn .getYarnLock() extracts 1`] = `
+Object {
+  "ansi-styles@^3.2.1": "3.2.1",
+  "chalk@^2.4.1": "2.4.1",
+  "color-convert@^1.9.0": "1.9.1",
+  "color-name@^1.1.1": "1.1.3",
+  "escape-string-regexp@^1.0.5": "1.0.5",
+  "has-flag@^3.0.0": "3.0.0",
+  "supports-color@^5.3.0": "5.4.0",
+}
+`;
diff --git a/test/manager/npm/extract/index.spec.js b/test/manager/npm/extract/index.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..d10e126b655bceefc03fc20069ebd5c0c9b02a69
--- /dev/null
+++ b/test/manager/npm/extract/index.spec.js
@@ -0,0 +1,76 @@
+const fs = require('fs');
+const path = require('path');
+const npmExtract = require('../../../../lib/manager/npm/extract');
+
+function readFixture(fixture) {
+  return fs.readFileSync(
+    path.resolve(__dirname, `../../../_fixtures/package-json/${fixture}`),
+    'utf8'
+  );
+}
+
+const input01Content = readFixture('inputs/01.json');
+
+describe('manager/npm/extract', () => {
+  describe('.extractDependencies()', () => {
+    beforeEach(() => {
+      platform.getFile.mockReturnValue(null);
+    });
+    it('returns null if cannot parse', async () => {
+      const res = await npmExtract.extractDependencies(
+        'not json',
+        'package.json'
+      );
+      expect(res).toBe(null);
+    });
+    it('returns null if no deps', async () => {
+      const res = await npmExtract.extractDependencies('{}', 'package.json');
+      expect(res).toBe(null);
+    });
+    it('handles invalid', async () => {
+      const res = await npmExtract.extractDependencies(
+        '{"dependencies": true, "devDependencies": []}',
+        'package.json'
+      );
+      expect(res).toBe(null);
+    });
+    it('returns an array of dependencies', async () => {
+      const res = await npmExtract.extractDependencies(
+        input01Content,
+        'package.json'
+      );
+      expect(res).toMatchSnapshot();
+    });
+    it('finds a lock file', async () => {
+      platform.getFile = jest.fn(fileName => {
+        if (fileName === 'yarn.lock') {
+          return '# yarn.lock';
+        }
+        return null;
+      });
+      const res = await npmExtract.extractDependencies(
+        input01Content,
+        'package.json'
+      );
+      expect(res).toMatchSnapshot();
+    });
+    it('finds lerna', async () => {
+      platform.getFile = jest.fn(fileName => {
+        if (fileName === 'lerna.json') {
+          return '{}';
+        }
+        return null;
+      });
+      const res = await npmExtract.extractDependencies(
+        input01Content,
+        'package.json'
+      );
+      expect(res).toMatchSnapshot();
+    });
+  });
+  describe('.postExtract()', () => {
+    it('runs', async () => {
+      await npmExtract.postExtract([]);
+    });
+  });
+});
diff --git a/test/manager/npm/extract/locked-versions.spec.js b/test/manager/npm/extract/locked-versions.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..98872ed275803c183bbb86e1f76b898795d65efa
--- /dev/null
+++ b/test/manager/npm/extract/locked-versions.spec.js
@@ -0,0 +1,82 @@
+const {
+  getLockedVersions,
+} = require('../../../../lib/manager/npm/extract/locked-versions');
+
+const npm = require('../../../../lib/manager/npm/extract/npm');
+const yarn = require('../../../../lib/manager/npm/extract/yarn');
+
+jest.mock('../../../../lib/manager/npm/extract/npm');
+jest.mock('../../../../lib/manager/npm/extract/yarn');
+
+describe('manager/npm/extract/locked-versions', () => {
+  describe('.getLockedVersions()', () => {
+    it('uses yarn.lock', async () => {
+      yarn.getYarnLock.mockReturnValue({
+        'a@1.0.0': '1.0.0',
+        'b@2.0.0': '2.0.0',
+        'c@2.0.0': '3.0.0',
+      });
+      const packageFiles = [
+        {
+          npmLock: 'package-lock.json',
+          yarnLock: 'yarn.lock',
+          deps: [
+            {
+              depName: 'a',
+              currentVersion: '1.0.0',
+            },
+            {
+              depName: 'b',
+              currentVersion: '2.0.0',
+            },
+          ],
+        },
+      ];
+      await getLockedVersions(packageFiles);
+      expect(packageFiles).toMatchSnapshot();
+    });
+    it('uses package-lock.json', async () => {
+      npm.getNpmLock.mockReturnValue({
+        a: '1.0.0',
+        b: '2.0.0',
+        c: '3.0.0',
+      });
+      const packageFiles = [
+        {
+          npmLock: 'package-lock.json',
+          deps: [
+            {
+              depName: 'a',
+              currentVersion: '1.0.0',
+            },
+            {
+              depName: 'b',
+              currentVersion: '2.0.0',
+            },
+          ],
+        },
+      ];
+      await getLockedVersions(packageFiles);
+      expect(packageFiles).toMatchSnapshot();
+    });
+    it('ignores pnpm', async () => {
+      const packageFiles = [
+        {
+          pnpmShrinkwrap: 'shrinkwrap.yaml',
+          deps: [
+            {
+              depName: 'a',
+              currentVersion: '1.0.0',
+            },
+            {
+              depName: 'b',
+              currentVersion: '2.0.0',
+            },
+          ],
+        },
+      ];
+      await getLockedVersions(packageFiles);
+      expect(packageFiles).toMatchSnapshot();
+    });
+  });
+});
diff --git a/test/manager/npm/extract/monorepo.spec.js b/test/manager/npm/extract/monorepo.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..b4be349bad429e762f605ee01ec75a62a201a114
--- /dev/null
+++ b/test/manager/npm/extract/monorepo.spec.js
@@ -0,0 +1,52 @@
+const {
+  detectMonorepos,
+} = require('../../../../lib/manager/npm/extract/monorepo');
+
+describe('manager/npm/extract', () => {
+  describe('.extractDependencies()', () => {
+    it('uses lerna package settings', async () => {
+      const packageFiles = [
+        {
+          packageFile: 'package.json',
+          lernaDir: '.',
+          lernaPackages: ['packages/*'],
+        },
+        {
+          packageFile: 'packages/a/package.json',
+          packageJsonName: '@org/a',
+        },
+        {
+          packageFile: 'packages/b/package.json',
+          packageJsonName: '@org/b',
+        },
+      ];
+      await detectMonorepos(packageFiles);
+      expect(packageFiles).toMatchSnapshot();
+      expect(packageFiles[1].lernaDir).toEqual('.');
+      expect(packageFiles[1].monorepoPackages).toEqual(['@org/b']);
+    });
+    it('uses yarn workspaces package settings', async () => {
+      const packageFiles = [
+        {
+          packageFile: 'package.json',
+          lernaDir: '.',
+          lernaPackages: ['oldpackages/*'],
+          lernaClient: 'yarn',
+          yarnWorkspacesPackages: ['packages/*'],
+        },
+        {
+          packageFile: 'packages/a/package.json',
+          packageJsonName: '@org/a',
+        },
+        {
+          packageFile: 'packages/b/package.json',
+          packageJsonName: '@org/b',
+        },
+      ];
+      await detectMonorepos(packageFiles);
+      expect(packageFiles).toMatchSnapshot();
+      expect(packageFiles[1].lernaDir).toEqual('.');
+      expect(packageFiles[1].monorepoPackages).toEqual(['@org/b']);
+    });
+  });
+});
diff --git a/test/manager/npm/extract/npm.spec.js b/test/manager/npm/extract/npm.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..a9e66b645d6ce05f07ba1c085d1c1b51dc848900
--- /dev/null
+++ b/test/manager/npm/extract/npm.spec.js
@@ -0,0 +1,21 @@
+const fs = require('fs');
+const { getNpmLock } = require('../../../../lib/manager/npm/extract/npm');
+
+describe('manager/npm/extract/npm', () => {
+  describe('.getNpmLock()', () => {
+    it('returns empty if failed to parse', async () => {
+      platform.getFile.mockReturnValueOnce('abcd');
+      const res = await getNpmLock('package.json');
+      expect(Object.keys(res)).toHaveLength(0);
+    });
+    it('extracts', async () => {
+      const plocktest1Lock = fs.readFileSync(
+        'test/_fixtures/npm/plocktest1/package-lock.json'
+      );
+      platform.getFile.mockReturnValueOnce(plocktest1Lock);
+      const res = await getNpmLock('package.json');
+      expect(res).toMatchSnapshot();
+      expect(Object.keys(res)).toHaveLength(7);
+    });
+  });
+});
diff --git a/test/manager/npm/extract/yarn.spec.js b/test/manager/npm/extract/yarn.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..32657ef369595563f17784ea0be0b3fe8e215c90
--- /dev/null
+++ b/test/manager/npm/extract/yarn.spec.js
@@ -0,0 +1,22 @@
+const fs = require('fs');
+const { getYarnLock } = require('../../../../lib/manager/npm/extract/yarn');
+
+describe('manager/npm/extract/yarn', () => {
+  describe('.getYarnLock()', () => {
+    it('returns empty if exception parsing', async () => {
+      platform.getFile.mockReturnValueOnce('abcd');
+      const res = await getYarnLock('package.json');
+      expect(Object.keys(res)).toHaveLength(0);
+    });
+    it('extracts', async () => {
+      const plocktest1Lock = fs.readFileSync(
+        'test/_fixtures/npm/plocktest1/yarn.lock',
+        'utf8'
+      );
+      platform.getFile.mockReturnValueOnce(plocktest1Lock);
+      const res = await getYarnLock('package.json');
+      expect(res).toMatchSnapshot();
+      expect(Object.keys(res)).toHaveLength(7);
+    });
+  });
+});
diff --git a/test/manager/npm/monorepo.spec.js b/test/manager/npm/monorepo.spec.js
deleted file mode 100644
index 85bff2ddc5c12f329eea79ac173352d5e84344b4..0000000000000000000000000000000000000000
--- a/test/manager/npm/monorepo.spec.js
+++ /dev/null
@@ -1,80 +0,0 @@
-const { checkMonorepos } = require('../../../lib/manager/npm/monorepos');
-
-let config;
-beforeEach(() => {
-  jest.resetAllMocks();
-  config = { ...require('../../_fixtures/config') };
-  config.errors = [];
-  config.warnings = [];
-});
-
-describe('manager/npm/monorepo', () => {
-  describe('checkMonorepos', () => {
-    it('adds yarn workspaces', async () => {
-      config.packageFiles = [
-        {
-          packageFile: 'package.json',
-          content: { workspaces: ['packages/*'] },
-        },
-        {
-          packageFile: 'packages/something/package.json',
-          content: { name: '@a/b' },
-        },
-        {
-          packageFile: 'packages/something-else/package.json',
-          content: { name: '@a/c' },
-        },
-      ];
-      const res = await checkMonorepos(config);
-      expect(res.monorepoPackages).toMatchSnapshot();
-    });
-    it('adds nested yarn workspaces', async () => {
-      config.packageFiles = [
-        {
-          packageFile: 'frontend/package.json',
-          content: { workspaces: ['packages/*'] },
-        },
-        {
-          packageFile: 'frontend/packages/something/package.json',
-          content: { name: '@a/b' },
-        },
-        {
-          packageFile: 'frontend/packages/something-else/package.json',
-          content: { name: '@a/c' },
-        },
-      ];
-      const res = await checkMonorepos(config);
-      expect(res.monorepoPackages).toMatchSnapshot();
-    });
-    it('adds lerna packages', async () => {
-      config.packageFiles = [
-        {
-          packageFile: 'package.json',
-          content: {},
-        },
-        {
-          packageFile: 'packages/something/package.json',
-          content: { name: '@a/b' },
-        },
-        {
-          packageFile: 'packages/something-else/package.json',
-          content: { name: '@a/c' },
-        },
-      ];
-      platform.getFile.mockReturnValue('{ "packages": ["packages/*"] }');
-      const res = await checkMonorepos(config);
-      expect(res.monorepoPackages).toMatchSnapshot();
-    });
-    it('skips if no lerna packages', async () => {
-      config.packageFiles = [
-        {
-          packageFile: 'package.json',
-          content: {},
-        },
-      ];
-      platform.getFile.mockReturnValue(null);
-      const res = await checkMonorepos(config);
-      expect(res.monorepoPackages).toMatchSnapshot();
-    });
-  });
-});
diff --git a/test/manager/resolve.spec.js b/test/manager/resolve.spec.js
deleted file mode 100644
index 38637091a245dd796133bcde805e2cccc5dc0893..0000000000000000000000000000000000000000
--- a/test/manager/resolve.spec.js
+++ /dev/null
@@ -1,131 +0,0 @@
-const manager = require('../../lib/manager');
-
-const { resolvePackageFiles } = manager;
-
-let config;
-beforeEach(() => {
-  jest.resetAllMocks();
-  config = { ...require('../_fixtures/config') };
-  config.global = {};
-  config.errors = [];
-  config.warnings = [];
-});
-
-describe('manager/resolve', () => {
-  describe('resolvePackageFiles()', () => {
-    beforeEach(() => {
-      manager.detectPackageFiles = jest.fn();
-    });
-    it('detect package.json and adds error if cannot parse (onboarding)', async () => {
-      manager.detectPackageFiles.mockReturnValueOnce([
-        { packageFile: 'package.json', manager: 'npm' },
-      ]);
-      platform.getFileList.mockReturnValueOnce(['package.json']);
-      platform.getFile.mockReturnValueOnce('not json');
-      const res = await resolvePackageFiles(config);
-      expect(res.packageFiles).toMatchSnapshot();
-      expect(res.errors).toHaveLength(1);
-    });
-    it('detect package.json and throws error if cannot parse (onboarded)', async () => {
-      manager.detectPackageFiles.mockReturnValueOnce([
-        { packageFile: 'package.json', manager: 'npm' },
-      ]);
-      platform.getFileList.mockReturnValueOnce(['package.json']);
-      platform.getFile.mockReturnValueOnce('not json');
-      config.repoIsOnboarded = true;
-      let e;
-      try {
-        await resolvePackageFiles(config);
-      } catch (err) {
-        e = err;
-      }
-      expect(e).toBeDefined();
-      expect(e).toMatchSnapshot();
-    });
-    it('clears npmrc and yarnrc fields', async () => {
-      manager.detectPackageFiles.mockReturnValueOnce([
-        { packageFile: 'package.json', manager: 'npm' },
-      ]);
-      const pJson = {
-        name: 'something',
-        version: '1.0.0',
-        renovate: {
-          automerge: true,
-        },
-      };
-      platform.getFile.mockReturnValueOnce(JSON.stringify(pJson));
-      platform.getFileList.mockReturnValueOnce(['package.json']);
-      platform.getFileList.mockReturnValueOnce(['package.json']);
-      const res = await resolvePackageFiles(config);
-      expect(res.packageFiles).toMatchSnapshot();
-      expect(res.warnings).toHaveLength(0);
-    });
-    it('detects accompanying files', async () => {
-      manager.detectPackageFiles.mockReturnValueOnce([
-        { packageFile: 'package.json', manager: 'npm' },
-      ]);
-      platform.getFileList.mockReturnValue([
-        'package.json',
-        'yarn.lock',
-        'package-lock.json',
-        'npm-shrinkwrap.json',
-        'shrinkwrap.yaml',
-      ]);
-      platform.getFile.mockReturnValueOnce(
-        '{"name": "package.json", "version": "0.0.1"}'
-      );
-      platform.getFile.mockReturnValueOnce('npmrc');
-      platform.getFile.mockReturnValueOnce('yarnrc');
-      const res = await resolvePackageFiles(config);
-      expect(res.packageFiles).toMatchSnapshot();
-      expect(res.warnings).toHaveLength(0);
-    });
-    it('resolves docker', async () => {
-      platform.getFileList.mockReturnValue(['Dockerfile']);
-      platform.getFile.mockReturnValue('# comment\nFROM node:8\n'); // Dockerfile
-      const res = await resolvePackageFiles(config);
-      expect(res.packageFiles).toMatchSnapshot();
-      expect(res.packageFiles).toHaveLength(1);
-      expect(res.warnings).toHaveLength(0);
-    });
-    it('resolves package files without own resolve', async () => {
-      platform.getFileList.mockReturnValue(['WORKSPACE']);
-      platform.getFile.mockReturnValue('git_repository(\n'); // WORKSPACE
-      const res = await resolvePackageFiles(config);
-      expect(res.packageFiles).toMatchSnapshot();
-      expect(res.packageFiles).toHaveLength(1);
-      expect(res.warnings).toHaveLength(0);
-    });
-    it('strips npmrc with NPM_TOKEN', async () => {
-      manager.detectPackageFiles.mockReturnValueOnce([
-        { packageFile: 'package.json', manager: 'npm' },
-      ]);
-      platform.getFileList.mockReturnValue(['package.json', '.npmrc']);
-      platform.getFile.mockReturnValueOnce(
-        '{"name": "package.json", "version": "0.0.1"}'
-      );
-      platform.getFile.mockReturnValueOnce(
-        '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' // eslint-disable-line
-      );
-      const res = await resolvePackageFiles(config);
-      expect(res.packageFiles).toMatchSnapshot();
-      expect(res.warnings).toHaveLength(0);
-    });
-    it('checks if renovate config in nested package.json throws an error', async () => {
-      manager.detectPackageFiles.mockReturnValueOnce([
-        { packageFile: 'package.json', manager: 'npm' },
-      ]);
-      platform.getFileList.mockReturnValue(['test/package.json']);
-      platform.getFile.mockReturnValueOnce(
-        '{"name": "test/package.json", "version": "0.0.1", "renovate":{"enabled": true}}'
-      );
-      let e;
-      try {
-        await resolvePackageFiles(config);
-      } catch (err) {
-        e = err;
-      }
-      expect(e).toEqual(new Error('config-validation'));
-    });
-  });
-});
diff --git a/test/manager/travis/__snapshots__/extract.spec.js.snap b/test/manager/travis/__snapshots__/extract.spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..d58cccbd596715372eda6a19e40df5d16f0eeb6f
--- /dev/null
+++ b/test/manager/travis/__snapshots__/extract.spec.js.snap
@@ -0,0 +1,15 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`lib/manager/travis/extract extractDependencies() returns results 1`] = `
+Object {
+  "deps": Array [
+    Object {
+      "currentVersion": Array [
+        6,
+        8,
+      ],
+      "depName": "node",
+    },
+  ],
+}
+`;
diff --git a/test/manager/travis/extract.spec.js b/test/manager/travis/extract.spec.js
index ca8038bac4c23b0d67d30df4af053f563165d7b0..3106bafa80f6bfbf6211c45a7b8329dfecdd3e75 100644
--- a/test/manager/travis/extract.spec.js
+++ b/test/manager/travis/extract.spec.js
@@ -10,5 +10,10 @@ describe('lib/manager/travis/extract', () => {
       const res = extractDependencies('blahhhhh:foo:@what\n', config);
       expect(res).toBe(null);
     });
+    it('returns results', () => {
+      const res = extractDependencies('node_js:\n  - 6\n  - 8\n', config);
+      expect(res).toMatchSnapshot();
+      expect(res.deps).toHaveLength(1);
+    });
   });
 });
diff --git a/test/util/package-rules.spec.js b/test/util/package-rules.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..de924f33b389a39f9296b85ecbc35888b3bd9c09
--- /dev/null
+++ b/test/util/package-rules.spec.js
@@ -0,0 +1,241 @@
+const { applyPackageRules } = require('../../lib/util/package-rules');
+
+describe('applyPackageRules()', () => {
+  const config1 = {
+    foo: 'bar',
+
+    packageRules: [
+      {
+        packageNames: ['a', 'b'],
+        x: 2,
+      },
+      {
+        packagePatterns: ['a', 'b'],
+        excludePackageNames: ['aa'],
+        excludePackagePatterns: ['d'],
+        y: 2,
+      },
+    ],
+  };
+  it('applies both rules for a', () => {
+    const dep = {
+      depName: 'a',
+    };
+    const res = applyPackageRules({ ...config1, ...dep });
+    expect(res.x).toBe(2);
+    expect(res.y).toBe(2);
+  });
+  it('applies both rules for b', () => {
+    const dep = {
+      depName: 'b',
+    };
+    const res = applyPackageRules({ ...config1, ...dep });
+    expect(res.x).toBe(2);
+    expect(res.y).toBe(2);
+  });
+  it('applies the second rule', () => {
+    const dep = {
+      depName: 'abc',
+    };
+    const res = applyPackageRules({ ...config1, ...dep });
+    expect(res.x).toBeUndefined();
+    expect(res.y).toBe(2);
+  });
+  it('applies the second second rule', () => {
+    const dep = {
+      depName: 'bc',
+    };
+    const res = applyPackageRules({ ...config1, ...dep });
+    expect(res.x).toBeUndefined();
+    expect(res.y).toBe(2);
+  });
+  it('excludes package name', () => {
+    const dep = {
+      depName: 'aa',
+    };
+    const res = applyPackageRules({ ...config1, ...dep });
+    expect(res.x).toBeUndefined();
+    expect(res.y).toBeUndefined();
+  });
+  it('excludes package pattern', () => {
+    const dep = {
+      depName: 'bcd',
+    };
+    const res = applyPackageRules({ ...config1, ...dep });
+    expect(res.x).toBeUndefined();
+    expect(res.y).toBeUndefined();
+  });
+  it('matches anything if missing inclusive rules', () => {
+    const config = {
+      packageRules: [
+        {
+          excludePackageNames: ['foo'],
+          x: 1,
+        },
+      ],
+    };
+    const res1 = applyPackageRules({
+      ...config,
+      depName: 'foo',
+    });
+    expect(res1.x).toBeUndefined();
+    const res2 = applyPackageRules({
+      ...config,
+      depName: 'bar',
+    });
+    expect(res2.x).toBeDefined();
+  });
+  it('supports inclusive or', () => {
+    const config = {
+      packageRules: [
+        {
+          packageNames: ['neutrino'],
+          packagePatterns: ['^@neutrino\\/'],
+          x: 1,
+        },
+      ],
+    };
+    const res1 = applyPackageRules({ ...config, depName: 'neutrino' });
+    expect(res1.x).toBeDefined();
+    const res2 = applyPackageRules({
+      ...config,
+      depName: '@neutrino/something',
+    });
+    expect(res2.x).toBeDefined();
+  });
+  it('filters depType', () => {
+    const config = {
+      packageRules: [
+        {
+          depTypeList: ['dependencies', 'peerDependencies'],
+          packageNames: ['a'],
+          x: 1,
+        },
+      ],
+    };
+    const dep = {
+      depType: 'dependencies',
+      depName: 'a',
+    };
+    const res = applyPackageRules({ ...config, ...dep });
+    expect(res.x).toBe(1);
+  });
+  it('filters naked depType', () => {
+    const config = {
+      packageRules: [
+        {
+          depTypeList: ['dependencies', 'peerDependencies'],
+          x: 1,
+        },
+      ],
+    };
+    const dep = {
+      depType: 'dependencies',
+      depName: 'a',
+    };
+    const res = applyPackageRules({ ...config, ...dep });
+    expect(res.x).toBe(1);
+  });
+  it('filters depType', () => {
+    const config = {
+      packageRules: [
+        {
+          depTypeList: ['dependencies', 'peerDependencies'],
+          packageNames: ['a'],
+          x: 1,
+        },
+      ],
+    };
+    const dep = {
+      depType: 'devDependencies',
+      depName: 'a',
+    };
+    const res = applyPackageRules({ ...config, ...dep });
+    expect(res.x).toBeUndefined();
+  });
+  it('checks if matchCurrentVersion selector is valid and satisfies the condition on range overlap', () => {
+    const config = {
+      packageRules: [
+        {
+          packageNames: ['test'],
+          matchCurrentVersion: '<= 2.0.0',
+          x: 1,
+        },
+      ],
+    };
+    const res1 = applyPackageRules({
+      ...config,
+      ...{
+        depName: 'test',
+        currentVersion: '^1.0.0',
+      },
+    });
+    expect(res1.x).toBeDefined();
+  });
+  it('checks if matchCurrentVersion selector is valid and satisfies the condition on pinned to range overlap', () => {
+    const config = {
+      packageRules: [
+        {
+          packageNames: ['test'],
+          matchCurrentVersion: '>= 2.0.0',
+          x: 1,
+        },
+      ],
+    };
+    const res1 = applyPackageRules({
+      ...config,
+      ...{
+        depName: 'test',
+        currentVersion: '2.4.6',
+      },
+    });
+    expect(res1.x).toBeDefined();
+  });
+  it('checks if matchCurrentVersion selector works with static values', () => {
+    const config = {
+      packageRules: [
+        {
+          packageNames: ['test'],
+          matchCurrentVersion: '4.6.0',
+          x: 1,
+        },
+      ],
+    };
+    const res1 = applyPackageRules({
+      ...config,
+      ...{
+        depName: 'test',
+        currentVersion: '4.6.0',
+      },
+    });
+    expect(res1.x).toBeDefined();
+  });
+  it('matches paths', () => {
+    const config = {
+      packageFile: 'examples/foo/package.json',
+      packageRules: [
+        {
+          paths: ['examples/**', 'lib/'],
+          x: 1,
+        },
+      ],
+    };
+    const res1 = applyPackageRules({
+      ...config,
+      depName: 'test',
+    });
+    expect(res1.x).toBeDefined();
+    config.packageFile = 'package.json';
+    const res2 = applyPackageRules({
+      ...config,
+      depName: 'test',
+    });
+    expect(res2.x).toBeUndefined();
+    config.packageFile = 'lib/a/package.json';
+    const res3 = applyPackageRules({
+      ...config,
+      depName: 'test',
+    });
+    expect(res3.x).toBeDefined();
+  });
+});
diff --git a/test/workers/branch/__snapshots__/get-updated.spec.js.snap b/test/workers/branch/__snapshots__/get-updated.spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..6cb392e1064cffd86f9ff85e1e145d4212bf910c
--- /dev/null
+++ b/test/workers/branch/__snapshots__/get-updated.spec.js.snap
@@ -0,0 +1,20 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`workers/branch/get-updated getUpdatedPackageFiles() handles content change 1`] = `
+Object {
+  "parentBranch": undefined,
+  "updatedPackageFiles": Array [
+    Object {
+      "contents": "some new content",
+      "name": "undefined",
+    },
+  ],
+}
+`;
+
+exports[`workers/branch/get-updated getUpdatedPackageFiles() handles empty 1`] = `
+Object {
+  "parentBranch": undefined,
+  "updatedPackageFiles": Array [],
+}
+`;
diff --git a/test/workers/branch/__snapshots__/lock-files.spec.js.snap b/test/workers/branch/__snapshots__/lock-files.spec.js.snap
deleted file mode 100644
index 27bb0177b41c8930c3622436ef21be205c854051..0000000000000000000000000000000000000000
--- a/test/workers/branch/__snapshots__/lock-files.spec.js.snap
+++ /dev/null
@@ -1,107 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`workers/branch/lock-files determineLockFileDirs returns all directories if lock file maintenance 1`] = `
-Object {
-  "npmShrinkwrapDirs": Array [
-    "leftend",
-  ],
-  "packageLockFileDirs": Array [
-    "backend",
-  ],
-  "shrinkwrapYamlDirs": Array [
-    "frontend",
-  ],
-  "yarnLockFileDirs": Array [
-    ".",
-  ],
-}
-`;
-
-exports[`workers/branch/lock-files determineLockFileDirs returns directories from updated package files 1`] = `
-Object {
-  "lernaDirs": Array [],
-  "npmShrinkwrapDirs": Array [
-    "leftend",
-  ],
-  "packageLockFileDirs": Array [
-    "backend",
-  ],
-  "shrinkwrapYamlDirs": Array [
-    "frontend",
-  ],
-  "yarnLockFileDirs": Array [
-    ".",
-  ],
-}
-`;
-
-exports[`workers/branch/lock-files determineLockFileDirs returns root directory if using lerna package lock 1`] = `
-Object {
-  "lernaDirs": Array [
-    ".",
-  ],
-  "npmShrinkwrapDirs": Array [],
-  "packageLockFileDirs": Array [],
-  "shrinkwrapYamlDirs": Array [],
-  "yarnLockFileDirs": Array [],
-}
-`;
-
-exports[`workers/branch/lock-files determineLockFileDirs returns root directory if using yarn workspaces 1`] = `
-Object {
-  "lernaDirs": Array [],
-  "npmShrinkwrapDirs": Array [],
-  "packageLockFileDirs": Array [],
-  "shrinkwrapYamlDirs": Array [],
-  "yarnLockFileDirs": Array [
-    ".",
-  ],
-}
-`;
-
-exports[`workers/branch/lock-files getUpdatedLockFiles returns no error and empty lockfiles if lock file maintenance exists 1`] = `
-Object {
-  "lockFileErrors": Array [],
-  "updatedLockFiles": Array [],
-}
-`;
-
-exports[`workers/branch/lock-files getUpdatedLockFiles returns no error and empty lockfiles if none updated 1`] = `
-Object {
-  "lockFileErrors": Array [],
-  "updatedLockFiles": Array [],
-}
-`;
-
-exports[`workers/branch/lock-files getUpdatedLockFiles returns no error and empty lockfiles if updateLockFiles false 1`] = `
-Object {
-  "lockFileErrors": Array [],
-  "updatedLockFiles": Array [],
-}
-`;
-
-exports[`workers/branch/lock-files getUpdatedLockFiles tries lerna npm 1`] = `
-Object {
-  "lockFileErrors": Array [],
-  "updatedLockFiles": Array [],
-}
-`;
-
-exports[`workers/branch/lock-files getUpdatedLockFiles tries lerna yarn 1`] = `
-Object {
-  "lockFileErrors": Array [
-    Object {
-      "lockFile": "yarn.lock",
-      "stderr": undefined,
-    },
-  ],
-  "updatedLockFiles": Array [],
-}
-`;
-
-exports[`workers/branch/lock-files getUpdatedLockFiles tries multiple lock files 1`] = `
-Object {
-  "lockFileErrors": Array [],
-  "updatedLockFiles": Array [],
-}
-`;
diff --git a/test/workers/branch/get-updated.spec.js b/test/workers/branch/get-updated.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..744ea0f1d5905e4b4fce335a483b3ae94713ec24
--- /dev/null
+++ b/test/workers/branch/get-updated.spec.js
@@ -0,0 +1,44 @@
+const npm = require('../../../lib/manager/npm');
+const {
+  getUpdatedPackageFiles,
+} = require('../../../lib/workers/branch/get-updated');
+const defaultConfig = require('../../../lib/config/defaults').getConfig();
+
+describe('workers/branch/get-updated', () => {
+  describe('getUpdatedPackageFiles()', () => {
+    let config;
+    beforeEach(() => {
+      config = {
+        ...defaultConfig,
+        upgrades: [],
+      };
+      npm.updateDependency = jest.fn();
+    });
+    it('handles empty', async () => {
+      const res = await getUpdatedPackageFiles(config);
+      expect(res).toMatchSnapshot();
+    });
+    it('handles null content', async () => {
+      config.parentBranch = 'some-branch';
+      config.upgrades.push({
+        manager: 'npm',
+      });
+      let e;
+      try {
+        await getUpdatedPackageFiles(config);
+      } catch (err) {
+        e = err;
+      }
+      expect(e).toBeDefined();
+    });
+    it('handles content change', async () => {
+      config.parentBranch = 'some-branch';
+      config.upgrades.push({
+        manager: 'npm',
+      });
+      npm.updateDependency.mockReturnValue('some new content');
+      const res = await getUpdatedPackageFiles(config);
+      expect(res).toMatchSnapshot();
+    });
+  });
+});
diff --git a/test/workers/branch/index.spec.js b/test/workers/branch/index.spec.js
index 2826715f4ad352db46069043fecc09a561f4273d..76ee38eed288a96fe93781e1a2c21126bf0c25a8 100644
--- a/test/workers/branch/index.spec.js
+++ b/test/workers/branch/index.spec.js
@@ -4,18 +4,18 @@ const defaultConfig = require('../../../lib/config/defaults').getConfig();
 const schedule = require('../../../lib/workers/branch/schedule');
 const checkExisting = require('../../../lib/workers/branch/check-existing');
 const parent = require('../../../lib/workers/branch/parent');
-const manager = require('../../../lib/manager');
-const lockFiles = require('../../../lib/workers/branch/lock-files');
+const npmPostExtract = require('../../../lib/manager/npm/post-update');
 const commit = require('../../../lib/workers/branch/commit');
 const statusChecks = require('../../../lib/workers/branch/status-checks');
 const automerge = require('../../../lib/workers/branch/automerge');
 const prWorker = require('../../../lib/workers/pr');
+const getUpdated = require('../../../lib/workers/branch/get-updated');
 
-jest.mock('../../../lib/manager');
+jest.mock('../../../lib/workers/branch/get-updated');
 jest.mock('../../../lib/workers/branch/schedule');
 jest.mock('../../../lib/workers/branch/check-existing');
 jest.mock('../../../lib/workers/branch/parent');
-jest.mock('../../../lib/workers/branch/lock-files');
+jest.mock('../../../lib/manager/npm/post-update');
 jest.mock('../../../lib/workers/branch/status-checks');
 jest.mock('../../../lib/workers/branch/automerge');
 jest.mock('../../../lib/workers/pr');
@@ -126,24 +126,24 @@ describe('workers/branch', () => {
       expect(res).not.toEqual('pr-edited');
     });
     it('returns if pr creation limit exceeded', async () => {
-      manager.getUpdatedPackageFiles.mockReturnValueOnce({
+      getUpdated.getUpdatedPackageFiles.mockReturnValueOnce({
         updatedPackageFiles: [],
       });
-      lockFiles.getUpdatedLockFiles.mockReturnValueOnce({
+      npmPostExtract.getAdditionalFiles.mockReturnValueOnce({
         lockFileError: false,
         updatedLockFiles: [],
       });
-      platform.branchExists.mockReturnValueOnce(false);
+      platform.branchExists.mockReturnValue(false);
       config.prHourlyLimitReached = true;
       expect(await branchWorker.processBranch(config)).toEqual(
         'pr-hourly-limit-reached'
       );
     });
     it('returns if no work', async () => {
-      manager.getUpdatedPackageFiles.mockReturnValueOnce({
+      getUpdated.getUpdatedPackageFiles.mockReturnValueOnce({
         updatedPackageFiles: [],
       });
-      lockFiles.getUpdatedLockFiles.mockReturnValueOnce({
+      npmPostExtract.getAdditionalFiles.mockReturnValueOnce({
         lockFileError: false,
         updatedLockFiles: [],
       });
@@ -151,10 +151,10 @@ describe('workers/branch', () => {
       expect(await branchWorker.processBranch(config)).toEqual('no-work');
     });
     it('returns if branch automerged', async () => {
-      manager.getUpdatedPackageFiles.mockReturnValueOnce({
+      getUpdated.getUpdatedPackageFiles.mockReturnValueOnce({
         updatedPackageFiles: [{}],
       });
-      lockFiles.getUpdatedLockFiles.mockReturnValueOnce({
+      npmPostExtract.getAdditionalFiles.mockReturnValueOnce({
         lockFileError: false,
         updatedLockFiles: [{}],
       });
@@ -166,10 +166,10 @@ describe('workers/branch', () => {
       expect(prWorker.ensurePr.mock.calls).toHaveLength(0);
     });
     it('ensures PR and tries automerge', async () => {
-      manager.getUpdatedPackageFiles.mockReturnValueOnce({
+      getUpdated.getUpdatedPackageFiles.mockReturnValueOnce({
         updatedPackageFiles: [{}],
       });
-      lockFiles.getUpdatedLockFiles.mockReturnValueOnce({
+      npmPostExtract.getAdditionalFiles.mockReturnValueOnce({
         lockFileError: false,
         updatedLockFiles: [{}],
       });
@@ -183,10 +183,10 @@ describe('workers/branch', () => {
       expect(prWorker.checkAutoMerge.mock.calls).toHaveLength(1);
     });
     it('ensures PR and adds lock file error comment', async () => {
-      manager.getUpdatedPackageFiles.mockReturnValueOnce({
+      getUpdated.getUpdatedPackageFiles.mockReturnValueOnce({
         updatedPackageFiles: [{}],
       });
-      lockFiles.getUpdatedLockFiles.mockReturnValueOnce({
+      npmPostExtract.getAdditionalFiles.mockReturnValueOnce({
         lockFileError: false,
         updatedLockFiles: [{}],
       });
@@ -202,10 +202,10 @@ describe('workers/branch', () => {
       expect(prWorker.checkAutoMerge.mock.calls).toHaveLength(0);
     });
     it('ensures PR and adds lock file error comment recreate closed', async () => {
-      manager.getUpdatedPackageFiles.mockReturnValueOnce({
+      getUpdated.getUpdatedPackageFiles.mockReturnValueOnce({
         updatedPackageFiles: [{}],
       });
-      lockFiles.getUpdatedLockFiles.mockReturnValueOnce({
+      npmPostExtract.getAdditionalFiles.mockReturnValueOnce({
         lockFileError: false,
         updatedLockFiles: [{}],
       });
@@ -222,26 +222,26 @@ describe('workers/branch', () => {
       expect(prWorker.checkAutoMerge.mock.calls).toHaveLength(0);
     });
     it('swallows branch errors', async () => {
-      manager.getUpdatedPackageFiles.mockImplementationOnce(() => {
+      getUpdated.getUpdatedPackageFiles.mockImplementationOnce(() => {
         throw new Error('some error');
       });
       await branchWorker.processBranch(config);
     });
     it('throws and swallows branch errors', async () => {
-      manager.getUpdatedPackageFiles.mockReturnValueOnce({
+      getUpdated.getUpdatedPackageFiles.mockReturnValueOnce({
         updatedPackageFiles: [{}],
       });
-      lockFiles.getUpdatedLockFiles.mockReturnValueOnce({
+      npmPostExtract.getAdditionalFiles.mockReturnValueOnce({
         lockFileError: true,
         updatedLockFiles: [{}],
       });
       await branchWorker.processBranch(config);
     });
     it('swallows pr errors', async () => {
-      manager.getUpdatedPackageFiles.mockReturnValueOnce({
+      getUpdated.getUpdatedPackageFiles.mockReturnValueOnce({
         updatedPackageFiles: [{}],
       });
-      lockFiles.getUpdatedLockFiles.mockReturnValueOnce({
+      npmPostExtract.getAdditionalFiles.mockReturnValueOnce({
         lockFileError: false,
         updatedLockFiles: [{}],
       });
diff --git a/test/workers/branch/lock-files/__snapshots__/index.spec.js.snap b/test/workers/branch/lock-files/__snapshots__/index.spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..8cc701bba7684051cc3202c6dbb318c10bc5044b
--- /dev/null
+++ b/test/workers/branch/lock-files/__snapshots__/index.spec.js.snap
@@ -0,0 +1,29 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`manager/npm/post-update getAdditionalFiles returns no error and empty lockfiles if lock file maintenance exists 1`] = `
+Object {
+  "lockFileErrors": Array [],
+  "updatedLockFiles": Array [],
+}
+`;
+
+exports[`manager/npm/post-update getAdditionalFiles returns no error and empty lockfiles if updateLockFiles false 1`] = `
+Object {
+  "lockFileErrors": Array [],
+  "updatedLockFiles": Array [],
+}
+`;
+
+exports[`manager/npm/post-update getAdditionalFiles returns no error and empty lockfiles if lock file maintenance exists 1`] = `
+Object {
+  "lockFileErrors": Array [],
+  "updatedLockFiles": Array [],
+}
+`;
+
+exports[`manager/npm/post-update getAdditionalFiles returns no error and empty lockfiles if updateLockFiles false 1`] = `
+Object {
+  "lockFileErrors": Array [],
+  "updatedLockFiles": Array [],
+}
+`;
diff --git a/test/workers/branch/lock-files.spec.js b/test/workers/branch/lock-files/index.spec.js
similarity index 55%
rename from test/workers/branch/lock-files.spec.js
rename to test/workers/branch/lock-files/index.spec.js
index 6682a2277186e631d25819f561d5f3f4c07df997..cc3622e87fc00d71cf7d122e3f2be046c9bc9975 100644
--- a/test/workers/branch/lock-files.spec.js
+++ b/test/workers/branch/lock-files/index.spec.js
@@ -1,241 +1,51 @@
 const fs = require('fs-extra');
-const lockFiles = require('../../../lib/workers/branch/lock-files');
-const defaultConfig = require('../../../lib/config/defaults').getConfig();
-const upath = require('upath');
+const lockFiles = require('../../../../lib/manager/npm/post-update');
+const defaultConfig = require('../../../../lib/config/defaults').getConfig();
+// const upath = require('upath');
 
-const npm = require('../../../lib/workers/branch/npm');
-const yarn = require('../../../lib/workers/branch/yarn');
-const pnpm = require('../../../lib/workers/branch/pnpm');
-const lerna = require('../../../lib/workers/branch/lerna');
+const npm = require('../../../../lib/manager/npm/post-update/npm');
+const yarn = require('../../../../lib/manager/npm/post-update/yarn');
+const pnpm = require('../../../../lib/manager/npm/post-update/pnpm');
+const lerna = require('../../../../lib/manager/npm/post-update/lerna');
 
 const {
-  hasPackageLock,
-  hasNpmShrinkwrap,
-  hasYarnLock,
-  hasShrinkwrapYaml,
-  determineLockFileDirs,
-  writeExistingFiles,
+  // determineLockFileDirs,
+  // writeExistingFiles,
   writeUpdatedPackageFiles,
-  getUpdatedLockFiles,
+  getAdditionalFiles,
 } = lockFiles;
 
-describe('workers/branch/lock-files', () => {
-  describe('hasPackageLock', () => {
+describe('manager/npm/post-update', () => {
+  /*
+  describe('determineLockFileDirs', () => {
     let config;
+    let packageFiles;
     beforeEach(() => {
       config = {
         ...defaultConfig,
       };
-    });
-    it('returns true if found and true', () => {
-      config.packageFiles = [
-        {
-          packageFile: 'package.json',
-          packageLock: 'some package lock',
-        },
-      ];
-      expect(hasPackageLock(config, 'package.json')).toBe(true);
-    });
-    it('returns false if found and false', () => {
-      config.packageFiles = [
+      packageFiles = [
         {
           packageFile: 'package.json',
-          packageLock: 'some package lock',
+          yarnLock: '# some yarn lock',
         },
         {
           packageFile: 'backend/package.json',
-        },
-      ];
-      expect(hasPackageLock(config, 'backend/package.json')).toBe(false);
-    });
-    it('throws error if not found', () => {
-      config.packageFiles = [
-        {
-          packageFile: 'package.json',
           packageLock: 'some package lock',
         },
         {
-          packageFile: 'backend/package.json',
+          packageFile: 'frontend/package.json',
+          pnpmShrinkwrap: 'some package lock',
         },
-      ];
-      let e;
-      try {
-        hasPackageLock(config, 'frontend/package.json');
-      } catch (err) {
-        e = err;
-      }
-      expect(e).toBeDefined();
-    });
-  });
-  describe('hasNpmShrinkWrap', () => {
-    let config;
-    beforeEach(() => {
-      config = {
-        ...defaultConfig,
-      };
-    });
-    it('returns true if found and true', () => {
-      config.packageFiles = [
-        {
-          packageFile: 'package.json',
-          npmShrinkwrap: 'some package lock',
-        },
-      ];
-      expect(hasNpmShrinkwrap(config, 'package.json')).toBe(true);
-    });
-    it('returns false if found and false', () => {
-      config.packageFiles = [
         {
-          packageFile: 'package.json',
-          npmShrinkwrap: 'some package lock',
-        },
-        {
-          packageFile: 'backend/package.json',
-        },
-      ];
-      expect(hasNpmShrinkwrap(config, 'backend/package.json')).toBe(false);
-    });
-    it('throws error if not found', () => {
-      config.packageFiles = [
-        {
-          packageFile: 'package.json',
+          packageFile: 'leftend/package.json',
           npmShrinkwrap: 'some package lock',
         },
-        {
-          packageFile: 'backend/package.json',
-        },
-      ];
-      let e;
-      try {
-        hasNpmShrinkwrap(config, 'frontend/package.json');
-      } catch (err) {
-        e = err;
-      }
-      expect(e).toBeDefined();
-    });
-  });
-  describe('hasYarnLock', () => {
-    let config;
-    beforeEach(() => {
-      config = {
-        ...defaultConfig,
-      };
-    });
-    it('returns true if found and true', () => {
-      config.packageFiles = [
-        {
-          packageFile: 'package.json',
-          yarnLock: '# some yarn lock',
-        },
-      ];
-      expect(hasYarnLock(config, 'package.json')).toBe(true);
-    });
-    it('returns false if found and false', () => {
-      config.packageFiles = [
-        {
-          packageFile: 'package.json',
-          yarnLock: '# some yarn lock',
-        },
-        {
-          packageFile: 'backend/package.json',
-        },
-      ];
-      expect(hasYarnLock(config, 'backend/package.json')).toBe(false);
-    });
-    it('throws error if not found', () => {
-      config.packageFiles = [
-        {
-          packageFile: 'package.json',
-          yarnLock: '# some yarn lock',
-        },
-        {
-          packageFile: 'backend/package.json',
-        },
-      ];
-      let e;
-      try {
-        hasYarnLock(config, 'frontend/package.json');
-      } catch (err) {
-        e = err;
-      }
-      expect(e).toBeDefined();
-    });
-  });
-  describe('hasShrinkWrapYaml', () => {
-    let config;
-    beforeEach(() => {
-      config = {
-        ...defaultConfig,
-      };
-    });
-    it('returns true if found and true', () => {
-      config.packageFiles = [
-        {
-          packageFile: 'package.json',
-          shrinkwrapYaml: 'some shrinkwrap',
-        },
-      ];
-      expect(hasShrinkwrapYaml(config, 'package.json')).toBe(true);
-    });
-    it('returns false if found and false', () => {
-      config.packageFiles = [
-        {
-          packageFile: 'package.json',
-          shrinkwrapYaml: 'some shrinkwrap',
-        },
-        {
-          packageFile: 'backend/package.json',
-        },
-      ];
-      expect(hasShrinkwrapYaml(config, 'backend/package.json')).toBe(false);
-    });
-    it('throws error if not found', () => {
-      config.packageFiles = [
-        {
-          packageFile: 'package.json',
-          shrinkwrapYaml: 'some package lock',
-        },
-        {
-          packageFile: 'backend/package.json',
-        },
       ];
-      let e;
-      try {
-        hasShrinkwrapYaml(config, 'frontend/package.json');
-      } catch (err) {
-        e = err;
-      }
-      expect(e).toBeDefined();
-    });
-  });
-  describe('determineLockFileDirs', () => {
-    let config;
-    beforeEach(() => {
-      config = {
-        ...defaultConfig,
-        packageFiles: [
-          {
-            packageFile: 'package.json',
-            yarnLock: '# some yarn lock',
-          },
-          {
-            packageFile: 'backend/package.json',
-            packageLock: 'some package lock',
-          },
-          {
-            packageFile: 'frontend/package.json',
-            shrinkwrapYaml: 'some package lock',
-          },
-          {
-            packageFile: 'leftend/package.json',
-            npmShrinkwrap: 'some package lock',
-          },
-        ],
-      };
     });
     it('returns all directories if lock file maintenance', () => {
       config.upgrades = [{ type: 'lockFileMaintenance' }];
-      const res = determineLockFileDirs(config);
+      const res = determineLockFileDirs(config, packageFiles);
       expect(res).toMatchSnapshot();
     });
     it('returns directories from updated package files', () => {
@@ -258,7 +68,7 @@ describe('workers/branch/lock-files', () => {
           contents: 'some contents',
         },
       ];
-      const res = determineLockFileDirs(config);
+      const res = determineLockFileDirs(config, packageFiles);
       expect(res).toMatchSnapshot();
     });
     it('returns root directory if using yarn workspaces', () => {
@@ -282,9 +92,9 @@ describe('workers/branch/lock-files', () => {
       ];
       const res = determineLockFileDirs(config);
       expect(res).toMatchSnapshot();
-      expect(res.packageLockFileDirs).toHaveLength(0);
-      expect(res.yarnLockFileDirs).toHaveLength(1);
-      expect(res.yarnLockFileDirs[0]).toEqual('.');
+      expect(res.npmLockDirs).toHaveLength(0);
+      expect(res.yarnLockDirs).toHaveLength(1);
+      expect(res.yarnLockDirs[0]).toEqual('.');
     });
     it('returns root directory if using lerna package lock', () => {
       config.lernaLockFile = 'yarn';
@@ -307,8 +117,8 @@ describe('workers/branch/lock-files', () => {
       ];
       const res = determineLockFileDirs(config);
       expect(res).toMatchSnapshot();
-      expect(res.packageLockFileDirs).toHaveLength(0);
-      expect(res.yarnLockFileDirs).toHaveLength(0);
+      expect(res.npmLockDirs).toHaveLength(0);
+      expect(res.yarnLockDirs).toHaveLength(0);
       expect(res.lernaDirs).toHaveLength(1);
       expect(res.lernaDirs[0]).toEqual('.');
     });
@@ -326,31 +136,32 @@ describe('workers/branch/lock-files', () => {
     it('returns if no packageFiles', async () => {
       config.npmrc = 'some-npmrc';
       config.yarnrc = 'some-yarnrc';
-      delete config.packageFiles;
-      await writeExistingFiles(config);
+      await writeExistingFiles(config, {});
       expect(fs.outputFile.mock.calls).toHaveLength(2);
     });
     it('writes files and removes files', async () => {
       config.npmrc = 'some-npmrc';
-      config.packageFiles = [
-        {
-          packageFile: 'package.json',
-          content: { name: 'package 1' },
-          npmrc: 'some npmrc',
-        },
-        {
-          packageFile: 'backend/package.json',
-          hasPackageLock: true,
-          content: { name: 'package-2', engines: { yarn: '^0.27.5' } },
-          yarnrc: 'some yarnrc',
-        },
-        {
-          packageFile: 'leftend/package.json',
-          hasNpmShrinkwrap: true,
-          content: { name: 'package-3' },
-        },
-      ];
-      await writeExistingFiles(config);
+      const packageFiles = {
+        npm: [
+          {
+            packageFile: 'package.json',
+            content: { name: 'package 1' },
+            npmrc: 'some npmrc',
+          },
+          {
+            packageFile: 'backend/package.json',
+            hasPackageLock: true,
+            content: { name: 'package-2', engines: { yarn: '^0.27.5' } },
+            yarnrc: 'some yarnrc',
+          },
+          {
+            packageFile: 'leftend/package.json',
+            hasNpmShrinkwrap: true,
+            content: { name: 'package-3' },
+          },
+        ],
+      };
+      await writeExistingFiles(config, packageFiles);
       expect(fs.outputFile.mock.calls).toHaveLength(7);
       expect(fs.remove.mock.calls).toHaveLength(9);
     });
@@ -358,22 +169,24 @@ describe('workers/branch/lock-files', () => {
       const renoPath = upath.join(__dirname, '../../../');
       config.copyLocalLibs = true;
       config.tmpDir = { path: renoPath };
-      config.packageFiles = [
-        {
-          packageFile: 'client/package.json',
-          content: {
-            name: 'package 1',
-            dependencies: {
-              test: 'file:../test.tgz',
-              testFolder: 'file:../test',
+      const packageFiles = {
+        npm: [
+          {
+            packageFile: 'client/package.json',
+            content: {
+              name: 'package 1',
+              dependencies: {
+                test: 'file:../test.tgz',
+                testFolder: 'file:../test',
+              },
             },
+            yarnLock: 'some yarn lock',
+            packageLock: 'some package lock',
           },
-          yarnLock: 'some yarn lock',
-          packageLock: 'some package lock',
-        },
-      ];
+        ],
+      };
       platform.getFile.mockReturnValue('some lock file contents');
-      await writeExistingFiles(config);
+      await writeExistingFiles(config, packageFiles);
       expect(fs.outputFile.mock.calls).toHaveLength(5);
       expect(fs.remove.mock.calls).toHaveLength(1);
     });
@@ -381,22 +194,24 @@ describe('workers/branch/lock-files', () => {
       const renoPath = upath.join(__dirname, '../../../');
       config.copyLocalLibs = true;
       config.tmpDir = { path: renoPath };
-      config.packageFiles = [
-        {
-          packageFile: 'client/package.json',
-          content: {
-            name: 'package 1',
-            dependencies: {
-              test: 'file:../test.tgz',
-              testFolder: 'file:../test',
+      const packageFiles = {
+        npm: [
+          {
+            packageFile: 'client/package.json',
+            content: {
+              name: 'package 1',
+              dependencies: {
+                test: 'file:../test.tgz',
+                testFolder: 'file:../test',
+              },
             },
+            yarnLock: 'some yarn lock',
+            packageLock: 'some package lock',
           },
-          yarnLock: 'some yarn lock',
-          packageLock: 'some package lock',
-        },
-      ];
+        ],
+      };
       platform.getFile.mockReturnValue(null);
-      await writeExistingFiles(config);
+      await writeExistingFiles(config, packageFiles);
       expect(fs.outputFile.mock.calls).toHaveLength(3);
       expect(fs.remove.mock.calls).toHaveLength(1);
     });
@@ -404,26 +219,29 @@ describe('workers/branch/lock-files', () => {
       const renoPath = upath.join(__dirname, '../../../');
       config.copyLocalLibs = true;
       config.tmpDir = { path: renoPath };
-      config.packageFiles = [
-        {
-          packageFile: 'client/package.json',
-          content: {
-            name: 'package 1',
-            dependencies: {
-              test: 'file:../test.tgz',
-              testFolder: 'file:../../../../test',
+      const packageFiles = {
+        npm: [
+          {
+            packageFile: 'client/package.json',
+            content: {
+              name: 'package 1',
+              dependencies: {
+                test: 'file:../test.tgz',
+                testFolder: 'file:../../../../test',
+              },
             },
+            yarnLock: 'some yarn lock',
+            packageLock: 'some package lock',
           },
-          yarnLock: 'some yarn lock',
-          packageLock: 'some package lock',
-        },
-      ];
+        ],
+      };
       platform.getFile.mockReturnValue(null);
-      await writeExistingFiles(config);
+      await writeExistingFiles(config, packageFiles);
       expect(fs.outputFile.mock.calls).toHaveLength(3);
       expect(fs.remove.mock.calls).toHaveLength(1);
     });
   });
+  */
   describe('writeUpdatedPackageFiles', () => {
     let config;
     beforeEach(() => {
@@ -465,7 +283,7 @@ describe('workers/branch/lock-files', () => {
       expect(fs.outputFile.mock.calls[1][1].includes('"engines"')).toBe(false);
     });
   });
-  describe('getUpdatedLockFiles', () => {
+  describe('getAdditionalFiles', () => {
     let config;
     beforeEach(() => {
       config = {
@@ -493,7 +311,7 @@ describe('workers/branch/lock-files', () => {
     });
     it('returns no error and empty lockfiles if updateLockFiles false', async () => {
       config.updateLockFiles = false;
-      const res = await getUpdatedLockFiles(config);
+      const res = await getAdditionalFiles(config);
       expect(res).toMatchSnapshot();
       expect(res.lockFileErrors).toHaveLength(0);
       expect(res.updatedLockFiles).toHaveLength(0);
@@ -502,33 +320,34 @@ describe('workers/branch/lock-files', () => {
       config.type = 'lockFileMaintenance';
       config.parentBranch = 'renovate/lock-file-maintenance';
       platform.branchExists.mockReturnValueOnce(true);
-      const res = await getUpdatedLockFiles(config);
+      const res = await getAdditionalFiles(config);
       expect(res).toMatchSnapshot();
       expect(res.lockFileErrors).toHaveLength(0);
       expect(res.updatedLockFiles).toHaveLength(0);
     });
+    /*
     it('returns no error and empty lockfiles if none updated', async () => {
       lockFiles.determineLockFileDirs.mockReturnValueOnce({
-        packageLockFileDirs: [],
+        npmLockDirs: [],
         npmShrinkwrapDirs: [],
-        yarnLockFileDirs: [],
-        shrinkwrapYamlDirs: [],
+        yarnLockDirs: [],
+        pnpmShrinkwrapDirs: [],
         lernaDirs: [],
       });
-      const res = await getUpdatedLockFiles(config);
+      const res = await getAdditionalFiles(config);
       expect(res).toMatchSnapshot();
       expect(res.lockFileErrors).toHaveLength(0);
       expect(res.updatedLockFiles).toHaveLength(0);
     });
     it('tries multiple lock files', async () => {
       lockFiles.determineLockFileDirs.mockReturnValueOnce({
-        packageLockFileDirs: ['a', 'b'],
+        npmLockDirs: ['a', 'b'],
         npmShrinkwrapDirs: ['f'],
-        yarnLockFileDirs: ['c', 'd'],
-        shrinkwrapYamlDirs: ['e'],
+        yarnLockDirs: ['c', 'd'],
+        pnpmShrinkwrapDirs: ['e'],
         lernaDirs: [],
       });
-      const res = await getUpdatedLockFiles(config);
+      const res = await getAdditionalFiles(config);
       expect(res).toMatchSnapshot();
       expect(res.lockFileErrors).toHaveLength(0);
       expect(res.updatedLockFiles).toHaveLength(0);
@@ -538,43 +357,43 @@ describe('workers/branch/lock-files', () => {
     });
     it('tries lerna npm', async () => {
       lockFiles.determineLockFileDirs.mockReturnValueOnce({
-        packageLockFileDirs: ['a', 'b'],
+        npmLockDirs: ['a', 'b'],
         npmShrinkwrapDirs: [],
-        yarnLockFileDirs: [],
-        shrinkwrapYamlDirs: [],
+        yarnLockDirs: [],
+        pnpmShrinkwrapDirs: [],
         lernaDirs: ['.'],
       });
       config.packageFiles = [];
       config.lernaLockFile = 'npm';
       lerna.generateLockFiles.mockReturnValueOnce({ error: false });
-      const res = await getUpdatedLockFiles(config);
+      const res = await getAdditionalFiles(config);
       expect(res).toMatchSnapshot();
     });
     it('tries lerna yarn', async () => {
       lockFiles.determineLockFileDirs.mockReturnValueOnce({
-        packageLockFileDirs: [],
+        npmLockDirs: [],
         npmShrinkwrapDirs: [],
-        yarnLockFileDirs: ['c', 'd'],
-        shrinkwrapYamlDirs: [],
+        yarnLockDirs: ['c', 'd'],
+        pnpmShrinkwrapDirs: [],
         lernaDirs: ['.'],
       });
       config.lernaLockFile = 'yarn';
       lerna.generateLockFiles.mockReturnValueOnce({ error: true });
-      const res = await getUpdatedLockFiles(config);
+      const res = await getAdditionalFiles(config);
       expect(res).toMatchSnapshot();
     });
     it('sets error if receiving null', async () => {
       lockFiles.determineLockFileDirs.mockReturnValueOnce({
-        packageLockFileDirs: ['a', 'b'],
+        npmLockDirs: ['a', 'b'],
         npmShrinkwrapDirs: ['f'],
-        yarnLockFileDirs: ['c', 'd'],
-        shrinkwrapYamlDirs: ['e'],
+        yarnLockDirs: ['c', 'd'],
+        pnpmShrinkwrapDirs: ['e'],
         lernaDirs: [],
       });
       npm.generateLockFile.mockReturnValueOnce({ error: true });
       yarn.generateLockFile.mockReturnValueOnce({ error: true });
       pnpm.generateLockFile.mockReturnValueOnce({ error: true });
-      const res = await getUpdatedLockFiles(config);
+      const res = await getAdditionalFiles(config);
       expect(res.lockFileErrors).toHaveLength(3);
       expect(res.updatedLockFiles).toHaveLength(0);
       expect(npm.generateLockFile.mock.calls).toHaveLength(3);
@@ -583,21 +402,22 @@ describe('workers/branch/lock-files', () => {
     });
     it('adds multiple lock files', async () => {
       lockFiles.determineLockFileDirs.mockReturnValueOnce({
-        packageLockFileDirs: ['a', 'b'],
+        npmLockDirs: ['a', 'b'],
         npmShrinkwrapDirs: ['f'],
-        yarnLockFileDirs: ['c', 'd'],
-        shrinkwrapYamlDirs: ['e'],
+        yarnLockDirs: ['c', 'd'],
+        pnpmShrinkwrapDirs: ['e'],
         lernaDirs: [],
       });
       npm.generateLockFile.mockReturnValueOnce('some new lock file contents');
       yarn.generateLockFile.mockReturnValueOnce('some new lock file contents');
       pnpm.generateLockFile.mockReturnValueOnce('some new lock file contents');
-      const res = await getUpdatedLockFiles(config);
+      const res = await getAdditionalFiles(config);
       expect(res.lockFileErrors).toHaveLength(0);
       expect(res.updatedLockFiles).toHaveLength(3);
       expect(npm.generateLockFile.mock.calls).toHaveLength(3);
       expect(yarn.generateLockFile.mock.calls).toHaveLength(2);
       expect(platform.getFile.mock.calls).toHaveLength(7);
     });
+    */
   });
 });
diff --git a/test/workers/branch/lerna.spec.js b/test/workers/branch/lock-files/lerna.spec.js
similarity index 92%
rename from test/workers/branch/lerna.spec.js
rename to test/workers/branch/lock-files/lerna.spec.js
index 154db8cf645977f444555d077fc3e4bf8b874af0..7c3cae78625d210a746450ab0fd669adc594dff5 100644
--- a/test/workers/branch/lerna.spec.js
+++ b/test/workers/branch/lock-files/lerna.spec.js
@@ -1,4 +1,4 @@
-const lernaHelper = require('../../../lib/workers/branch/lerna');
+const lernaHelper = require('../../../../lib/manager/npm/post-update/lerna');
 
 jest.mock('child-process-promise');
 
diff --git a/test/workers/branch/npm.spec.js b/test/workers/branch/lock-files/npm.spec.js
similarity index 97%
rename from test/workers/branch/npm.spec.js
rename to test/workers/branch/lock-files/npm.spec.js
index 496ab86dfdcc28a9f535aaf0e83f646d3fcffbc6..4cc4121b309ce54de08f4ccf43481159f7d60c1a 100644
--- a/test/workers/branch/npm.spec.js
+++ b/test/workers/branch/lock-files/npm.spec.js
@@ -1,4 +1,4 @@
-const npmHelper = require('../../../lib/workers/branch/npm');
+const npmHelper = require('../../../../lib/manager/npm/post-update/npm');
 
 const { getInstalledPath } = require('get-installed-path');
 
diff --git a/test/workers/branch/pnpm.spec.js b/test/workers/branch/lock-files/pnpm.spec.js
similarity index 97%
rename from test/workers/branch/pnpm.spec.js
rename to test/workers/branch/lock-files/pnpm.spec.js
index 9464873b6026d108f7998ad3f5595a44750b1cd8..3791c0ad17ba786613f2a34046676d87ed2dcaac 100644
--- a/test/workers/branch/pnpm.spec.js
+++ b/test/workers/branch/lock-files/pnpm.spec.js
@@ -1,4 +1,4 @@
-const pnpmHelper = require('../../../lib/workers/branch/pnpm');
+const pnpmHelper = require('../../../../lib/manager/npm/post-update/pnpm');
 
 const { getInstalledPath } = require('get-installed-path');
 
diff --git a/test/workers/branch/yarn.spec.js b/test/workers/branch/lock-files/yarn.spec.js
similarity index 97%
rename from test/workers/branch/yarn.spec.js
rename to test/workers/branch/lock-files/yarn.spec.js
index 70b26e9cc6a1e1f425d3f4d5c54d9865c0276922..2a25f2a5dba35f17e78efe10f9fe65a7c437b1ef 100644
--- a/test/workers/branch/yarn.spec.js
+++ b/test/workers/branch/lock-files/yarn.spec.js
@@ -1,4 +1,4 @@
-const yarnHelper = require('../../../lib/workers/branch/yarn');
+const yarnHelper = require('../../../../lib/manager/npm/post-update/yarn');
 
 const { getInstalledPath } = require('get-installed-path');
 
diff --git a/test/workers/package-file/dep-type.spec.js b/test/workers/package-file/dep-type.spec.js
deleted file mode 100644
index 0130124be01e4cc5884ad0d09f4ad62b0c661c90..0000000000000000000000000000000000000000
--- a/test/workers/package-file/dep-type.spec.js
+++ /dev/null
@@ -1,324 +0,0 @@
-const path = require('path');
-const fs = require('fs');
-const npmExtract = require('../../../lib/manager/npm/extract');
-const pkgWorker = require('../../../lib/workers/package-file/package');
-const depTypeWorker = require('../../../lib/workers/package-file/dep-type');
-
-jest.mock('../../../lib/manager/npm/extract');
-jest.mock('../../../lib/workers/package-file/package');
-
-pkgWorker.renovatePackage = jest.fn(() => ['a']);
-
-describe('lib/workers/package-file/dep-type', () => {
-  describe('renovateDepType(packageContent, config)', () => {
-    let config;
-    beforeEach(() => {
-      config = {
-        packageFile: 'package.json',
-        manager: 'npm',
-        ignoreDeps: ['a', 'b'],
-        monorepoPackages: ['e'],
-        workspaceDir: '.',
-      };
-    });
-    it('returns empty if config is disabled', async () => {
-      config.enabled = false;
-      const res = await depTypeWorker.renovateDepType({}, config);
-      expect(res).toMatchObject([]);
-    });
-    it('returns empty if no deps found', async () => {
-      npmExtract.extractDependencies.mockReturnValueOnce(null);
-      const res = await depTypeWorker.renovateDepType({}, config);
-      expect(res).toMatchObject([]);
-    });
-    it('returns empty if all deps are filtered', async () => {
-      npmExtract.extractDependencies.mockReturnValueOnce({
-        deps: [{ depName: 'a' }, { depName: 'b' }, { depName: 'e' }],
-      });
-      const res = await depTypeWorker.renovateDepType({}, config);
-      expect(res).toMatchObject([]);
-    });
-    it('returns combined upgrades if all deps are filtered', async () => {
-      npmExtract.extractDependencies.mockReturnValueOnce({
-        deps: [{ depName: 'a' }, { depName: 'c' }, { depName: 'd' }],
-      });
-      const res = await depTypeWorker.renovateDepType({}, config);
-      expect(res).toHaveLength(2);
-    });
-    it('returns upgrades for meteor', async () => {
-      config.manager = 'meteor';
-      const content = fs.readFileSync(
-        path.resolve('test/_fixtures/meteor/package-1.js'),
-        'utf8'
-      );
-      const res = await depTypeWorker.renovateDepType(content, config);
-      expect(res).toHaveLength(6);
-    });
-    it('returns upgrades for bazel', async () => {
-      config.manager = 'bazel';
-      const content = fs.readFileSync(
-        path.resolve('test/_fixtures/bazel/WORKSPACE1'),
-        'utf8'
-      );
-      const res = await depTypeWorker.renovateDepType(content, config);
-      expect(res).toHaveLength(4);
-    });
-    it('returns upgrades for travis', async () => {
-      config.manager = 'travis';
-      const content = fs.readFileSync(
-        path.resolve('test/_fixtures/node/travis.yml'),
-        'utf8'
-      );
-      const res = await depTypeWorker.renovateDepType(content, config);
-      expect(res).toHaveLength(1);
-    });
-    it('handles malformed meteor', async () => {
-      config.manager = 'meteor';
-      const content = 'blah';
-      const res = await depTypeWorker.renovateDepType(content, config);
-      expect(res).toHaveLength(0);
-    });
-    it('returns upgrades for docker', async () => {
-      config.manager = 'docker';
-      config.currentFrom = 'node';
-      const res = await depTypeWorker.renovateDepType(
-        '# a comment\nFROM something\n',
-        config
-      );
-      expect(res).toHaveLength(1);
-    });
-    it('ignores Dockerfiles with no FROM', async () => {
-      config.manager = 'docker';
-      config.currentFrom = 'node';
-      const res = await depTypeWorker.renovateDepType(
-        '# a comment\nRUN something\n',
-        config
-      );
-      expect(res).toHaveLength(0);
-    });
-  });
-  describe('getDepConfig(depTypeConfig, dep)', () => {
-    const depTypeConfig = {
-      foo: 'bar',
-
-      packageRules: [
-        {
-          packageNames: ['a', 'b'],
-          x: 2,
-        },
-        {
-          packagePatterns: ['a', 'b'],
-          excludePackageNames: ['aa'],
-          excludePackagePatterns: ['d'],
-          y: 2,
-        },
-      ],
-    };
-    it('matches anything if missing inclusive rules', () => {
-      const allConfig = {
-        packageRules: [
-          {
-            excludePackageNames: ['foo'],
-            x: 1,
-          },
-        ],
-      };
-      const res1 = depTypeWorker.getDepConfig(allConfig, {
-        depName: 'foo',
-      });
-      expect(res1.x).toBeUndefined();
-      const res2 = depTypeWorker.getDepConfig(allConfig, {
-        depName: 'bar',
-      });
-      expect(res2.x).toBeDefined();
-    });
-    it('supports inclusive or', () => {
-      const nConfig = {
-        packageRules: [
-          {
-            packageNames: ['neutrino'],
-            packagePatterns: ['^@neutrino\\/'],
-            x: 1,
-          },
-        ],
-      };
-      const res1 = depTypeWorker.getDepConfig(nConfig, { depName: 'neutrino' });
-      expect(res1.x).toBeDefined();
-      const res2 = depTypeWorker.getDepConfig(nConfig, {
-        depName: '@neutrino/something',
-      });
-      expect(res2.x).toBeDefined();
-    });
-    it('applies both rules for a', () => {
-      const dep = {
-        depName: 'a',
-      };
-      const res = depTypeWorker.getDepConfig(depTypeConfig, dep);
-      expect(res.x).toBe(2);
-      expect(res.y).toBe(2);
-    });
-    it('applies both rules for b', () => {
-      const dep = {
-        depName: 'b',
-      };
-      const res = depTypeWorker.getDepConfig(depTypeConfig, dep);
-      expect(res.x).toBe(2);
-      expect(res.y).toBe(2);
-    });
-    it('applies the second rule', () => {
-      const dep = {
-        depName: 'abc',
-      };
-      const res = depTypeWorker.getDepConfig(depTypeConfig, dep);
-      expect(res.x).toBeUndefined();
-      expect(res.y).toBe(2);
-    });
-    it('applies the second second rule', () => {
-      const dep = {
-        depName: 'bc',
-      };
-      const res = depTypeWorker.getDepConfig(depTypeConfig, dep);
-      expect(res.x).toBeUndefined();
-      expect(res.y).toBe(2);
-    });
-    it('excludes package name', () => {
-      const dep = {
-        depName: 'aa',
-      };
-      const res = depTypeWorker.getDepConfig(depTypeConfig, dep);
-      expect(res.x).toBeUndefined();
-      expect(res.y).toBeUndefined();
-    });
-    it('excludes package pattern', () => {
-      const dep = {
-        depName: 'bcd',
-      };
-      const res = depTypeWorker.getDepConfig(depTypeConfig, dep);
-      expect(res.x).toBeUndefined();
-      expect(res.y).toBeUndefined();
-    });
-    it('filters depType', () => {
-      const config = {
-        packageRules: [
-          {
-            depTypeList: ['dependencies', 'peerDependencies'],
-            packageNames: ['a'],
-            x: 1,
-          },
-        ],
-      };
-      const dep = {
-        depType: 'dependencies',
-        depName: 'a',
-      };
-      const res = depTypeWorker.getDepConfig(config, dep);
-      expect(res.x).toBe(1);
-    });
-    it('filters naked depType', () => {
-      const config = {
-        packageRules: [
-          {
-            depTypeList: ['dependencies', 'peerDependencies'],
-            x: 1,
-          },
-        ],
-      };
-      const dep = {
-        depType: 'dependencies',
-        depName: 'a',
-      };
-      const res = depTypeWorker.getDepConfig(config, dep);
-      expect(res.x).toBe(1);
-    });
-    it('filters depType', () => {
-      const config = {
-        packageRules: [
-          {
-            depTypeList: ['dependencies', 'peerDependencies'],
-            packageNames: ['a'],
-            x: 1,
-          },
-        ],
-      };
-      const dep = {
-        depType: 'devDependencies',
-        depName: 'a',
-      };
-      const res = depTypeWorker.getDepConfig(config, dep);
-      expect(res.x).toBeUndefined();
-    });
-    it('checks if matchCurrentVersion selector is valid and satisfies the condition on range overlap', () => {
-      const config = {
-        packageRules: [
-          {
-            packageNames: ['test'],
-            matchCurrentVersion: '<= 2.0.0',
-            x: 1,
-          },
-        ],
-      };
-      const res1 = depTypeWorker.getDepConfig(config, {
-        depName: 'test',
-        currentVersion: '^1.0.0',
-      });
-      expect(res1.x).toBeDefined();
-    });
-    it('checks if matchCurrentVersion selector is valid and satisfies the condition on pinned to range overlap', () => {
-      const config = {
-        packageRules: [
-          {
-            packageNames: ['test'],
-            matchCurrentVersion: '>= 2.0.0',
-            x: 1,
-          },
-        ],
-      };
-      const res1 = depTypeWorker.getDepConfig(config, {
-        depName: 'test',
-        currentVersion: '2.4.6',
-      });
-      expect(res1.x).toBeDefined();
-    });
-    it('checks if matchCurrentVersion selector works with static values', () => {
-      const config = {
-        packageRules: [
-          {
-            packageNames: ['test'],
-            matchCurrentVersion: '4.6.0',
-            x: 1,
-          },
-        ],
-      };
-      const res1 = depTypeWorker.getDepConfig(config, {
-        depName: 'test',
-        currentVersion: '4.6.0',
-      });
-      expect(res1.x).toBeDefined();
-    });
-    it('matches paths', () => {
-      const config = {
-        packageFile: 'examples/foo/package.json',
-        packageRules: [
-          {
-            paths: ['examples/**', 'lib/'],
-            x: 1,
-          },
-        ],
-      };
-      const res1 = depTypeWorker.getDepConfig(config, {
-        depName: 'test',
-      });
-      expect(res1.x).toBeDefined();
-      config.packageFile = 'package.json';
-      const res2 = depTypeWorker.getDepConfig(config, {
-        depName: 'test',
-      });
-      expect(res2.x).toBeUndefined();
-      config.packageFile = 'lib/a/package.json';
-      const res3 = depTypeWorker.getDepConfig(config, {
-        depName: 'test',
-      });
-      expect(res3.x).toBeDefined();
-    });
-  });
-});
diff --git a/test/workers/package-file/index.spec.js b/test/workers/package-file/index.spec.js
deleted file mode 100644
index 60408e0a820d89cb9b3b68d6b92f9895f324b7d3..0000000000000000000000000000000000000000
--- a/test/workers/package-file/index.spec.js
+++ /dev/null
@@ -1,187 +0,0 @@
-const packageFileWorker = require('../../../lib/workers/package-file');
-const depTypeWorker = require('../../../lib/workers/package-file/dep-type');
-const defaultConfig = require('../../../lib/config/defaults').getConfig();
-const yarnLock = require('@yarnpkg/lockfile');
-
-jest.mock('@yarnpkg/lockfile');
-
-jest.mock('../../../lib/workers/package-file/dep-type');
-jest.mock('../../../lib/workers/branch/schedule');
-
-describe('packageFileWorker', () => {
-  describe('renovatePackageFile(config)', () => {
-    let config;
-    beforeEach(() => {
-      config = {
-        ...defaultConfig,
-        packageFile: 'package.json',
-        manager: 'npm',
-        content: {},
-        repoIsOnboarded: true,
-        npmrc: '# nothing',
-      };
-      depTypeWorker.renovateDepType.mockReturnValue([]);
-    });
-    it('returns empty if disabled', async () => {
-      config.enabled = false;
-      const res = await packageFileWorker.renovatePackageFile(config);
-      expect(res).toEqual([]);
-    });
-    it('returns upgrades', async () => {
-      depTypeWorker.renovateDepType.mockReturnValueOnce([{}]);
-      depTypeWorker.renovateDepType.mockReturnValueOnce([{}, {}]);
-      depTypeWorker.renovateDepType.mockReturnValueOnce([]);
-      depTypeWorker.renovateDepType.mockReturnValueOnce([]);
-      const res = await packageFileWorker.renovatePackageFile(config);
-      expect(res).toHaveLength(3);
-    });
-    it('autodetects dependency pinning true if private', async () => {
-      config.pinVersions = null;
-      config.content.private = true;
-      const res = await packageFileWorker.renovatePackageFile(config);
-      expect(res).toHaveLength(0);
-    });
-    it('autodetects dependency pinning true if no main', async () => {
-      config.pinVersions = null;
-      const res = await packageFileWorker.renovatePackageFile(config);
-      expect(res).toHaveLength(0);
-    });
-    it('autodetects dependency pinning true', async () => {
-      config.pinVersions = null;
-      config.content.main = 'something';
-      const res = await packageFileWorker.renovatePackageFile(config);
-      expect(res).toHaveLength(0);
-    });
-    it('maintains lock files', async () => {
-      config.lockFileMaintenance.enabled = true;
-      config.yarnLock = '# some yarn lock';
-      const res = await packageFileWorker.renovatePackageFile(config);
-      expect(res).toHaveLength(1);
-    });
-    it('uses workspaces yarn.lock', async () => {
-      config.workspaceDir = '.';
-      platform.getFile.mockReturnValueOnce('# yarn lock');
-      await packageFileWorker.renovatePackageFile(config);
-    });
-    it('skips unparseable yarn.lock', async () => {
-      config.yarnLock = 'yarn.lock';
-      await packageFileWorker.renovatePackageFile(config);
-    });
-    it('skips unparseable yarn.lock', async () => {
-      config.yarnLock = 'yarn.lock';
-      yarnLock.parse.mockReturnValueOnce({ type: 'failure' });
-      await packageFileWorker.renovatePackageFile(config);
-    });
-    it('uses workspace yarn.lock', async () => {
-      config.workspaceDir = '.';
-      yarnLock.parse.mockReturnValueOnce({ type: 'success' });
-      await packageFileWorker.renovatePackageFile(config);
-    });
-    it('skips unparseable package-lock.json', async () => {
-      config.packageLock = 'package-lock.lock';
-      await packageFileWorker.renovatePackageFile(config);
-    });
-    it('parses package-lock.json', async () => {
-      config.packageLock = 'package-lock.json';
-      platform.getFile.mockReturnValueOnce('{}');
-      await packageFileWorker.renovatePackageFile(config);
-    });
-    it('skips unparseable npm-shrinkwrap.json', async () => {
-      config.npmShrinkwrap = 'npm-shrinkwrap.json';
-      await packageFileWorker.renovatePackageFile(config);
-    });
-    it('parses npm-shrinkwrap.json', async () => {
-      config.npmShrinkwrap = 'npm-shrinkwrap.json';
-      platform.getFile.mockReturnValueOnce('{}');
-      await packageFileWorker.renovatePackageFile(config);
-    });
-  });
-  describe('renovateMeteorPackageFile(config)', () => {
-    let config;
-    beforeEach(() => {
-      config = {
-        ...defaultConfig,
-        packageFile: 'package.js',
-        manager: 'meteor',
-        repoIsOnboarded: true,
-      };
-      depTypeWorker.renovateDepType.mockReturnValue([]);
-    });
-    it('returns empty if disabled', async () => {
-      config.enabled = false;
-      const res = await packageFileWorker.renovatePackageFile(config);
-      expect(res).toEqual([]);
-    });
-    it('returns upgrades', async () => {
-      depTypeWorker.renovateDepType.mockReturnValueOnce([{}, {}]);
-      const res = await packageFileWorker.renovatePackageFile(config);
-      expect(res).toHaveLength(2);
-    });
-  });
-  describe('renovateBazelFile(config)', () => {
-    let config;
-    beforeEach(() => {
-      config = {
-        ...defaultConfig,
-        packageFile: 'WORKSPACE',
-        manager: 'bazel',
-        repoIsOnboarded: true,
-      };
-      depTypeWorker.renovateDepType.mockReturnValue([]);
-    });
-    it('returns empty if disabled', async () => {
-      config.enabled = false;
-      const res = await packageFileWorker.renovatePackageFile(config);
-      expect(res).toEqual([]);
-    });
-    it('returns upgrades', async () => {
-      depTypeWorker.renovateDepType.mockReturnValueOnce([{}, {}]);
-      const res = await packageFileWorker.renovatePackageFile(config);
-      expect(res).toHaveLength(2);
-    });
-  });
-  describe('renovateNodeFile(config)', () => {
-    let config;
-    beforeEach(() => {
-      config = {
-        ...defaultConfig,
-        packageFile: '.travis.yml',
-        manager: 'travis',
-        repoIsOnboarded: true,
-      };
-      depTypeWorker.renovateDepType.mockReturnValue([]);
-    });
-    it('returns empty if disabled', async () => {
-      config.enabled = false;
-      const res = await packageFileWorker.renovatePackageFile(config);
-      expect(res).toEqual([]);
-    });
-    it('returns upgrades', async () => {
-      depTypeWorker.renovateDepType.mockReturnValueOnce([{}]);
-      const res = await packageFileWorker.renovatePackageFile(config);
-      expect(res).toHaveLength(1);
-    });
-  });
-  describe('renovateDockerfile', () => {
-    let config;
-    beforeEach(() => {
-      config = {
-        ...defaultConfig,
-        packageFile: 'Dockerfile',
-        manager: 'docker',
-        repoIsOnboarded: true,
-      };
-      depTypeWorker.renovateDepType.mockReturnValue([]);
-    });
-    it('returns empty if disabled', async () => {
-      config.enabled = false;
-      const res = await packageFileWorker.renovatePackageFile(config);
-      expect(res).toEqual([]);
-    });
-    it('returns upgrades', async () => {
-      depTypeWorker.renovateDepType.mockReturnValueOnce([{}, {}]);
-      const res = await packageFileWorker.renovatePackageFile(config);
-      expect(res).toHaveLength(2);
-    });
-  });
-});
diff --git a/test/workers/package-file/package.spec.js b/test/workers/package-file/package.spec.js
deleted file mode 100644
index 9f9676a204a68aee64eeed3120620b18195bceec..0000000000000000000000000000000000000000
--- a/test/workers/package-file/package.spec.js
+++ /dev/null
@@ -1,76 +0,0 @@
-const pkgWorker = require('../../../lib/workers/package-file/package');
-const defaultConfig = require('../../../lib/config/defaults').getConfig();
-const configParser = require('../../../lib/config');
-
-const docker = require('../../../lib/manager/docker/package');
-const npm = require('../../../lib/manager/npm/package');
-const node = require('../../../lib/manager/travis/package');
-const bazel = require('../../../lib/manager/bazel/package');
-
-jest.mock('../../../lib/manager/docker/package');
-jest.mock('../../../lib/manager/npm/package');
-jest.mock('../../../lib/manager/travis/package');
-jest.mock('../../../lib/manager/bazel/package');
-
-describe('lib/workers/package-file/package', () => {
-  describe('renovatePackage(config)', () => {
-    let config;
-    beforeEach(() => {
-      config = configParser.filterConfig(defaultConfig, 'package');
-      config.depName = 'foo';
-      config.currentVersion = '1.0.0';
-    });
-    it('returns empty if package is disabled', async () => {
-      config.enabled = false;
-      const res = await pkgWorker.renovatePackage(config);
-      expect(res).toMatchObject([]);
-    });
-    it('calls docker', async () => {
-      docker.getPackageUpdates.mockReturnValueOnce([]);
-      config.manager = 'docker';
-      const res = await pkgWorker.renovatePackage(config);
-      expect(res).toMatchObject([]);
-    });
-    it('calls meteor', async () => {
-      npm.getPackageUpdates.mockReturnValueOnce([]);
-      config.manager = 'meteor';
-      const res = await pkgWorker.renovatePackage(config);
-      expect(res).toMatchObject([]);
-    });
-    it('calls node', async () => {
-      node.getPackageUpdates.mockReturnValueOnce([]);
-      config.manager = 'travis';
-      const res = await pkgWorker.renovatePackage(config);
-      expect(res).toMatchObject([]);
-    });
-    it('calls bazel', async () => {
-      bazel.getPackageUpdates.mockReturnValueOnce([]);
-      config.manager = 'bazel';
-      const res = await pkgWorker.renovatePackage(config);
-      expect(res).toMatchObject([]);
-    });
-    it('maps and filters type', async () => {
-      config.manager = 'npm';
-      config.major.enabled = false;
-      npm.getPackageUpdates.mockReturnValueOnce([
-        { type: 'pin' },
-        { type: 'major' },
-        { type: 'minor', enabled: false },
-      ]);
-      const res = await pkgWorker.renovatePackage(config);
-      expect(res).toHaveLength(1);
-      expect(res[0].groupName).toEqual('Pin Dependencies');
-    });
-    it('throws', async () => {
-      npm.getPackageUpdates.mockReturnValueOnce([]);
-      config.packageFile = 'something-else';
-      let e;
-      try {
-        await pkgWorker.renovatePackage(config);
-      } catch (err) {
-        e = err;
-      }
-      expect(e).toBeDefined();
-    });
-  });
-});
diff --git a/test/workers/repository/__snapshots__/index.spec.js.snap b/test/workers/repository/__snapshots__/index.spec.js.snap
index 559f51fc6ddb2c17516f0fa8fded51d5297f37f8..220f09db93b922ddb0af8cdc5ee30c7af77338af 100644
--- a/test/workers/repository/__snapshots__/index.spec.js.snap
+++ b/test/workers/repository/__snapshots__/index.spec.js.snap
@@ -1,8 +1,3 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`workers/repository renovateRepository() writes 1`] = `
-Object {
-  "res": undefined,
-  "status": "onboarding",
-}
-`;
+exports[`workers/repository renovateRepository() runs 1`] = `undefined`;
diff --git a/test/workers/repository/extract/__snapshots__/file-match.spec.js.snap b/test/workers/repository/extract/__snapshots__/file-match.spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..9abbb3dc652385cd1359453a2181dd36abaf3652
--- /dev/null
+++ b/test/workers/repository/extract/__snapshots__/file-match.spec.js.snap
@@ -0,0 +1,32 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`workers/repository/extract/file-match filterIgnoredFiles() ignores partial matches 1`] = `
+Array [
+  "package.json",
+]
+`;
+
+exports[`workers/repository/extract/file-match filterIgnoredFiles() returns minimatch matches 1`] = `
+Array [
+  "package.json",
+]
+`;
+
+exports[`workers/repository/extract/file-match getIncludedFiles() returns exact matches 1`] = `
+Array [
+  "frontend/package.json",
+]
+`;
+
+exports[`workers/repository/extract/file-match getIncludedFiles() returns minimatch matches 1`] = `
+Array [
+  "frontend/package.json",
+]
+`;
+
+exports[`workers/repository/extract/file-match getMatchingFiles() returns npm files 1`] = `
+Array [
+  "package.json",
+  "frontend/package.json",
+]
+`;
diff --git a/test/workers/repository/extract/__snapshots__/index.spec.js.snap b/test/workers/repository/extract/__snapshots__/index.spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..9dcb229b6d7052c2a474772abc8c65361a18e20c
--- /dev/null
+++ b/test/workers/repository/extract/__snapshots__/index.spec.js.snap
@@ -0,0 +1,36 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`workers/repository/extract/index extractAllDependencies() runs 1`] = `
+Object {
+  "bazel": Array [
+    Object {},
+  ],
+  "buildkite": Array [
+    Object {},
+  ],
+  "circleci": Array [
+    Object {},
+  ],
+  "docker": Array [
+    Object {},
+  ],
+  "docker-compose": Array [
+    Object {},
+  ],
+  "meteor": Array [
+    Object {},
+  ],
+  "npm": Array [
+    Object {},
+  ],
+  "nvm": Array [
+    Object {},
+  ],
+  "pip_requirements": Array [
+    Object {},
+  ],
+  "travis": Array [
+    Object {},
+  ],
+}
+`;
diff --git a/test/workers/repository/extract/__snapshots__/manager-files.spec.js.snap b/test/workers/repository/extract/__snapshots__/manager-files.spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..cf7804a7daa1f681850cc92115c71d3e9e77bb6e
--- /dev/null
+++ b/test/workers/repository/extract/__snapshots__/manager-files.spec.js.snap
@@ -0,0 +1,11 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`workers/repository/extract/manager-files getManagerPackageFiles() returns files 1`] = `
+Array [
+  Object {
+    "manager": "npm",
+    "packageFile": "package.json",
+    "some": "result",
+  },
+]
+`;
diff --git a/test/workers/repository/extract/file-match.spec.js b/test/workers/repository/extract/file-match.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..8a1ace151080133cfd741525db4296b4784dcb20
--- /dev/null
+++ b/test/workers/repository/extract/file-match.spec.js
@@ -0,0 +1,51 @@
+const fileMatch = require('../../../../lib/workers/repository/extract/file-match');
+
+describe('workers/repository/extract/file-match', () => {
+  const fileList = ['package.json', 'frontend/package.json'];
+  describe('getIncludedFiles()', () => {
+    it('returns fileList if no includePaths', () => {
+      const res = fileMatch.getIncludedFiles(fileList, []);
+      expect(res).toEqual(fileList);
+    });
+    it('returns exact matches', () => {
+      const includePaths = ['frontend/package.json'];
+      const res = fileMatch.getIncludedFiles(fileList, includePaths);
+      expect(res).toMatchSnapshot();
+      expect(res).toHaveLength(1);
+    });
+    it('returns minimatch matches', () => {
+      const includePaths = ['frontend/**'];
+      const res = fileMatch.getIncludedFiles(fileList, includePaths);
+      expect(res).toMatchSnapshot();
+      expect(res).toHaveLength(1);
+    });
+  });
+  describe('filterIgnoredFiles()', () => {
+    it('returns fileList if no ignoredPaths', () => {
+      const res = fileMatch.filterIgnoredFiles(fileList, []);
+      expect(res).toEqual(fileList);
+    });
+    it('ignores partial matches', () => {
+      const ignoredPaths = ['frontend'];
+      const res = fileMatch.filterIgnoredFiles(fileList, ignoredPaths);
+      expect(res).toMatchSnapshot();
+      expect(res).toHaveLength(1);
+    });
+    it('returns minimatch matches', () => {
+      const ignoredPaths = ['frontend/**'];
+      const res = fileMatch.filterIgnoredFiles(fileList, ignoredPaths);
+      expect(res).toMatchSnapshot();
+      expect(res).toHaveLength(1);
+    });
+  });
+  describe('getMatchingFiles()', () => {
+    it('returns npm files', () => {
+      fileList.push('Dockerfile');
+      const res = fileMatch.getMatchingFiles(fileList, 'npm', [
+        '(^|/)package.json$',
+      ]);
+      expect(res).toMatchSnapshot();
+      expect(res).toHaveLength(2);
+    });
+  });
+});
diff --git a/test/workers/repository/extract/index.spec.js b/test/workers/repository/extract/index.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..8ed1b5f859b5c0d1ebbaaae3cbe7bb891e7db08f
--- /dev/null
+++ b/test/workers/repository/extract/index.spec.js
@@ -0,0 +1,21 @@
+const managerFiles = require('../../../../lib/workers/repository/extract/manager-files');
+const {
+  extractAllDependencies,
+} = require('../../../../lib/workers/repository/extract');
+
+jest.mock('../../../../lib/workers/repository/extract/manager-files');
+
+describe('workers/repository/extract/index', () => {
+  describe('extractAllDependencies()', () => {
+    let config;
+    beforeEach(() => {
+      jest.resetAllMocks();
+      config = { ...require('../../../_fixtures/config') };
+    });
+    it('runs', async () => {
+      managerFiles.getManagerPackageFiles.mockReturnValue([{}]);
+      const res = await extractAllDependencies(config);
+      expect(res).toMatchSnapshot();
+    });
+  });
+});
diff --git a/test/workers/repository/extract/manager-files.spec.js b/test/workers/repository/extract/manager-files.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..c67ef1d3d996e68c13981b313939281cef16eb0a
--- /dev/null
+++ b/test/workers/repository/extract/manager-files.spec.js
@@ -0,0 +1,35 @@
+const {
+  getManagerPackageFiles,
+} = require('../../../../lib/workers/repository/extract/manager-files');
+const fileMatch = require('../../../../lib/workers/repository/extract/file-match');
+const npm = require('../../../../lib/manager/npm');
+
+jest.mock('../../../../lib/workers/repository/extract/file-match');
+
+describe('workers/repository/extract/manager-files', () => {
+  describe('getManagerPackageFiles()', () => {
+    let config;
+    beforeEach(() => {
+      jest.resetAllMocks();
+      config = { ...require('../../../_fixtures/config') };
+    });
+    it('returns empty of manager is disabled', async () => {
+      const managerConfig = { manager: 'travis', enabled: false };
+      const res = await getManagerPackageFiles(config, managerConfig);
+      expect(res).toHaveLength(0);
+    });
+    it('returns empty of manager is not enabled', async () => {
+      config.enabledManagers = ['npm'];
+      const managerConfig = { manager: 'docker', enabled: true };
+      const res = await getManagerPackageFiles(config, managerConfig);
+      expect(res).toHaveLength(0);
+    });
+    it('returns files', async () => {
+      const managerConfig = { manager: 'npm', enabled: true };
+      fileMatch.getMatchingFiles.mockReturnValue(['package.json']);
+      npm.extractDependencies = jest.fn(() => ({ some: 'result' }));
+      const res = await getManagerPackageFiles(config, managerConfig);
+      expect(res).toMatchSnapshot();
+    });
+  });
+});
diff --git a/test/workers/repository/index.spec.js b/test/workers/repository/index.spec.js
index 894278d63f23089c1a4d857726eb75603c63c7f8..81dfb9a551e6d3543407ebd66963c3267f0924a0 100644
--- a/test/workers/repository/index.spec.js
+++ b/test/workers/repository/index.spec.js
@@ -1,35 +1,20 @@
-const { initRepo } = require('../../../lib/workers/repository/init');
-const { determineUpdates } = require('../../../lib/workers/repository/updates');
-const {
-  writeUpdates,
-} = require('../../../lib/workers/repository/process/write');
 const { renovateRepository } = require('../../../lib/workers/repository/index');
+const process = require('../../../lib/workers/repository/process');
 
 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/process/write');
-jest.mock('../../../lib/workers/repository/finalise');
-jest.mock('../../../lib/manager');
-jest.mock('delay');
-
-let config;
-beforeEach(() => {
-  jest.resetAllMocks();
-  config = require('../../_fixtures/config');
-});
+jest.mock('../../../lib/workers/repository/process');
+jest.mock('../../../lib/workers/repository/result');
+jest.mock('../../../lib/workers/repository/error');
 
 describe('workers/repository', () => {
   describe('renovateRepository()', () => {
-    it('writes', async () => {
-      initRepo.mockReturnValue({});
-      determineUpdates.mockReturnValue({
-        repoIsOnboarded: true,
-        branches: [{ type: 'minor' }, { type: 'pin' }],
-      });
-      writeUpdates.mockReturnValueOnce('done');
-      const res = await renovateRepository(config, 'some-token');
+    let config;
+    beforeEach(() => {
+      config = require('../../_fixtures/config');
+    });
+    it('runs', async () => {
+      process.processRepo = jest.fn(() => ({}));
+      const res = await renovateRepository(config);
       expect(res).toMatchSnapshot();
     });
   });
diff --git a/test/workers/repository/onboarding/branch/index.spec.js b/test/workers/repository/onboarding/branch/index.spec.js
index a61d7cc2d1b9f883ffa4b6fea60ba2e323b010a4..e074cf678bf4de61ec8c2ec5b50de8403a9d9282 100644
--- a/test/workers/repository/onboarding/branch/index.spec.js
+++ b/test/workers/repository/onboarding/branch/index.spec.js
@@ -78,13 +78,6 @@ describe('workers/repository/onboarding/branch', () => {
       }
       expect(e).toBeDefined();
     });
-    it('creates onboarding branch', async () => {
-      platform.getFileList.mockReturnValue(['package.json']);
-      const res = await checkOnboardingBranch(config);
-      expect(res.repoIsOnboarded).toBe(false);
-      expect(res.branchList).toEqual(['renovate/configure']);
-      expect(platform.setBaseBranch.mock.calls).toHaveLength(1);
-    });
     it('creates onboarding branch with greenkeeper migration', async () => {
       platform.getFileList.mockReturnValue(['package.json']);
       const pJsonContent = JSON.stringify({
diff --git a/test/workers/repository/onboarding/pr/index.spec.js b/test/workers/repository/onboarding/pr/index.spec.js
index 32d2ca3b223c70a54bcf35de4d44cbb137c3f6cd..b14834830529d1bdde247d167d07356823bffec7 100644
--- a/test/workers/repository/onboarding/pr/index.spec.js
+++ b/test/workers/repository/onboarding/pr/index.spec.js
@@ -17,11 +17,15 @@ describe('workers/repository/onboarding/pr', () => {
         warnings: [],
         description: [],
       };
-      packageFiles = [{ packageFile: 'package.json' }];
+      packageFiles = { npm: [{ packageFile: 'package.json' }] };
       branches = [];
       platform.createPr.mockReturnValue({});
     });
     let createPrBody;
+    it('returns if onboarded', async () => {
+      config.repoIsOnboarded = true;
+      await ensureOnboardingPr(config, packageFiles, branches);
+    });
     it('creates PR', async () => {
       await ensureOnboardingPr(config, packageFiles, branches);
       expect(platform.createPr.mock.calls).toHaveLength(1);
diff --git a/test/workers/repository/process/__snapshots__/fetch.spec.js.snap b/test/workers/repository/process/__snapshots__/fetch.spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..063de32ae4632ba88fc10d98350ef3def0a1c9ba
--- /dev/null
+++ b/test/workers/repository/process/__snapshots__/fetch.spec.js.snap
@@ -0,0 +1,61 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`workers/repository/process/fetch fetchUpdates() fetches updates 1`] = `
+Object {
+  "npm": Array [
+    Object {
+      "deps": Array [
+        Object {
+          "depName": "aaa",
+          "updates": Array [
+            "a",
+            "b",
+          ],
+        },
+      ],
+      "packageFile": "package.json",
+    },
+  ],
+}
+`;
+
+exports[`workers/repository/process/fetch fetchUpdates() handles empty deps 1`] = `
+Object {
+  "npm": Array [
+    Object {
+      "deps": Array [],
+      "packageFile": "package.json",
+    },
+  ],
+}
+`;
+
+exports[`workers/repository/process/fetch fetchUpdates() handles ignores and disabled 1`] = `
+Object {
+  "npm": Array [
+    Object {
+      "deps": Array [
+        Object {
+          "depName": "abcd",
+          "skipReason": "ignored",
+          "updates": Array [],
+        },
+        Object {
+          "depName": "zzzz",
+          "skipReason": "monorepo",
+          "updates": Array [],
+        },
+        Object {
+          "depName": "foo",
+          "skipReason": "disabled",
+          "updates": Array [],
+        },
+      ],
+      "monorepoPackages": Array [
+        "zzzz",
+      ],
+      "packageFile": "package.json",
+    },
+  ],
+}
+`;
diff --git a/test/workers/repository/process/extract-update.spec.js b/test/workers/repository/process/extract-update.spec.js
index f91a8c3bcaaf20f8f916da8603a3593e15dacb8a..9747ee324f14e13451baa45c49c27047fa1e1164 100644
--- a/test/workers/repository/process/extract-update.spec.js
+++ b/test/workers/repository/process/extract-update.spec.js
@@ -1,18 +1,23 @@
 const {
   extractAndUpdate,
 } = require('../../../../lib/workers/repository/process/extract-update');
-const updates = require('../../../../lib/workers/repository/updates');
+const branchify = require('../../../../lib/workers/repository/updates/branchify');
 
-jest.mock('../../../../lib/manager');
-jest.mock('../../../../lib/workers/repository/updates');
-jest.mock('../../../../lib/workers/repository/process/sort');
 jest.mock('../../../../lib/workers/repository/process/write');
+jest.mock('../../../../lib/workers/repository/process/sort');
+jest.mock('../../../../lib/workers/repository/process/fetch');
+jest.mock('../../../../lib/workers/repository/updates/branchify');
+jest.mock('../../../../lib/workers/repository/extract');
+
+branchify.branchifyUpgrades.mockReturnValueOnce({});
 
 describe('workers/repository/process/extract-update', () => {
   describe('extractAndUpdate()', () => {
     it('runs', async () => {
-      updates.determineUpdates.mockReturnValue({ repoIsOnboarded: true });
-      await extractAndUpdate();
+      const config = {
+        repoIsOnboarded: true,
+      };
+      await extractAndUpdate(config);
     });
   });
 });
diff --git a/test/workers/repository/process/fetch.spec.js b/test/workers/repository/process/fetch.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..87796ab8018cf01e4a34e6ffaa55eb0a72fec36e
--- /dev/null
+++ b/test/workers/repository/process/fetch.spec.js
@@ -0,0 +1,67 @@
+const {
+  fetchUpdates,
+} = require('../../../../lib/workers/repository/process/fetch');
+
+const npm = require('../../../../lib/manager/npm');
+
+describe('workers/repository/process/fetch', () => {
+  describe('fetchUpdates()', () => {
+    let config;
+    beforeEach(() => {
+      jest.resetAllMocks();
+      config = require('../../../_fixtures/config');
+    });
+    it('handles empty deps', async () => {
+      const packageFiles = {
+        npm: [{ packageFile: 'package.json', deps: [] }],
+      };
+      await fetchUpdates(config, packageFiles);
+      expect(packageFiles).toMatchSnapshot();
+    });
+    it('handles ignores and disabled', async () => {
+      config.ignoreDeps = ['abcd'];
+      config.packageRules = [
+        {
+          packageNames: ['foo'],
+          enabled: false,
+        },
+      ];
+      const packageFiles = {
+        npm: [
+          {
+            packageFile: 'package.json',
+            deps: [
+              { depName: 'abcd' },
+              { depName: 'zzzz' },
+              { depName: 'foo' },
+            ],
+            monorepoPackages: ['zzzz'],
+          },
+        ],
+      };
+      await fetchUpdates(config, packageFiles);
+      expect(packageFiles).toMatchSnapshot();
+      expect(packageFiles.npm[0].deps[0].skipReason).toEqual('ignored');
+      expect(packageFiles.npm[0].deps[0].updates).toHaveLength(0);
+      expect(packageFiles.npm[0].deps[1].skipReason).toEqual('monorepo');
+      expect(packageFiles.npm[0].deps[1].updates).toHaveLength(0);
+      expect(packageFiles.npm[0].deps[2].skipReason).toEqual('disabled');
+      expect(packageFiles.npm[0].deps[2].updates).toHaveLength(0);
+    });
+    it('fetches updates', async () => {
+      const packageFiles = {
+        npm: [
+          {
+            packageFile: 'package.json',
+            deps: [{ depName: 'aaa' }],
+          },
+        ],
+      };
+      npm.getPackageUpdates = jest.fn(() => ['a', 'b']);
+      await fetchUpdates(config, packageFiles);
+      expect(packageFiles).toMatchSnapshot();
+      expect(packageFiles.npm[0].deps[0].skipReason).toBeUndefined();
+      expect(packageFiles.npm[0].deps[0].updates).toHaveLength(2);
+    });
+  });
+});
diff --git a/test/workers/repository/process/limits.spec.js b/test/workers/repository/process/limits.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..90c025e205377a2a3cde5cf96b31452ba8757fc3
--- /dev/null
+++ b/test/workers/repository/process/limits.spec.js
@@ -0,0 +1,54 @@
+const moment = require('moment');
+const limits = require('../../../../lib/workers/repository/process/limits');
+
+let config;
+beforeEach(() => {
+  jest.resetAllMocks();
+  config = { ...require('../../../_fixtures/config') };
+});
+
+describe('workers/repository/process/limits', () => {
+  describe('getPrHourlyRemaining()', () => {
+    it('calculates hourly limit remaining', async () => {
+      config.prHourlyLimit = 2;
+      platform.getPrList.mockReturnValueOnce([
+        { created_at: moment().format() },
+      ]);
+      const res = await limits.getPrHourlyRemaining(config);
+      expect(res).toEqual(1);
+    });
+    it('returns 99 if errored', async () => {
+      config.prHourlyLimit = 2;
+      platform.getPrList.mockReturnValueOnce([null]);
+      const res = await limits.getPrHourlyRemaining(config);
+      expect(res).toEqual(99);
+    });
+  });
+  describe('getConcurrentPrsRemaining()', () => {
+    it('calculates concurrent limit remaining', async () => {
+      config.prConcurrentLimit = 20;
+      platform.branchExists.mockReturnValueOnce(true);
+      const branches = [{}, {}];
+      const res = await limits.getConcurrentPrsRemaining(config, branches);
+      expect(res).toEqual(19);
+    });
+    it('returns 99 if no concurrent limit', async () => {
+      const res = await limits.getConcurrentPrsRemaining(config, []);
+      expect(res).toEqual(99);
+    });
+  });
+  describe('getPrsRemaining()', () => {
+    it('returns hourly limit', async () => {
+      limits.getPrHourlyRemaining = jest.fn(() => 5);
+      limits.getConcurrentPrsRemaining = jest.fn(() => 10);
+      const res = await limits.getPrsRemaining();
+      expect(res).toEqual(5);
+    });
+    it('returns concurrent limit', async () => {
+      limits.getPrHourlyRemaining = jest.fn(() => 10);
+      limits.getConcurrentPrsRemaining = jest.fn(() => 5);
+      const res = await limits.getPrsRemaining();
+      expect(res).toEqual(5);
+    });
+  });
+});
diff --git a/test/workers/repository/process/write.spec.js b/test/workers/repository/process/write.spec.js
index 48f1d22c1ab0c7362929c698c27bcb4c446a4009..55fdb818cd6710a0bf3208d6c5bf1efa914df746 100644
--- a/test/workers/repository/process/write.spec.js
+++ b/test/workers/repository/process/write.spec.js
@@ -2,9 +2,10 @@ const {
   writeUpdates,
 } = require('../../../../lib/workers/repository/process/write');
 const branchWorker = require('../../../../lib/workers/branch');
-const moment = require('moment');
+const limits = require('../../../../lib/workers/repository/process/limits');
 
 branchWorker.processBranch = jest.fn();
+limits.getPrsRemaining = jest.fn(() => 99);
 
 let config;
 beforeEach(() => {
@@ -14,44 +15,19 @@ beforeEach(() => {
 
 describe('workers/repository/write', () => {
   describe('writeUpdates()', () => {
-    it('calculates hourly limit remaining', async () => {
-      config.branches = [];
-      config.prHourlyLimit = 1;
-      platform.getPrList.mockReturnValueOnce([
-        { created_at: moment().format() },
-      ]);
-      const res = await writeUpdates(config);
-      expect(res).toEqual('done');
-    });
-    it('calculates concurrent limit remaining', async () => {
-      config.branches = ['renovate/chalk-2.x'];
-      config.prConcurrentLimit = 1;
-      platform.getPrList.mockReturnValueOnce([
-        { created_at: moment().format() },
-      ]);
-      platform.branchExists.mockReturnValueOnce(true);
-      const res = await writeUpdates(config);
-      expect(res).toEqual('done');
-    });
-    it('handles error in calculation', async () => {
-      config.branches = [];
-      config.prHourlyLimit = 1;
-      platform.getPrList.mockReturnValueOnce([{}, null]);
-      const res = await writeUpdates(config);
-      expect(res).toEqual('done');
-    });
+    const packageFiles = {};
     it('runs pins first', async () => {
-      config.branches = [{ isPin: true }, {}, {}];
-      const res = await writeUpdates(config);
+      const branches = [{ isPin: true }, {}, {}];
+      const res = await writeUpdates(config, packageFiles, branches);
       expect(res).toEqual('done');
       expect(branchWorker.processBranch.mock.calls).toHaveLength(1);
     });
     it('stops after automerge', async () => {
-      config.branches = [{}, {}, {}, {}];
+      const branches = [{}, {}, {}, {}];
       branchWorker.processBranch.mockReturnValueOnce('created');
       branchWorker.processBranch.mockReturnValueOnce('delete');
       branchWorker.processBranch.mockReturnValueOnce('automerged');
-      const res = await writeUpdates(config);
+      const res = await writeUpdates(config, packageFiles, branches);
       expect(res).toEqual('automerged');
       expect(branchWorker.processBranch.mock.calls).toHaveLength(3);
     });
diff --git a/test/workers/repository/result.spec.js b/test/workers/repository/result.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..ca0f39d7033f9bfee09bbc9c223947e3508414b8
--- /dev/null
+++ b/test/workers/repository/result.spec.js
@@ -0,0 +1,15 @@
+const { processResult } = require('../../../lib/workers/repository/result');
+
+let config;
+beforeEach(() => {
+  jest.resetAllMocks();
+  config = require('../../_fixtures/config');
+});
+
+describe('workers/repository/result', () => {
+  describe('processResult()', () => {
+    it('runs', () => {
+      processResult(config, 'done');
+    });
+  });
+});
diff --git a/test/workers/repository/updates/__snapshots__/flatten.spec.js.snap b/test/workers/repository/updates/__snapshots__/flatten.spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..d31d91eeb7098fd7d3819d9476a0259059e7820c
--- /dev/null
+++ b/test/workers/repository/updates/__snapshots__/flatten.spec.js.snap
@@ -0,0 +1,220 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`workers/repository/updates/flatten flattenUpdates() flattens 1`] = `
+Array [
+  Object {
+    "assignees": Array [],
+    "automerge": false,
+    "automergeComment": "automergeComment",
+    "automergeType": "pr",
+    "branchName": "{{{branchPrefix}}}{{{managerBranchPrefix}}}{{{branchTopic}}}",
+    "branchPrefix": "renovate/",
+    "branchTopic": "{{{depNameSanitized}}}-{{{newVersionMajor}}}.x",
+    "bumpVersion": null,
+    "commitBody": null,
+    "commitMessage": "{{{commitMessagePrefix}}} {{{commitMessageAction}}} {{{commitMessageTopic}}} {{{commitMessageExtra}}} {{{commitMessageSuffix}}}",
+    "commitMessageAction": "Update",
+    "commitMessageExtra": "to {{#if isMajor}}v{{{newVersionMajor}}}{{else}}{{#unless isRange}}v{{/unless}}{{{newVersion}}}{{/if}}",
+    "commitMessagePrefix": null,
+    "commitMessageTopic": "dependency {{depName}}",
+    "copyLocalLibs": false,
+    "depName": "@org/a",
+    "depNameSanitized": "org-a",
+    "errors": Array [],
+    "gitAuthor": null,
+    "gitPrivateKey": null,
+    "group": Object {
+      "branchTopic": "{{{groupSlug}}}",
+      "commitMessageTopic": "{{{groupName}}}",
+      "prBody": "This Pull Request renovates the package group \\"{{{groupName}}}\\".\\n\\n{{#if schedule}}\\n**Note**: This PR was created on a configured schedule (\\"{{{schedule}}}\\"{{#if timezone}} in timezone \`{{{timezone}}}\`{{/if}}) and will not receive updates outside those times.\\n{{/if}}\\n\\n{{#each upgrades as |upgrade|}}\\n-   {{#if repositoryUrl}}[{{{upgrade.depName}}}]({{upgrade.repositoryUrl}}){{else}}\`{{{depName}}}\`{{/if}}{{#if depType}} (\`{{{depType}}}\`){{/if}}: from \`{{{upgrade.currentVersion}}}\` to \`{{{upgrade.newVersion}}}\`\\n{{/each}}\\n\\n{{#if hasReleaseNotes}}\\n# Release Notes\\n{{#each upgrades as |upgrade|}}\\n{{#if upgrade.hasReleaseNotes}}\\n<details>\\n<summary>{{upgrade.githubName}}</summary>\\n\\n{{#each upgrade.releases as |release|}}\\n{{#if release.releaseNotes}}\\n### [\`v{{{release.version}}}\`]({{{release.releaseNotes.url}}})\\n{{#if release.compare.url}}\\n[Compare Source]({{release.compare.url}})\\n{{/if}}\\n{{{release.releaseNotes.body}}}\\n\\n---\\n\\n{{/if}}\\n{{/each}}\\n\\n</details>\\n{{/if}}\\n{{/each}}\\n{{/if}}\\n\\n{{#if isPin}}\\n**Important**: Renovate will wait until you have merged this Pin request before creating PRs for any *upgrades*. If you do not wish to pin anything, please update your config accordingly instead of leaving this PR open.\\n{{/if}}\\n\\n{{#if hasErrors}}\\n\\n---\\n\\n# Errors\\n\\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\\n\\n{{#each errors as |error|}}\\n-   \`{{error.depName}}\`: {{error.message}}\\n{{/each}}\\n{{/if}}\\n\\n{{#if hasWarnings}}\\n\\n---\\n\\n# Warnings\\n\\nPlease make sure the following warnings are safe to ignore:\\n\\n{{#each warnings as |warning|}}\\n-   \`{{warning.depName}}\`: {{warning.message}}\\n{{/each}}\\n{{/if}}",
+    },
+    "groupName": null,
+    "groupSlug": null,
+    "labels": Array [],
+    "lazyGrouping": true,
+    "lockFileMaintenance": Object {
+      "branchTopic": "lock-file-maintenance",
+      "commitMessageAction": "Lock file maintenance",
+      "commitMessageExtra": null,
+      "commitMessageTopic": null,
+      "enabled": true,
+      "groupName": null,
+      "prBody": "This Pull Request updates \`package.json\` lock files to use the latest dependency versions.\\n\\n{{#if schedule}}\\n**Note**: This PR was created on a configured schedule (\\"{{{schedule}}}\\"{{#if timezone}} in timezone \`{{{timezone}}}\`{{/if}}) and will not receive updates outside those times.\\n{{/if}}\\n\\n{{#if hasErrors}}\\n\\n---\\n\\n# Errors\\n\\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\\n\\n{{#each errors as |error|}}\\n-   \`{{error.depName}}\`: {{error.message}}\\n{{/each}}\\n{{/if}}\\n\\n{{#if hasWarnings}}\\n\\n---\\n\\n# Warnings\\n\\nPlease make sure the following warnings are safe to ignore:\\n\\n{{#each warnings as |warning|}}\\n-   \`{{warning.depName}}\`: {{warning.message}}\\n{{/each}}\\n{{/if}}",
+      "rebaseStalePrs": true,
+      "recreateClosed": true,
+      "schedule": Array [
+        "before 5am on monday",
+      ],
+    },
+    "manager": "npm",
+    "managerBranchPrefix": "",
+    "newVersion": "1.0.0",
+    "npmToken": null,
+    "npmrc": null,
+    "packageFile": "package.json ",
+    "prBody": "This Pull 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}}\\n{{#if hasTypes}}\\n\\nThis PR also includes an upgrade to the corresponding [@types/{{{depName}}}](https://npmjs.com/package/@types/{{{depName}}}) package.\\n{{/if}}\\n{{#if releases.length}}\\n\\n{{#if schedule}}\\n**Note**: This PR was created on a configured schedule (\\"{{{schedule}}}\\"{{#if timezone}} in timezone \`{{{timezone}}}\`{{/if}}) and will not receive updates outside those times.\\n{{/if}}\\n\\n{{#if isPin}}\\n**Important**: Renovate will wait until you have merged this Pin request before creating PRs for any *upgrades*. If you do not wish to pin anything, please update your config accordingly instead of leaving this PR open.\\n{{/if}}\\n{{#if hasReleaseNotes}}\\n\\n<details>\\n<summary>Release Notes</summary>\\n\\n{{#each releases as |release|}}\\n{{#if release.releaseNotes}}\\n### [\`v{{{release.version}}}\`]({{{release.releaseNotes.url}}})\\n{{#if release.compare.url}}\\n[Compare Source]({{release.compare.url}})\\n{{/if}}\\n{{{release.releaseNotes.body}}}\\n\\n---\\n\\n{{/if}}\\n{{/each}}\\n</details>\\n{{/if}}\\n\\n{{/if}}\\n\\n{{#if hasErrors}}\\n\\n---\\n\\n# Errors\\n\\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\\n\\n{{#each errors as |error|}}\\n-   \`{{error.depName}}\`: {{error.message}}\\n{{/each}}\\n{{/if}}\\n\\n{{#if hasWarnings}}\\n\\n---\\n\\n# Warnings\\n\\nPlease make sure the following warnings are safe to ignore:\\n\\n{{#each warnings as |warning|}}\\n-   \`{{warning.depName}}\`: {{warning.message}}\\n{{/each}}\\n{{/if}}",
+    "prConcurrentLimit": 0,
+    "prCreation": "immediate",
+    "prHourlyLimit": 0,
+    "prNotPendingHours": 25,
+    "prTitle": null,
+    "rebaseStalePrs": null,
+    "recreateClosed": false,
+    "requiredStatusChecks": Array [],
+    "reviewers": Array [],
+    "schedule": Array [],
+    "semanticCommitScope": "deps",
+    "semanticCommitType": "chore",
+    "semanticCommits": null,
+    "statusCheckVerify": false,
+    "timezone": null,
+    "unpublishSafe": false,
+    "updateLockFiles": true,
+    "updateNotScheduled": true,
+    "warnings": Array [],
+    "yarnrc": null,
+  },
+  Object {
+    "assignees": Array [],
+    "automerge": false,
+    "automergeComment": "automergeComment",
+    "automergeType": "pr",
+    "branchName": "{{{branchPrefix}}}{{{managerBranchPrefix}}}{{{branchTopic}}}",
+    "branchPrefix": "renovate/",
+    "branchTopic": "{{{depNameSanitized}}}-{{{newVersionMajor}}}.x",
+    "bumpVersion": null,
+    "commitBody": null,
+    "commitMessage": "{{{commitMessagePrefix}}} {{{commitMessageAction}}} {{{commitMessageTopic}}} {{{commitMessageExtra}}} {{{commitMessageSuffix}}}",
+    "commitMessageAction": "Update",
+    "commitMessageExtra": "to {{#if isMajor}}v{{{newVersionMajor}}}{{else}}{{#unless isRange}}v{{/unless}}{{{newVersion}}}{{/if}}",
+    "commitMessagePrefix": null,
+    "commitMessageTopic": "dependency {{depName}}",
+    "copyLocalLibs": false,
+    "depNameSanitized": undefined,
+    "errors": Array [],
+    "gitAuthor": null,
+    "gitPrivateKey": null,
+    "group": Object {
+      "branchTopic": "{{{groupSlug}}}",
+      "commitMessageTopic": "{{{groupName}}}",
+      "prBody": "This Pull Request renovates the package group \\"{{{groupName}}}\\".\\n\\n{{#if schedule}}\\n**Note**: This PR was created on a configured schedule (\\"{{{schedule}}}\\"{{#if timezone}} in timezone \`{{{timezone}}}\`{{/if}}) and will not receive updates outside those times.\\n{{/if}}\\n\\n{{#each upgrades as |upgrade|}}\\n-   {{#if repositoryUrl}}[{{{upgrade.depName}}}]({{upgrade.repositoryUrl}}){{else}}\`{{{depName}}}\`{{/if}}{{#if depType}} (\`{{{depType}}}\`){{/if}}: from \`{{{upgrade.currentVersion}}}\` to \`{{{upgrade.newVersion}}}\`\\n{{/each}}\\n\\n{{#if hasReleaseNotes}}\\n# Release Notes\\n{{#each upgrades as |upgrade|}}\\n{{#if upgrade.hasReleaseNotes}}\\n<details>\\n<summary>{{upgrade.githubName}}</summary>\\n\\n{{#each upgrade.releases as |release|}}\\n{{#if release.releaseNotes}}\\n### [\`v{{{release.version}}}\`]({{{release.releaseNotes.url}}})\\n{{#if release.compare.url}}\\n[Compare Source]({{release.compare.url}})\\n{{/if}}\\n{{{release.releaseNotes.body}}}\\n\\n---\\n\\n{{/if}}\\n{{/each}}\\n\\n</details>\\n{{/if}}\\n{{/each}}\\n{{/if}}\\n\\n{{#if isPin}}\\n**Important**: Renovate will wait until you have merged this Pin request before creating PRs for any *upgrades*. If you do not wish to pin anything, please update your config accordingly instead of leaving this PR open.\\n{{/if}}\\n\\n{{#if hasErrors}}\\n\\n---\\n\\n# Errors\\n\\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\\n\\n{{#each errors as |error|}}\\n-   \`{{error.depName}}\`: {{error.message}}\\n{{/each}}\\n{{/if}}\\n\\n{{#if hasWarnings}}\\n\\n---\\n\\n# Warnings\\n\\nPlease make sure the following warnings are safe to ignore:\\n\\n{{#each warnings as |warning|}}\\n-   \`{{warning.depName}}\`: {{warning.message}}\\n{{/each}}\\n{{/if}}",
+    },
+    "groupName": null,
+    "groupSlug": null,
+    "labels": Array [],
+    "lazyGrouping": true,
+    "lockFileMaintenance": Object {
+      "branchTopic": "lock-file-maintenance",
+      "commitMessageAction": "Lock file maintenance",
+      "commitMessageExtra": null,
+      "commitMessageTopic": null,
+      "enabled": true,
+      "groupName": null,
+      "prBody": "This Pull Request updates \`package.json\` lock files to use the latest dependency versions.\\n\\n{{#if schedule}}\\n**Note**: This PR was created on a configured schedule (\\"{{{schedule}}}\\"{{#if timezone}} in timezone \`{{{timezone}}}\`{{/if}}) and will not receive updates outside those times.\\n{{/if}}\\n\\n{{#if hasErrors}}\\n\\n---\\n\\n# Errors\\n\\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\\n\\n{{#each errors as |error|}}\\n-   \`{{error.depName}}\`: {{error.message}}\\n{{/each}}\\n{{/if}}\\n\\n{{#if hasWarnings}}\\n\\n---\\n\\n# Warnings\\n\\nPlease make sure the following warnings are safe to ignore:\\n\\n{{#each warnings as |warning|}}\\n-   \`{{warning.depName}}\`: {{warning.message}}\\n{{/each}}\\n{{/if}}",
+      "rebaseStalePrs": true,
+      "recreateClosed": true,
+      "schedule": Array [
+        "before 5am on monday",
+      ],
+    },
+    "manager": "npm",
+    "managerBranchPrefix": "",
+    "newVersion": "2.0.0",
+    "npmToken": null,
+    "npmrc": null,
+    "packageFile": "package.json ",
+    "prBody": "This Pull 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}}\\n{{#if hasTypes}}\\n\\nThis PR also includes an upgrade to the corresponding [@types/{{{depName}}}](https://npmjs.com/package/@types/{{{depName}}}) package.\\n{{/if}}\\n{{#if releases.length}}\\n\\n{{#if schedule}}\\n**Note**: This PR was created on a configured schedule (\\"{{{schedule}}}\\"{{#if timezone}} in timezone \`{{{timezone}}}\`{{/if}}) and will not receive updates outside those times.\\n{{/if}}\\n\\n{{#if isPin}}\\n**Important**: Renovate will wait until you have merged this Pin request before creating PRs for any *upgrades*. If you do not wish to pin anything, please update your config accordingly instead of leaving this PR open.\\n{{/if}}\\n{{#if hasReleaseNotes}}\\n\\n<details>\\n<summary>Release Notes</summary>\\n\\n{{#each releases as |release|}}\\n{{#if release.releaseNotes}}\\n### [\`v{{{release.version}}}\`]({{{release.releaseNotes.url}}})\\n{{#if release.compare.url}}\\n[Compare Source]({{release.compare.url}})\\n{{/if}}\\n{{{release.releaseNotes.body}}}\\n\\n---\\n\\n{{/if}}\\n{{/each}}\\n</details>\\n{{/if}}\\n\\n{{/if}}\\n\\n{{#if hasErrors}}\\n\\n---\\n\\n# Errors\\n\\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\\n\\n{{#each errors as |error|}}\\n-   \`{{error.depName}}\`: {{error.message}}\\n{{/each}}\\n{{/if}}\\n\\n{{#if hasWarnings}}\\n\\n---\\n\\n# Warnings\\n\\nPlease make sure the following warnings are safe to ignore:\\n\\n{{#each warnings as |warning|}}\\n-   \`{{warning.depName}}\`: {{warning.message}}\\n{{/each}}\\n{{/if}}",
+    "prConcurrentLimit": 0,
+    "prCreation": "immediate",
+    "prHourlyLimit": 0,
+    "prNotPendingHours": 25,
+    "prTitle": null,
+    "rebaseStalePrs": null,
+    "recreateClosed": false,
+    "requiredStatusChecks": Array [],
+    "reviewers": Array [],
+    "schedule": Array [],
+    "semanticCommitScope": "deps",
+    "semanticCommitType": "chore",
+    "semanticCommits": null,
+    "statusCheckVerify": false,
+    "timezone": null,
+    "unpublishSafe": false,
+    "updateLockFiles": true,
+    "updateNotScheduled": true,
+    "warnings": Array [],
+    "yarnrc": null,
+  },
+  Object {
+    "assignees": Array [],
+    "automerge": false,
+    "automergeComment": "automergeComment",
+    "automergeType": "pr",
+    "branchName": "{{{branchPrefix}}}{{{managerBranchPrefix}}}{{{branchTopic}}}",
+    "branchPrefix": "renovate/",
+    "branchTopic": "lock-file-maintenance",
+    "bumpVersion": null,
+    "commitBody": null,
+    "commitMessage": "{{{commitMessagePrefix}}} {{{commitMessageAction}}} {{{commitMessageTopic}}} {{{commitMessageExtra}}} {{{commitMessageSuffix}}}",
+    "commitMessageAction": "Lock file maintenance",
+    "commitMessageExtra": null,
+    "commitMessagePrefix": null,
+    "commitMessageTopic": null,
+    "copyLocalLibs": false,
+    "errors": Array [],
+    "gitAuthor": null,
+    "gitPrivateKey": null,
+    "group": Object {
+      "branchTopic": "{{{groupSlug}}}",
+      "commitMessageTopic": "{{{groupName}}}",
+      "prBody": "This Pull Request renovates the package group \\"{{{groupName}}}\\".\\n\\n{{#if schedule}}\\n**Note**: This PR was created on a configured schedule (\\"{{{schedule}}}\\"{{#if timezone}} in timezone \`{{{timezone}}}\`{{/if}}) and will not receive updates outside those times.\\n{{/if}}\\n\\n{{#each upgrades as |upgrade|}}\\n-   {{#if repositoryUrl}}[{{{upgrade.depName}}}]({{upgrade.repositoryUrl}}){{else}}\`{{{depName}}}\`{{/if}}{{#if depType}} (\`{{{depType}}}\`){{/if}}: from \`{{{upgrade.currentVersion}}}\` to \`{{{upgrade.newVersion}}}\`\\n{{/each}}\\n\\n{{#if hasReleaseNotes}}\\n# Release Notes\\n{{#each upgrades as |upgrade|}}\\n{{#if upgrade.hasReleaseNotes}}\\n<details>\\n<summary>{{upgrade.githubName}}</summary>\\n\\n{{#each upgrade.releases as |release|}}\\n{{#if release.releaseNotes}}\\n### [\`v{{{release.version}}}\`]({{{release.releaseNotes.url}}})\\n{{#if release.compare.url}}\\n[Compare Source]({{release.compare.url}})\\n{{/if}}\\n{{{release.releaseNotes.body}}}\\n\\n---\\n\\n{{/if}}\\n{{/each}}\\n\\n</details>\\n{{/if}}\\n{{/each}}\\n{{/if}}\\n\\n{{#if isPin}}\\n**Important**: Renovate will wait until you have merged this Pin request before creating PRs for any *upgrades*. If you do not wish to pin anything, please update your config accordingly instead of leaving this PR open.\\n{{/if}}\\n\\n{{#if hasErrors}}\\n\\n---\\n\\n# Errors\\n\\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\\n\\n{{#each errors as |error|}}\\n-   \`{{error.depName}}\`: {{error.message}}\\n{{/each}}\\n{{/if}}\\n\\n{{#if hasWarnings}}\\n\\n---\\n\\n# Warnings\\n\\nPlease make sure the following warnings are safe to ignore:\\n\\n{{#each warnings as |warning|}}\\n-   \`{{warning.depName}}\`: {{warning.message}}\\n{{/each}}\\n{{/if}}",
+    },
+    "groupName": null,
+    "groupSlug": null,
+    "labels": Array [],
+    "lazyGrouping": true,
+    "lockFileMaintenance": Object {
+      "branchTopic": "lock-file-maintenance",
+      "commitMessageAction": "Lock file maintenance",
+      "commitMessageExtra": null,
+      "commitMessageTopic": null,
+      "enabled": true,
+      "groupName": null,
+      "prBody": "This Pull Request updates \`package.json\` lock files to use the latest dependency versions.\\n\\n{{#if schedule}}\\n**Note**: This PR was created on a configured schedule (\\"{{{schedule}}}\\"{{#if timezone}} in timezone \`{{{timezone}}}\`{{/if}}) and will not receive updates outside those times.\\n{{/if}}\\n\\n{{#if hasErrors}}\\n\\n---\\n\\n# Errors\\n\\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\\n\\n{{#each errors as |error|}}\\n-   \`{{error.depName}}\`: {{error.message}}\\n{{/each}}\\n{{/if}}\\n\\n{{#if hasWarnings}}\\n\\n---\\n\\n# Warnings\\n\\nPlease make sure the following warnings are safe to ignore:\\n\\n{{#each warnings as |warning|}}\\n-   \`{{warning.depName}}\`: {{warning.message}}\\n{{/each}}\\n{{/if}}",
+      "rebaseStalePrs": true,
+      "recreateClosed": true,
+      "schedule": Array [
+        "before 5am on monday",
+      ],
+    },
+    "manager": "npm",
+    "managerBranchPrefix": "",
+    "npmToken": null,
+    "npmrc": null,
+    "prBody": "This Pull Request updates \`package.json\` lock files to use the latest dependency versions.\\n\\n{{#if schedule}}\\n**Note**: This PR was created on a configured schedule (\\"{{{schedule}}}\\"{{#if timezone}} in timezone \`{{{timezone}}}\`{{/if}}) and will not receive updates outside those times.\\n{{/if}}\\n\\n{{#if hasErrors}}\\n\\n---\\n\\n# Errors\\n\\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\\n\\n{{#each errors as |error|}}\\n-   \`{{error.depName}}\`: {{error.message}}\\n{{/each}}\\n{{/if}}\\n\\n{{#if hasWarnings}}\\n\\n---\\n\\n# Warnings\\n\\nPlease make sure the following warnings are safe to ignore:\\n\\n{{#each warnings as |warning|}}\\n-   \`{{warning.depName}}\`: {{warning.message}}\\n{{/each}}\\n{{/if}}",
+    "prConcurrentLimit": 0,
+    "prCreation": "immediate",
+    "prHourlyLimit": 0,
+    "prNotPendingHours": 25,
+    "prTitle": null,
+    "rebaseStalePrs": true,
+    "recreateClosed": true,
+    "requiredStatusChecks": Array [],
+    "reviewers": Array [],
+    "schedule": Array [
+      "before 5am on monday",
+    ],
+    "semanticCommitScope": "deps",
+    "semanticCommitType": "chore",
+    "semanticCommits": null,
+    "statusCheckVerify": false,
+    "timezone": null,
+    "type": "lockFileMaintenance",
+    "unpublishSafe": false,
+    "updateLockFiles": true,
+    "updateNotScheduled": true,
+    "warnings": Array [],
+    "yarnrc": null,
+  },
+]
+`;
diff --git a/test/workers/repository/updates/branchify.spec.js b/test/workers/repository/updates/branchify.spec.js
index 6a9d34d1c44a08c96b1e64c85b3f56fcfdf2c7c4..563d3ecc1c1edd85d9744a281775912dd7425893 100644
--- a/test/workers/repository/updates/branchify.spec.js
+++ b/test/workers/repository/updates/branchify.spec.js
@@ -9,29 +9,34 @@ beforeEach(() => {
 const {
   branchifyUpgrades,
 } = require('../../../../lib/workers/repository/updates/branchify');
+const {
+  flattenUpdates,
+} = require('../../../../lib/workers/repository/updates/flatten');
+
+jest.mock('../../../../lib/workers/repository/updates/flatten');
 
 describe('workers/repository/updates/branchify', () => {
   describe('branchifyUpgrades()', () => {
     it('returns empty', async () => {
-      config.upgrades = [];
+      flattenUpdates.mockReturnValueOnce([]);
       const res = await branchifyUpgrades(config);
       expect(res.branches).toEqual([]);
     });
     it('returns one branch if one input', async () => {
-      config.upgrades = [
+      flattenUpdates.mockReturnValueOnce([
         {
           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);
     });
     it('does not group if different compiled branch names', async () => {
-      config.upgrades = [
+      flattenUpdates.mockReturnValueOnce([
         {
           depName: 'foo',
           branchName: 'foo-{{version}}',
@@ -50,12 +55,12 @@ describe('workers/repository/updates/branchify', () => {
           version: '1.1.0',
           prTitle: 'some-title',
         },
-      ];
+      ]);
       const res = await branchifyUpgrades(config);
       expect(Object.keys(res.branches).length).toBe(3);
     });
     it('groups if same compiled branch names', async () => {
-      config.upgrades = [
+      flattenUpdates.mockReturnValueOnce([
         {
           depName: 'foo',
           branchName: 'foo',
@@ -74,12 +79,12 @@ describe('workers/repository/updates/branchify', () => {
           version: '1.1.0',
           prTitle: 'some-title',
         },
-      ];
+      ]);
       const res = await branchifyUpgrades(config);
       expect(Object.keys(res.branches).length).toBe(2);
     });
     it('groups if same compiled group name', async () => {
-      config.upgrades = [
+      flattenUpdates.mockReturnValueOnce([
         {
           depName: 'foo',
           branchName: 'foo',
@@ -102,12 +107,12 @@ describe('workers/repository/updates/branchify', () => {
           groupName: 'My Group',
           group: { branchName: 'renovate/my-group' },
         },
-      ];
+      ]);
       const res = await branchifyUpgrades(config);
       expect(Object.keys(res.branches).length).toBe(2);
     });
     it('mixes errors and warnings', async () => {
-      config.upgrades = [
+      flattenUpdates.mockReturnValueOnce([
         {
           type: 'error',
         },
@@ -127,7 +132,7 @@ describe('workers/repository/updates/branchify', () => {
           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);
@@ -193,7 +198,9 @@ describe('workers/repository/updates/branchify', () => {
           expectedBranchName: 'renovate/bad-branch-name9',
         },
       ];
-      config.upgrades = fixtures.map(({ upgrade }) => upgrade);
+      flattenUpdates.mockReturnValueOnce(
+        fixtures.map(({ upgrade }) => upgrade)
+      );
 
       (await branchifyUpgrades(config)).branches.forEach(
         ({ branchName }, index) => {
diff --git a/test/workers/repository/updates/determine.spec.js b/test/workers/repository/updates/determine.spec.js
deleted file mode 100644
index 73d0fe58a3c77a155d788335969c6375c4e02fbf..0000000000000000000000000000000000000000
--- a/test/workers/repository/updates/determine.spec.js
+++ /dev/null
@@ -1,67 +0,0 @@
-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',
-          manager: 'docker',
-        },
-        {
-          packageFile: 'backend/package.json',
-          manager: 'npm',
-        },
-        {
-          packageFile: 'frontend/package.js',
-          manager: 'meteor',
-        },
-        {
-          packageFile: '.travis.yml',
-          manager: 'travis',
-        },
-        {
-          packageFile: 'WORKSPACE',
-          manager: 'bazel',
-        },
-      ];
-      packageFileWorker.renovatePackageFile.mockReturnValueOnce([
-        { depName: 'a' },
-      ]);
-      packageFileWorker.renovatePackageFile.mockReturnValueOnce([
-        { depName: 'b' },
-        { depName: 'c' },
-      ]);
-      packageFileWorker.renovatePackageFile.mockReturnValueOnce([{ foo: 'd' }]);
-      packageFileWorker.renovatePackageFile.mockReturnValueOnce([{ foo: 'e' }]);
-      packageFileWorker.renovatePackageFile.mockReturnValueOnce([{ bar: 'f' }]);
-      const res = await determineRepoUpgrades(config);
-      expect(res.upgrades).toHaveLength(6);
-    });
-  });
-});
diff --git a/test/workers/repository/updates/flatten.spec.js b/test/workers/repository/updates/flatten.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..1553d5c07d4207daa5bbde66314dfb839b528bad
--- /dev/null
+++ b/test/workers/repository/updates/flatten.spec.js
@@ -0,0 +1,32 @@
+const {
+  flattenUpdates,
+} = require('../../../../lib/workers/repository/updates/flatten');
+
+let config;
+beforeEach(() => {
+  jest.resetAllMocks();
+  config = { ...require('../../../_fixtures/config') };
+  config.errors = [];
+  config.warnings = [];
+});
+
+describe('workers/repository/updates/flatten', () => {
+  describe('flattenUpdates()', () => {
+    it('flattens', async () => {
+      config.lockFileMaintenance.enabled = true;
+      const packageFiles = {
+        npm: [
+          {
+            packageFile: 'package.json ',
+            deps: [
+              { depName: '@org/a', updates: [{ newVersion: '1.0.0' }] },
+              { updates: [{ newVersion: '2.0.0' }] },
+            ],
+          },
+        ],
+      };
+      const res = await flattenUpdates(config, packageFiles);
+      expect(res).toMatchSnapshot();
+    });
+  });
+});
diff --git a/test/workers/repository/updates/index.spec.js b/test/workers/repository/updates/index.spec.js
deleted file mode 100644
index f8c3a784296741cd1fd6b65eb85efebe44f7f5b8..0000000000000000000000000000000000000000
--- a/test/workers/repository/updates/index.spec.js
+++ /dev/null
@@ -1,20 +0,0 @@
-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');
-    });
-  });
-});