diff --git a/.gitignore b/.gitignore
index 9ec18cd2840875cf3b67e343e6c500ab38917c94..0259fac66b0b7548e2be10041078da478411c0e5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@
 /coverage
 /dist
 .DS_Store
+*.log
diff --git a/lib/api/github.js b/lib/api/github.js
index f9e762ca77569d3b8caed4361bfae074de47fbbf..29118debb8f684f82f93fb8dc4fed2ac667dd574 100644
--- a/lib/api/github.js
+++ b/lib/api/github.js
@@ -1,4 +1,4 @@
-const logger = require('../logger');
+let logger = require('../helpers/logger');
 const ghGot = require('gh-got');
 
 const config = {};
@@ -123,8 +123,11 @@ async function getRepos(token, endpoint) {
 }
 
 // Initialize GitHub by getting base branch and SHA
-async function initRepo(repoName, token, endpoint) {
-  logger.debug(`initRepo(${repoName})`);
+async function initRepo(repoName, token, endpoint, repoLogger) {
+  logger.debug(`initRepo(${JSON.stringify(repoName)})`);
+  if (repoLogger) {
+    logger = repoLogger;
+  }
   if (token) {
     process.env.GITHUB_TOKEN = token;
   } else if (!process.env.GITHUB_TOKEN) {
diff --git a/lib/api/gitlab.js b/lib/api/gitlab.js
index 94c64213ab751b00c8fdbd9dbf87cb6c0da0dce5..5c4ffb71db5b368f96770aaa0e145c9105e4d97f 100644
--- a/lib/api/gitlab.js
+++ b/lib/api/gitlab.js
@@ -1,4 +1,4 @@
-const logger = require('../logger');
+let logger = require('../helpers/logger');
 const glGot = require('gl-got');
 
 const config = {};
@@ -51,7 +51,10 @@ async function getRepos(token, endpoint) {
 }
 
 // Initialize GitLab by getting base branch
-async function initRepo(repoName, token, endpoint) {
+async function initRepo(repoName, token, endpoint, repoLogger) {
+  if (repoLogger) {
+    logger = repoLogger;
+  }
   logger.debug(`initRepo(${repoName})`);
   if (token) {
     process.env.GITLAB_TOKEN = token;
diff --git a/lib/api/npm.js b/lib/api/npm.js
index fd707874ccb09d72953cad7c5eeea0014b22e841..e4d476a435eccaa3dbc48da5ba27406df595d97e 100644
--- a/lib/api/npm.js
+++ b/lib/api/npm.js
@@ -4,7 +4,7 @@ const got = require('got');
 const url = require('url');
 const registryUrl = require('registry-url');
 const registryAuthToken = require('registry-auth-token');
-const logger = require('../logger');
+const logger = require('../helpers/logger');
 
 module.exports = {
   setNpmrc,
diff --git a/lib/config/file.js b/lib/config/file.js
index d844daa09992ee106d67b6026107482b1e97a9fb..066a2847ac69cbad41fa48fefdfd5d9f951432ff 100644
--- a/lib/config/file.js
+++ b/lib/config/file.js
@@ -1,4 +1,4 @@
-const logger = require('../logger');
+const logger = require('../helpers/logger');
 const path = require('path');
 
 module.exports = {
diff --git a/lib/config/index.js b/lib/config/index.js
index 4e182faefbd7a31b6a2a66309fee8b3652bee98a..341091161daca54f8cad6b20f266e17448e87160 100644
--- a/lib/config/index.js
+++ b/lib/config/index.js
@@ -1,5 +1,4 @@
-const logger = require('../logger');
-const stringify = require('json-stringify-pretty-compact');
+const logger = require('../helpers/logger');
 const githubApi = require('../api/github');
 const gitlabApi = require('../api/gitlab');
 
@@ -10,12 +9,9 @@ const envParser = require('./env');
 
 const githubAppHelper = require('../helpers/github-app');
 
-let config = null;
-
 module.exports = {
   parseConfigs,
-  getCascadedConfig,
-  getRepositories,
+  getRepoConfig,
 };
 
 async function parseConfigs(env, argv) {
@@ -27,7 +23,13 @@ async function parseConfigs(env, argv) {
   const cliConfig = cliParser.getConfig(argv);
   const envConfig = envParser.getConfig(env);
 
-  config = Object.assign({}, defaultConfig, fileConfig, envConfig, cliConfig);
+  const config = Object.assign(
+    {},
+    defaultConfig,
+    fileConfig,
+    envConfig,
+    cliConfig
+  );
 
   // Set log level
   logger.levels('stdout', config.logLevel);
@@ -44,13 +46,13 @@ async function parseConfigs(env, argv) {
     });
   }
 
-  logger.debug(`Default config = ${redact(defaultConfig)}`);
-  logger.debug(`File config = ${redact(fileConfig)}`);
-  logger.debug(`CLI config: ${redact(cliConfig)}`);
-  logger.debug(`Env config: ${redact(envConfig)}`);
+  logger.debug({ config: defaultConfig }, 'Default config');
+  logger.debug({ config: fileConfig }, 'File config');
+  logger.debug({ config: cliConfig }, 'CLI config');
+  logger.debug({ config: envConfig }, 'Env config');
 
   // Get global config
-  logger.debug(`raw config=${redact(config)}`);
+  logger.debug({ config }, 'Raw config');
 
   // Check platforms and tokens
   if (config.platform === 'github') {
@@ -74,8 +76,7 @@ async function parseConfigs(env, argv) {
     }
     config.repositories = await githubAppHelper.getRepositories(config);
     logger.info(`Found ${config.repositories.length} repositories installed`);
-    delete config.githubAppKey;
-    logger.debug(`GitHub App config: ${JSON.stringify(config)}`);
+    logger.debug({ config }, 'GitHub App config');
   } else if (config.autodiscover) {
     // Autodiscover list of repositories
     if (config.platform === 'github') {
@@ -96,7 +97,7 @@ async function parseConfigs(env, argv) {
       logger.info(
         'The account associated with your token does not have access to any repos'
       );
-      return;
+      return config;
     }
   } else if (!config.repositories || config.repositories.length === 0) {
     // We need at least one repository defined
@@ -105,78 +106,20 @@ async function parseConfigs(env, argv) {
     );
   }
 
-  // Configure each repository
-  config.repositories = config.repositories.map(item => {
-    // Convert any repository strings to objects
-    const repo = typeof item === 'string' ? { repository: item } : item;
-
-    // copy across some fields from the base config if not present
-    repo.token = repo.token || config.token;
-    repo.platform = repo.platform || config.platform;
-    repo.onboarding = repo.onboarding || config.onboarding;
-    repo.endpoint = repo.endpoint || config.endpoint;
-
-    // Set default packageFiles
-    if (!repo.packageFiles || !repo.packageFiles.length) {
-      repo.packageFiles = config.packageFiles;
-    }
-
-    // Expand packageFile format
-    repo.packageFiles = repo.packageFiles.map(packageFile => {
-      if (typeof packageFile === 'string') {
-        return { fileName: packageFile };
-      }
-      return packageFile;
-    });
-
-    return repo;
-  });
-
   // Print config
-  logger.debug(`config=${redact(config)}`);
+  logger.debug({ config }, 'Global config');
   // Remove log file entries
   delete config.logFile;
   delete config.logFileLevel;
+  return config;
 }
 
-function getCascadedConfig(repo, packageFile) {
-  const cascadedConfig = Object.assign({}, config, repo, packageFile);
-  // Remove unnecessary fields
-  delete cascadedConfig.repositories;
-  delete cascadedConfig.repository;
-  delete cascadedConfig.fileName;
-  return cascadedConfig;
-}
-
-function getRepositories() {
-  return config.repositories;
-}
-
-function redact(inputConfig) {
-  const redactedConfig = Object.assign({}, inputConfig);
-  if (redactedConfig.token) {
-    redactedConfig.token = `${redactedConfig.token.substr(0, 4)}${new Array(
-      redactedConfig.token.length - 3
-    ).join('*')}`;
-  }
-  if (redactedConfig.githubAppKey) {
-    redactedConfig.githubAppKey = '***REDACTED***';
-  }
-  if (inputConfig.repositories) {
-    redactedConfig.repositories = [];
-    for (const repository of inputConfig.repositories) {
-      if (typeof repository !== 'string') {
-        const redactedRepo = Object.assign({}, repository);
-        if (redactedRepo.token) {
-          redactedRepo.token = `${redactedRepo.token.substr(0, 4)}${new Array(
-            redactedRepo.token.length - 3
-          ).join('*')}`;
-        }
-        redactedConfig.repositories.push(redactedRepo);
-      } else {
-        redactedConfig.repositories.push(repository);
-      }
-    }
+function getRepoConfig(config, index) {
+  let repository = config.repositories[index];
+  if (typeof repository === 'string') {
+    repository = { repository };
   }
-  return stringify(redactedConfig);
+  const returnConfig = Object.assign({}, config, repository);
+  delete returnConfig.repositories;
+  return returnConfig;
 }
diff --git a/lib/helpers/changelog.js b/lib/helpers/changelog.js
index 03ae98d55c90e8f8ce1d63b2203028b8cce65518..85b1487771402d7b7d53e39af91328848a840fbe 100644
--- a/lib/helpers/changelog.js
+++ b/lib/helpers/changelog.js
@@ -1,4 +1,3 @@
-const logger = require('../logger');
 const changelog = require('changelog');
 
 module.exports = {
@@ -7,7 +6,7 @@ module.exports = {
   getChangeLog,
 };
 
-async function getChangeLogJSON(depName, fromVersion, newVersion) {
+async function getChangeLogJSON(depName, fromVersion, newVersion, logger) {
   logger.debug(`getChangeLogJSON(${depName}, ${fromVersion}, ${newVersion})`);
   if (!fromVersion || fromVersion === newVersion) {
     return null;
@@ -35,7 +34,12 @@ function getMarkdown(changelogJSON) {
 }
 
 // Get Changelog
-async function getChangeLog(depName, fromVersion, newVersion) {
-  const logJSON = await getChangeLogJSON(depName, fromVersion, newVersion);
+async function getChangeLog(depName, fromVersion, newVersion, logger) {
+  const logJSON = await getChangeLogJSON(
+    depName,
+    fromVersion,
+    newVersion,
+    logger
+  );
   return getMarkdown(logJSON);
 }
diff --git a/lib/helpers/cli.js b/lib/helpers/cli.js
deleted file mode 100644
index 4e6691634751bde13345e31fd1f3f8eddef03da1..0000000000000000000000000000000000000000
--- a/lib/helpers/cli.js
+++ /dev/null
@@ -1,36 +0,0 @@
-// Code derived from https://github.com/hadfieldn/node-bunyan-RenovateStream and heavily edited
-// Neither fork nor original repo appear to be maintained
-
-const Stream = require('stream').Stream;
-const util = require('util');
-const chalk = require('chalk');
-
-const levels = {
-  10: chalk.gray('TRACE'),
-  20: chalk.blue('DEBUG'),
-  30: chalk.green(' INFO'),
-  40: chalk.magenta(' WARN'),
-  50: chalk.red('ERROR'),
-  60: chalk.bgRed('FATAL'),
-};
-
-function RenovateStream() {
-  this.readable = true;
-  this.writable = true;
-  Stream.call(this);
-
-  this.formatRecord = function formatRecord(rec) {
-    const level = levels[rec.level];
-    const msg = `${rec.msg.split(/\r?\n/).join('\n       ')}`;
-    return util.format('%s: %s\n', level, msg);
-  };
-}
-
-util.inherits(RenovateStream, Stream);
-
-RenovateStream.prototype.write = function write(data) {
-  this.emit('data', this.formatRecord(data));
-  return true;
-};
-
-module.exports = RenovateStream;
diff --git a/lib/helpers/github-app.js b/lib/helpers/github-app.js
index 29d40f9fa2fff4ea977d8425cc2dc1cf988d9afd..8e115e54a098a8fa6304d4cc510407d6d2569593 100644
--- a/lib/helpers/github-app.js
+++ b/lib/helpers/github-app.js
@@ -1,5 +1,5 @@
 const jwt = require('jsonwebtoken');
-const logger = require('../logger');
+const logger = require('../helpers/logger');
 const ghApi = require('../api/github');
 
 module.exports = {
diff --git a/lib/helpers/logger/config-serializer.js b/lib/helpers/logger/config-serializer.js
new file mode 100644
index 0000000000000000000000000000000000000000..afc3c76d67975503d087d94ea8d358705201bcc0
--- /dev/null
+++ b/lib/helpers/logger/config-serializer.js
@@ -0,0 +1,17 @@
+const traverse = require('traverse');
+
+module.exports = configSerializer;
+
+function configSerializer(config) {
+  const redactedFields = ['token', 'githubAppKey'];
+  const functionFields = ['api', 'logger'];
+  // eslint-disable-next-line array-callback-return
+  return traverse(config).map(function scrub(val) {
+    if (val && redactedFields.indexOf(this.key) !== -1) {
+      this.update('***********');
+    }
+    if (val && functionFields.indexOf(this.key) !== -1) {
+      this.update('[Function]');
+    }
+  });
+}
diff --git a/lib/helpers/logger/index.js b/lib/helpers/logger/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..5a191247259decba7472a306bd66ea09a38a664c
--- /dev/null
+++ b/lib/helpers/logger/index.js
@@ -0,0 +1,23 @@
+const bunyan = require('bunyan');
+const PrettyStdout = require('./pretty-stdout').RenovateStream;
+const configSerializer = require('./config-serializer');
+
+const prettyStdOut = new PrettyStdout();
+prettyStdOut.pipe(process.stdout);
+
+const logger = bunyan.createLogger({
+  name: 'renovate',
+  serializers: {
+    config: configSerializer,
+  },
+  streams: [
+    {
+      name: 'stdout',
+      level: process.env.LOG_LEVEL || 'info',
+      type: 'raw',
+      stream: prettyStdOut,
+    },
+  ],
+});
+
+module.exports = logger;
diff --git a/lib/helpers/logger/pretty-stdout.js b/lib/helpers/logger/pretty-stdout.js
new file mode 100644
index 0000000000000000000000000000000000000000..9679d1bb2d722b611844e76798a17a891f4c970d
--- /dev/null
+++ b/lib/helpers/logger/pretty-stdout.js
@@ -0,0 +1,74 @@
+// Code derived from https://github.com/hadfieldn/node-bunyan-RenovateStream and heavily edited
+// Neither fork nor original repo appear to be maintained
+
+const Stream = require('stream').Stream;
+const util = require('util');
+const chalk = require('chalk');
+const stringify = require('json-stringify-pretty-compact');
+
+const levels = {
+  10: chalk.gray('TRACE'),
+  20: chalk.blue('DEBUG'),
+  30: chalk.green(' INFO'),
+  40: chalk.magenta(' WARN'),
+  50: chalk.red('ERROR'),
+  60: chalk.bgRed('FATAL'),
+};
+
+function indent(str, leading = false) {
+  const prefix = leading ? '       ' : '';
+  return prefix + str.split(/\r?\n/).join('\n       ');
+}
+
+function getMeta(rec) {
+  if (!rec) {
+    return '';
+  }
+  const metaFields = [
+    'repository',
+    'packageFile',
+    'dependency',
+    'branch',
+  ].filter(elem => rec[elem]);
+  if (!metaFields.length) {
+    return '';
+  }
+  const metaStr = metaFields.map(field => `${field}=${rec[field]}`).join(', ');
+  return chalk.gray(` (${metaStr})`);
+}
+
+function getDetails(rec) {
+  if (!rec || !rec.config) {
+    return '';
+  }
+  return `${indent(stringify(rec.config), true)}\n`;
+}
+
+function formatRecord(rec) {
+  const level = levels[rec.level];
+  const msg = `${indent(rec.msg)}`;
+  const meta = getMeta(rec);
+  const details = getDetails(rec);
+  return util.format('%s: %s%s\n%s', level, msg, meta, details);
+}
+
+function RenovateStream() {
+  this.readable = true;
+  this.writable = true;
+  Stream.call(this);
+}
+
+util.inherits(RenovateStream, Stream);
+
+RenovateStream.prototype.write = function write(data) {
+  this.emit('data', formatRecord(data));
+  return true;
+};
+
+module.exports = {
+  indent,
+  getMeta,
+  getDetails,
+  formatRecord,
+  RenovateStream,
+};
diff --git a/lib/helpers/npm.js b/lib/helpers/npm.js
index 162c357fee9602158ce6f439daca5d6abe91fb85..3ef62334bff10d017556c798ae755dfe873e1245 100644
--- a/lib/helpers/npm.js
+++ b/lib/helpers/npm.js
@@ -1,4 +1,4 @@
-const logger = require('../logger');
+const logger = require('../helpers/logger');
 const fs = require('fs');
 const cp = require('child_process');
 const tmp = require('tmp');
diff --git a/lib/helpers/package-json.js b/lib/helpers/package-json.js
index d71bc90bac8b6791376b995342bb47bdf9ed8a44..a4ad33dc5eb929b6405782c77c56748e55ff4ff3 100644
--- a/lib/helpers/package-json.js
+++ b/lib/helpers/package-json.js
@@ -1,4 +1,3 @@
-const logger = require('../logger');
 const _ = require('lodash');
 
 module.exports = {
@@ -24,7 +23,7 @@ function extractDependencies(packageJson, sections) {
   }, []);
 }
 
-function setNewValue(currentFileContent, depType, depName, newVersion) {
+function setNewValue(currentFileContent, depType, depName, newVersion, logger) {
   logger.debug(`setNewValue: ${depType}.${depName} = ${newVersion}`);
   const parsedContents = JSON.parse(currentFileContent);
   // Save the old version
@@ -52,7 +51,8 @@ function setNewValue(currentFileContent, depType, depName, newVersion) {
         currentFileContent,
         searchIndex,
         searchString,
-        newString
+        newString,
+        logger
       );
       // Compare the parsed JSON structure of old and new
       if (_.isEqual(parsedContents, JSON.parse(testContent))) {
@@ -74,7 +74,7 @@ function matchAt(content, index, match) {
 }
 
 // Replace oldString with newString at location index of content
-function replaceAt(content, index, oldString, newString) {
+function replaceAt(content, index, oldString, newString, logger) {
   logger.debug(`Replacing ${oldString} with ${newString} at index ${index}`);
   return (
     content.substr(0, index) +
diff --git a/lib/helpers/versions.js b/lib/helpers/versions.js
index c0ae33244ace0c6aaa4fdde513d2fa573db04b2d..357ed9512313ddc86aa7e107c8173840cd66ef04 100644
--- a/lib/helpers/versions.js
+++ b/lib/helpers/versions.js
@@ -1,4 +1,4 @@
-const logger = require('../logger');
+const logger = require('../helpers/logger');
 const semver = require('semver');
 const stable = require('semver-stable');
 const _ = require('lodash');
diff --git a/lib/helpers/yarn.js b/lib/helpers/yarn.js
index 8b02ad23260065916166ecec050167f02a1134c0..2a52926cb3977c736308485c5f7e84f6b438b658 100644
--- a/lib/helpers/yarn.js
+++ b/lib/helpers/yarn.js
@@ -1,4 +1,4 @@
-const logger = require('../logger');
+const logger = require('../helpers/logger');
 const fs = require('fs');
 const cp = require('child_process');
 const tmp = require('tmp');
diff --git a/lib/index.js b/lib/index.js
deleted file mode 100644
index 6822a0d8afd04f72ed4c4a5fee368b134173139d..0000000000000000000000000000000000000000
--- a/lib/index.js
+++ /dev/null
@@ -1,187 +0,0 @@
-const stringify = require('json-stringify-pretty-compact');
-const logger = require('./logger');
-const configParser = require('./config');
-const githubApi = require('./api/github');
-const gitlabApi = require('./api/gitlab');
-const npmApi = require('./api/npm');
-const defaultsParser = require('./config/defaults');
-const ini = require('ini');
-
-// Require main source
-const worker = require('./worker');
-
-module.exports = {
-  start,
-  processRepo,
-  setNpmrc,
-};
-
-// This will be github or others
-let api;
-
-async function start() {
-  // Parse config
-  try {
-    await configParser.parseConfigs(process.env, process.argv);
-    // Iterate through repositories sequentially
-    for (const repo of configParser.getRepositories()) {
-      await processRepo(repo);
-    }
-    logger.info('Renovate finished');
-  } catch (error) {
-    logger.error(error.message);
-  }
-}
-
-// Queue package files in sequence within a repo
-async function processRepo(repo) {
-  logger.info(`Processing repository ${repo.repository}`);
-  // Take a copy of the config, as we will modify it
-  const config = Object.assign({}, repo);
-  if (config.platform === 'github') {
-    api = githubApi;
-  } else if (config.platform === 'gitlab') {
-    api = gitlabApi;
-  } else {
-    logger.error(
-      `Unknown platform ${config.platform} for repository ${repo.repository}`
-    );
-    return;
-  }
-  logger.debug(`Repository config:\n${stringify(config)}`);
-  try {
-    // Initialize repo
-    await api.initRepo(config.repository, config.token, config.endpoint);
-    await mergeRenovateJson(config);
-    const isConfigured = await checkIfConfigured(config);
-    if (isConfigured === false) {
-      return;
-    }
-    await setNpmrc();
-    await findPackageFiles(config);
-    const upgrades = await getAllRepoUpgrades(config);
-    await worker.processUpgrades(upgrades);
-  } catch (error) {
-    throw error;
-  }
-}
-
-// Check for config in `renovate.json`
-async function setNpmrc() {
-  try {
-    let npmrc = null;
-    const npmrcContent = await api.getFileContent('.npmrc');
-    if (npmrcContent) {
-      logger.debug('Found .npmrc file in repository');
-      npmrc = ini.parse(npmrcContent);
-    }
-    npmApi.setNpmrc(npmrc);
-  } catch (err) {
-    logger.error('Failed to set .npmrc');
-  }
-}
-
-// Check for config in `renovate.json`
-async function mergeRenovateJson(config) {
-  const renovateJson = await api.getFileJson('renovate.json');
-  if (renovateJson) {
-    logger.debug(`renovate.json config: ${stringify(renovateJson)}`);
-    Object.assign(config, renovateJson, { repoConfigured: true });
-  } else {
-    logger.debug('No renovate.json found');
-  }
-}
-
-async function checkIfConfigured(config) {
-  logger.debug('Checking if repo is configured');
-  // Check if repository is configured
-  if (config.repoConfigured || config.onboarding === false) {
-    logger.debug('Repo is configured or onboarding disabled');
-    return true;
-  }
-  const pr = await api.findPr('renovate/configure', 'Configure Renovate');
-  if (pr) {
-    if (pr.isClosed) {
-      logger.debug('Closed Configure Renovate PR found - continuing');
-      return true;
-    }
-    // PR exists but hasn't been closed yet
-    logger.error(`Close PR #${pr.displayNumber} before continuing`);
-    return false;
-  }
-  await configureRepository(config);
-  return false;
-}
-
-async function configureRepository(config) {
-  const defaultConfig = defaultsParser.getConfig();
-  delete defaultConfig.onboarding;
-  delete defaultConfig.platform;
-  delete defaultConfig.endpoint;
-  delete defaultConfig.token;
-  delete defaultConfig.autodiscover;
-  delete defaultConfig.githubAppId;
-  delete defaultConfig.githubAppKey;
-  delete defaultConfig.repositories;
-  delete defaultConfig.logLevel;
-  let prBody = `Welcome to [Renovate](https://keylocation.sg/our-tech/renovate)! Once you close this Pull Request, we will begin keeping your dependencies up-to-date via automated Pull Requests.
-
-The [Configuration](https://github.com/singapore/renovate/blob/master/docs/configuration.md) and [Configuration FAQ](https://github.com/singapore/renovate/blob/master/docs/faq.md) documents should be helpful.
-
-#### Important!
-
-You do not need to *merge* this Pull Request - renovate will begin even if it's closed *unmerged*.
-In fact, you only need to add a \`renovate.json\` file to your repository if you wish to override any default settings. The file is included as part of this PR only in case you wish to change default settings before you start.
-If the default settings are all suitable for you, simply close this Pull Request unmerged and your first renovation will begin the next time the program is run.`;
-
-  if (config.platform === 'gitlab') {
-    defaultConfig.platform = 'gitlab';
-    prBody = prBody.replace(/Pull Request/g, 'Merge Request');
-  }
-  const defaultConfigString = `${stringify(defaultConfig)}\n`;
-  await api.commitFilesToBranch(
-    'renovate/configure',
-    [
-      {
-        name: 'renovate.json',
-        contents: defaultConfigString,
-      },
-    ],
-    'Add renovate.json'
-  );
-  const pr = await api.createPr(
-    'renovate/configure',
-    'Configure Renovate',
-    prBody
-  );
-  logger.info(`Created ${pr.displayNumber} for configuration`);
-}
-
-// Ensure config contains packageFiles
-async function findPackageFiles(config) {
-  if (config.packageFiles.length === 0) {
-    // autodiscover filenames if none manually configured
-    const fileNames = await api.findFilePaths('package.json');
-    // Map to config structure
-    const packageFiles = fileNames.map(fileName => ({ fileName }));
-    Object.assign(config, { packageFiles });
-  }
-}
-
-async function getAllRepoUpgrades(repo) {
-  let upgrades = [];
-  for (let packageFile of repo.packageFiles) {
-    if (typeof packageFile === 'string') {
-      packageFile = { fileName: packageFile };
-    }
-    const cascadedConfig = configParser.getCascadedConfig(repo, packageFile);
-    upgrades = upgrades.concat(
-      await worker.processPackageFile(
-        repo.repository,
-        packageFile.fileName,
-        cascadedConfig
-      )
-    );
-  }
-  return upgrades;
-}
diff --git a/lib/logger.js b/lib/logger.js
deleted file mode 100644
index fd7ceabc75f8d581bde2429b9d20b55ffb71b990..0000000000000000000000000000000000000000
--- a/lib/logger.js
+++ /dev/null
@@ -1,19 +0,0 @@
-const bunyan = require('bunyan');
-const CliHelper = require('./helpers/cli');
-
-const cliHelper = new CliHelper();
-cliHelper.pipe(process.stdout);
-
-const logger = bunyan.createLogger({
-  name: 'myapp',
-  streams: [
-    {
-      name: 'stdout',
-      level: process.env.LOG_LEVEL || 'info',
-      type: 'raw',
-      stream: cliHelper,
-    },
-  ],
-});
-
-module.exports = logger;
diff --git a/lib/renovate.js b/lib/renovate.js
index 398a59b7ddf75c1dc654be551a3205dffa9a9761..6fb57b2856e32136cbaa8f4abbe2067e44c65c97 100644
--- a/lib/renovate.js
+++ b/lib/renovate.js
@@ -1,5 +1,5 @@
 #!/usr/bin/env node
 
-const renovate = require('./index');
+const renovateWorker = require('./workers/index');
 
-renovate.start();
+renovateWorker.start();
diff --git a/lib/workers/branch.js b/lib/workers/branch.js
index c03aec59dd996ea329ad260a2dc3d5a125539745..9a91e0e3b6f41ac1fcf3550f5f5b9c17df22075c 100644
--- a/lib/workers/branch.js
+++ b/lib/workers/branch.js
@@ -1,12 +1,14 @@
-const logger = require('../logger');
 const handlebars = require('handlebars');
 const packageJsonHelper = require('../helpers/package-json');
 const npmHelper = require('../helpers/npm');
 const yarnHelper = require('../helpers/yarn');
+const prWorker = require('./pr');
+let logger = require('../helpers/logger');
 
 module.exports = {
   getParentBranch,
   ensureBranch,
+  updateBranch,
 };
 
 async function getParentBranch(branchName, config) {
@@ -53,7 +55,7 @@ async function getParentBranch(branchName, config) {
 
 // Ensure branch exists with appropriate content
 async function ensureBranch(upgrades) {
-  logger.debug(`ensureBranch(${JSON.stringify(upgrades)})`);
+  logger.debug({ config: upgrades }, 'ensureBranch');
   // Use the first upgrade for all the templates
   const branchName = handlebars.compile(upgrades[0].branchName)(upgrades[0]);
   // parentBranch is the branch we will base off
@@ -93,7 +95,8 @@ async function ensureBranch(upgrades) {
         packageFiles[upgrade.packageFile],
         upgrade.depType,
         upgrade.depName,
-        upgrade.newVersion
+        upgrade.newVersion,
+        logger
       );
       if (packageFiles[upgrade.packageFile] === newContent) {
         logger.debug('packageFile content unchanged');
@@ -188,3 +191,48 @@ async function ensureBranch(upgrades) {
   // Return true as branch exists
   return true;
 }
+
+async function updateBranch(upgrades, parentLogger) {
+  const upgrade0 = upgrades[0];
+  // Use templates to generate strings
+  const branchName = handlebars.compile(upgrade0.branchName)(upgrade0);
+  const prTitle = handlebars.compile(upgrade0.prTitle)(upgrade0);
+
+  logger = parentLogger.child({
+    repository: upgrade0.repository,
+    branch: branchName,
+  });
+
+  const upgradeCount = upgrades.length === 1
+    ? '1 upgrade'
+    : `${upgrades.length} upgrades`;
+  logger.info(
+    `Branch '${branchName}' has ${upgradeCount}: ${upgrades.map(
+      upgrade => upgrade.depName
+    )}`
+  );
+
+  try {
+    if (
+      upgrade0.upgradeType !== 'maintainYarnLock' &&
+      upgrade0.groupName === null &&
+      !upgrade0.recreateClosed &&
+      (await upgrade0.api.checkForClosedPr(branchName, prTitle))
+    ) {
+      logger.info(
+        `Skipping ${branchName} upgrade as matching closed PR already existed`
+      );
+      return;
+    }
+    const branchCreated = await module.exports.ensureBranch(upgrades);
+    if (branchCreated) {
+      const pr = await prWorker.ensurePr(upgrades, logger);
+      if (pr) {
+        await prWorker.checkAutoMerge(pr, upgrade0, logger);
+      }
+    }
+  } catch (error) {
+    logger.error(`Error updating branch ${branchName}: ${error}`);
+    // Don't throw here - we don't want to stop the other renovations
+  }
+}
diff --git a/lib/workers/index.js b/lib/workers/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..6c3a3e8b277924b647a4090f29f0a7b9a9e7c2b4
--- /dev/null
+++ b/lib/workers/index.js
@@ -0,0 +1,25 @@
+const logger = require('../helpers/logger');
+const configParser = require('../config');
+const repositoryWorker = require('./repository');
+
+module.exports = {
+  start,
+};
+
+async function start() {
+  // Parse config
+  try {
+    logger.info('Renovate starting');
+    const config = await configParser.parseConfigs(process.env, process.argv);
+    // Iterate through repositories sequentially
+    for (let index = 0; index < config.repositories.length; index += 1) {
+      const repoConfig = configParser.getRepoConfig(config, index);
+      repoConfig.logger = logger;
+      await repositoryWorker.processRepo(repoConfig);
+    }
+    logger.info('Renovate finished');
+  } catch (err) {
+    logger.fatal(`Renovate fatal error: ${err.message}`);
+    logger.error(err);
+  }
+}
diff --git a/lib/worker.js b/lib/workers/package-file.js
similarity index 51%
rename from lib/worker.js
rename to lib/workers/package-file.js
index fa47ff8fe72105a9704abfe760d2e43193c2af53..712ecb028d76affb1aba9fa659594429945ed999 100644
--- a/lib/worker.js
+++ b/lib/workers/package-file.js
@@ -1,33 +1,29 @@
-const logger = require('./logger');
-const stringify = require('json-stringify-pretty-compact');
-const handlebars = require('handlebars');
-const versionsHelper = require('./helpers/versions');
-const packageJson = require('./helpers/package-json');
-const npmApi = require('./api/npm');
-const prWorker = require('./workers/pr');
-const branchWorker = require('./workers/branch');
+// API
+const npmApi = require('../api/npm');
+// Helpers
+const packageJson = require('../helpers/package-json');
+const versionsHelper = require('../helpers/versions');
 
-let config;
+let logger = require('../helpers/logger');
 
 module.exports = {
   processPackageFile,
-  findUpgrades,
-  processUpgrades,
-  updateBranch,
-  removeStandaloneBranches,
   assignDepConfigs,
+  findUpgrades,
   getDepTypeConfig,
 };
 
 // This function manages the queue per-package file
-async function processPackageFile(repoName, packageFile, packageConfig) {
-  // Initialize globals
-  config = Object.assign({}, packageConfig);
-  config.packageFile = packageFile;
+async function processPackageFile(config) {
+  // Initialize logger
+  logger = config.logger.child({
+    repository: config.repository,
+    packageFile: config.packageFile,
+  });
 
-  logger.info(`Processing package file ${repoName}:${packageFile}`);
+  logger.info(`Processing package file`);
 
-  const packageContent = await config.api.getFileJson(packageFile);
+  const packageContent = await config.api.getFileJson(config.packageFile);
 
   if (!packageContent) {
     logger.warn('No package.json content found - skipping');
@@ -37,7 +33,8 @@ async function processPackageFile(repoName, packageFile, packageConfig) {
   // Check for renovate config inside the package.json
   if (packageContent.renovate) {
     logger.debug(
-      `package.json>renovate config:\n${stringify(packageContent.renovate)}`
+      { config: packageContent.renovate },
+      'package.json>renovate config'
     );
     Object.assign(config, packageContent.renovate, { repoConfigured: true });
   }
@@ -63,9 +60,9 @@ async function processPackageFile(repoName, packageFile, packageConfig) {
   dependencies = dependencies.filter(
     dependency => config.ignoreDeps.indexOf(dependency.depName) === -1
   );
-  dependencies = assignDepConfigs(config, dependencies);
+  dependencies = module.exports.assignDepConfigs(config, dependencies);
   // Find all upgrades for remaining dependencies
-  const upgrades = await findUpgrades(dependencies);
+  const upgrades = await module.exports.findUpgrades(dependencies);
   // Process all upgrades sequentially
   if (config.maintainYarnLock) {
     const upgrade = Object.assign({}, config, {
@@ -109,6 +106,7 @@ function assignDepConfigs(inputConfig, deps) {
         }
       });
     }
+    // TODO: clean this up
     delete returnDep.config.depType;
     delete returnDep.config.depTypes;
     delete returnDep.config.enabled;
@@ -132,18 +130,6 @@ function assignDepConfigs(inputConfig, deps) {
   });
 }
 
-function getDepTypeConfig(depTypes, depTypeName) {
-  let depTypeConfig = {};
-  if (depTypes) {
-    depTypes.forEach(depType => {
-      if (typeof depType !== 'string' && depType.depType === depTypeName) {
-        depTypeConfig = depType;
-      }
-    });
-  }
-  return depTypeConfig;
-}
-
 async function findUpgrades(dependencies) {
   const allUpgrades = [];
   // findDepUpgrades can add more than one upgrade to allUpgrades
@@ -181,111 +167,14 @@ async function findUpgrades(dependencies) {
   return allUpgrades;
 }
 
-async function processUpgrades(upgrades) {
-  if (upgrades.length) {
-    const upgradeCount = upgrades.length === 1
-      ? '1 dependency upgrade'
-      : `${upgrades.length} dependency upgrades`;
-    logger.info(`Processing ${upgradeCount}`);
-  } else {
-    logger.info('No upgrades to process');
-  }
-  logger.debug(`All upgrades: ${JSON.stringify(upgrades)}`);
-  const branchUpgrades = {};
-  for (const upgrade of upgrades) {
-    const flattened = Object.assign({}, upgrade.config, upgrade);
-    delete flattened.config;
-    if (flattened.upgradeType === 'pin') {
-      flattened.isPin = true;
-    } else if (flattened.upgradeType === 'major') {
-      flattened.isMajor = true;
-    } else if (flattened.upgradeType === 'minor') {
-      flattened.isMinor = true;
-    }
-    // Check whether to use a group name
-    let branchName;
-    if (flattened.groupName) {
-      logger.debug(
-        `Dependency ${flattened.depName} is part of group '${flattened.groupName}'`
-      );
-      flattened.groupSlug =
-        flattened.groupSlug ||
-        flattened.groupName.toLowerCase().replace(/[^a-z0-9+]+/g, '-');
-      branchName = handlebars.compile(flattened.groupBranchName)(flattened);
-      logger.debug(`branchName=${branchName}`);
-      if (branchUpgrades[branchName]) {
-        // flattened.branchName = flattened.groupBranchName;
-        flattened.commitMessage = flattened.groupCommitMessage;
-        flattened.prTitle = flattened.groupPrTitle;
-        flattened.prBody = flattened.groupPrBody;
-      }
-    } else {
-      branchName = handlebars.compile(flattened.branchName)(flattened);
-    }
-    branchUpgrades[branchName] = branchUpgrades[branchName] || [];
-    branchUpgrades[branchName] = [flattened].concat(branchUpgrades[branchName]);
-  }
-  logger.debug(`Branched upgrades: ${JSON.stringify(branchUpgrades)}`);
-  for (const branch of Object.keys(branchUpgrades)) {
-    await module.exports.removeStandaloneBranches(branchUpgrades[branch]);
-    await module.exports.updateBranch(branchUpgrades[branch]);
-  }
-}
-
-async function removeStandaloneBranches(upgrades) {
-  if (upgrades.length > 1) {
-    for (const upgrade of upgrades) {
-      const standaloneBranchName = handlebars.compile(upgrade.branchName)(
-        upgrade
-      );
-      logger.debug(`Need to delete branch ${standaloneBranchName}`);
-      try {
-        await upgrade.api.deleteBranch(standaloneBranchName);
-      } catch (err) {
-        logger.debug(`Couldn't delete branch ${standaloneBranchName}`);
-      }
-      // Rename to group branchName
-      upgrade.branchName = upgrade.groupBranchName;
-    }
-  }
-}
-
-async function updateBranch(upgrades) {
-  // Use templates to generate strings
-  const upgrade0 = upgrades[0];
-  const branchName = handlebars.compile(upgrade0.branchName)(upgrade0);
-  const prTitle = handlebars.compile(upgrade0.prTitle)(upgrade0);
-
-  const upgradeCount = upgrades.length === 1
-    ? '1 upgrade'
-    : `${upgrades.length} upgrades`;
-  logger.info(
-    `Branch '${branchName}' has ${upgradeCount}: ${upgrades.map(
-      upgrade => upgrade.depName
-    )}`
-  );
-
-  try {
-    if (
-      upgrade0.upgradeType !== 'maintainYarnLock' &&
-      upgrade0.groupName === null &&
-      !upgrade0.recreateClosed &&
-      (await upgrade0.api.checkForClosedPr(branchName, prTitle))
-    ) {
-      logger.info(
-        `Skipping ${branchName} upgrade as matching closed PR already existed`
-      );
-      return;
-    }
-    const branchCreated = await branchWorker.ensureBranch(upgrades);
-    if (branchCreated) {
-      const pr = await prWorker.ensurePr(upgrades);
-      if (pr) {
-        await prWorker.checkAutoMerge(pr, upgrade0);
+function getDepTypeConfig(depTypes, depTypeName) {
+  let depTypeConfig = {};
+  if (depTypes) {
+    depTypes.forEach(depType => {
+      if (typeof depType !== 'string' && depType.depType === depTypeName) {
+        depTypeConfig = depType;
       }
-    }
-  } catch (error) {
-    logger.error(`Error updating branch ${branchName}: ${error}`);
-    // Don't throw here - we don't want to stop the other renovations
+    });
   }
+  return depTypeConfig;
 }
diff --git a/lib/workers/pr.js b/lib/workers/pr.js
index 003c2d80cdb6690b5f7e98df3cb07d33cf7e5358..8c1884f8043cabaed8950eeb5474241cefb84851 100644
--- a/lib/workers/pr.js
+++ b/lib/workers/pr.js
@@ -1,4 +1,3 @@
-const logger = require('../logger');
 const handlebars = require('handlebars');
 const changelogHelper = require('../helpers/changelog');
 
@@ -8,8 +7,8 @@ module.exports = {
 };
 
 // Ensures that PR exists with matching title/body
-async function ensurePr(upgrades) {
-  logger.debug(`ensurePr(${JSON.stringify(upgrades)})`);
+async function ensurePr(upgrades, logger) {
+  logger.debug({ config: upgrades }, 'ensurePr');
   // If there is a group, it will use the config of the first upgrade in the array
   const config = Object.assign({}, upgrades[0]);
   config.upgrades = [];
@@ -57,7 +56,8 @@ async function ensurePr(upgrades) {
     const logJSON = await changelogHelper.getChangeLogJSON(
       upgrade.depName,
       upgrade.changeLogFromVersion,
-      upgrade.changeLogToVersion
+      upgrade.changeLogToVersion,
+      logger
     );
     // Store changelog markdown for backwards compatibility
     if (logJSON) {
@@ -140,7 +140,7 @@ async function ensurePr(upgrades) {
   return null;
 }
 
-async function checkAutoMerge(pr, config) {
+async function checkAutoMerge(pr, config, logger) {
   logger.debug(`Checking #${pr.number} for automerge`);
   if (config.automergeEnabled && config.automergeType === 'pr') {
     logger.info('PR is configured for automerge');
diff --git a/lib/workers/repository.js b/lib/workers/repository.js
new file mode 100644
index 0000000000000000000000000000000000000000..7329ee5cfb42c33acbf4013d1d1eb9550dcf69b8
--- /dev/null
+++ b/lib/workers/repository.js
@@ -0,0 +1,256 @@
+// Global requires
+const handlebars = require('handlebars');
+const ini = require('ini');
+let logger = require('../helpers/logger');
+const stringify = require('json-stringify-pretty-compact');
+// API
+const githubApi = require('../api/github');
+const gitlabApi = require('../api/gitlab');
+const npmApi = require('../api/npm');
+// Config
+const defaultsParser = require('../config/defaults');
+// Workers
+const packageFileWorker = require('./package-file');
+const branchWorker = require('./branch');
+
+module.exports = {
+  processRepo,
+  processUpgrades,
+  removeStandaloneBranches,
+};
+
+// This will be github or others
+let api;
+
+// Queue package files in sequence within a repo
+async function processRepo(config) {
+  logger = config.logger.child({ repository: config.repository });
+  config.logger = logger; // eslint-disable-line no-param-reassign
+  logger.info('Renovating repository');
+  logger.debug({ config }, 'processRepo');
+  if (config.platform === 'github') {
+    api = githubApi;
+  } else if (config.platform === 'gitlab') {
+    api = gitlabApi;
+  } else {
+    // TODO: throw this?
+    logger.error(
+      `Unknown platform ${config.platform} for repository ${config.repository}`
+    );
+    return;
+  }
+  try {
+    // Initialize repo
+    await api.initRepo(
+      config.repository,
+      config.token,
+      config.endpoint,
+      logger
+    );
+    // Override settings with renovate.json if present
+    await mergeRenovateJson(config);
+    // Check that the repository is onboarded
+    const isOnboarded = await checkIfOnboarded(config);
+    if (isOnboarded === false) {
+      return;
+    }
+    // Check for presence of .npmrc in repository
+    await setNpmrc(config);
+    // Detect package files if none already configured
+    await detectPackageFiles(config);
+    const upgrades = await getAllRepoUpgrades(config);
+    await module.exports.processUpgrades(upgrades);
+  } catch (error) {
+    throw error;
+  }
+  logger.info('Finished repository');
+}
+
+// Check for config in `renovate.json`
+async function mergeRenovateJson(config) {
+  const renovateJson = await api.getFileJson('renovate.json');
+  if (renovateJson) {
+    logger.debug({ config: renovateJson }, 'renovate.json config');
+    Object.assign(config, renovateJson, { repoConfigured: true });
+  } else {
+    logger.debug('No renovate.json found');
+  }
+}
+
+// Check for .npmrc in repository and pass it to npm api if found
+async function setNpmrc() {
+  try {
+    let npmrc = null;
+    const npmrcContent = await api.getFileContent('.npmrc');
+    if (npmrcContent) {
+      logger.debug('Found .npmrc file in repository');
+      npmrc = ini.parse(npmrcContent);
+    }
+    npmApi.setNpmrc(npmrc);
+  } catch (err) {
+    logger.error('Failed to set .npmrc');
+  }
+}
+
+async function checkIfOnboarded(config) {
+  logger.debug('Checking if repo is configured');
+  // Check if repository is configured
+  if (config.repoConfigured || config.onboarding === false) {
+    logger.debug('Repo is configured or onboarding disabled');
+    return true;
+  }
+  const pr = await api.findPr('renovate/configure', 'Configure Renovate');
+  if (pr) {
+    if (pr.isClosed) {
+      logger.debug('Closed Configure Renovate PR found - continuing');
+      return true;
+    }
+    // PR exists but hasn't been closed yet
+    logger.error(`Close PR #${pr.displayNumber} before continuing`);
+    return false;
+  }
+  await onboardRepository(config);
+  return false;
+}
+
+// Ensure config contains packageFiles
+async function detectPackageFiles(config) {
+  if (config.packageFiles.length === 0) {
+    // autodiscover filenames if none manually configured
+    const fileNames = await api.findFilePaths('package.json');
+    // Map to config structure
+    const packageFiles = fileNames.map(fileName => ({ fileName }));
+    Object.assign(config, { packageFiles });
+  }
+}
+
+async function onboardRepository(config) {
+  const defaultConfig = defaultsParser.getConfig();
+  delete defaultConfig.onboarding;
+  delete defaultConfig.platform;
+  delete defaultConfig.endpoint;
+  delete defaultConfig.token;
+  delete defaultConfig.autodiscover;
+  delete defaultConfig.githubAppId;
+  delete defaultConfig.githubAppKey;
+  delete defaultConfig.repositories;
+  delete defaultConfig.logLevel;
+  let prBody = `Welcome to [Renovate](https://keylocation.sg/our-tech/renovate)! Once you close this Pull Request, we will begin keeping your dependencies up-to-date via automated Pull Requests.
+
+The [Configuration](https://github.com/singapore/renovate/blob/master/docs/configuration.md) and [Configuration FAQ](https://github.com/singapore/renovate/blob/master/docs/faq.md) documents should be helpful.
+
+#### Important!
+
+You do not need to *merge* this Pull Request - renovate will begin even if it's closed *unmerged*.
+In fact, you only need to add a \`renovate.json\` file to your repository if you wish to override any default settings. The file is included as part of this PR only in case you wish to change default settings before you start.
+If the default settings are all suitable for you, simply close this Pull Request unmerged and your first renovation will begin the next time the program is run.`;
+
+  if (config.platform === 'gitlab') {
+    defaultConfig.platform = 'gitlab';
+    prBody = prBody.replace(/Pull Request/g, 'Merge Request');
+  }
+  const defaultConfigString = `${stringify(defaultConfig)}\n`;
+  await api.commitFilesToBranch(
+    'renovate/configure',
+    [
+      {
+        name: 'renovate.json',
+        contents: defaultConfigString,
+      },
+    ],
+    'Add renovate.json'
+  );
+  const pr = await api.createPr(
+    'renovate/configure',
+    'Configure Renovate',
+    prBody
+  );
+  logger.info(`Created ${pr.displayNumber} for configuration`);
+}
+
+async function getAllRepoUpgrades(config) {
+  logger.info('getAllRepoUpgrades');
+  let upgrades = [];
+  for (let packageFile of config.packageFiles) {
+    if (typeof packageFile === 'string') {
+      packageFile = { fileName: packageFile };
+    }
+    const cascadedConfig = Object.assign({}, config, packageFile);
+    // Remove unnecessary fields
+    cascadedConfig.packageFile = cascadedConfig.fileName;
+    delete cascadedConfig.fileName;
+    upgrades = upgrades.concat(
+      await packageFileWorker.processPackageFile(cascadedConfig)
+    );
+  }
+  return upgrades;
+}
+
+async function processUpgrades(upgrades) {
+  if (upgrades.length) {
+    const upgradeCount = upgrades.length === 1
+      ? '1 dependency upgrade'
+      : `${upgrades.length} dependency upgrades`;
+    logger.info(`Processing ${upgradeCount}`);
+  } else {
+    logger.info('No upgrades to process');
+  }
+  logger.debug({ config: upgrades }, 'All upgrades');
+  const branchUpgrades = {};
+  for (const upgrade of upgrades) {
+    const flattened = Object.assign({}, upgrade.config, upgrade);
+    delete flattened.config;
+    if (flattened.upgradeType === 'pin') {
+      flattened.isPin = true;
+    } else if (flattened.upgradeType === 'major') {
+      flattened.isMajor = true;
+    } else if (flattened.upgradeType === 'minor') {
+      flattened.isMinor = true;
+    }
+    // Check whether to use a group name
+    let branchName;
+    if (flattened.groupName) {
+      logger.debug(
+        `Dependency ${flattened.depName} is part of group '${flattened.groupName}'`
+      );
+      flattened.groupSlug =
+        flattened.groupSlug ||
+        flattened.groupName.toLowerCase().replace(/[^a-z0-9+]+/g, '-');
+      branchName = handlebars.compile(flattened.groupBranchName)(flattened);
+      logger.debug(`branchName=${branchName}`);
+      if (branchUpgrades[branchName]) {
+        // flattened.branchName = flattened.groupBranchName;
+        flattened.commitMessage = flattened.groupCommitMessage;
+        flattened.prTitle = flattened.groupPrTitle;
+        flattened.prBody = flattened.groupPrBody;
+      }
+    } else {
+      branchName = handlebars.compile(flattened.branchName)(flattened);
+    }
+    branchUpgrades[branchName] = branchUpgrades[branchName] || [];
+    branchUpgrades[branchName] = [flattened].concat(branchUpgrades[branchName]);
+  }
+  logger.debug({ config: branchUpgrades }, 'Branched upgrades');
+  for (const branch of Object.keys(branchUpgrades)) {
+    await module.exports.removeStandaloneBranches(branchUpgrades[branch]);
+    await branchWorker.updateBranch(branchUpgrades[branch], logger);
+  }
+}
+
+async function removeStandaloneBranches(upgrades) {
+  if (upgrades.length > 1) {
+    for (const upgrade of upgrades) {
+      const standaloneBranchName = handlebars.compile(upgrade.branchName)(
+        upgrade
+      );
+      logger.debug(`Need to delete branch ${standaloneBranchName}`);
+      try {
+        await upgrade.api.deleteBranch(standaloneBranchName);
+      } catch (err) {
+        logger.debug(`Couldn't delete branch ${standaloneBranchName}`);
+      }
+      // Rename to group branchName
+      upgrade.branchName = upgrade.groupBranchName;
+    }
+  }
+}
diff --git a/package.json b/package.json
index 8e5a1b5bfa40ed49478bd55efed78ce86ed6c8f5..03fe526dff87ed3f08eeed76c6a2155e2ba763ee 100644
--- a/package.json
+++ b/package.json
@@ -59,7 +59,8 @@
     "semver": "5.3.0",
     "semver-stable": "2.0.4",
     "semver-utils": "1.1.1",
-    "tmp": "0.0.31"
+    "tmp": "0.0.31",
+    "traverse": "0.6.6"
   },
   "devDependencies": {
     "babel-cli": "6.24.1",
diff --git a/test/config/__snapshots__/index.spec.js.snap b/test/config/__snapshots__/index.spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..2f519246fb9f258cb9bc3565593d3c2ce244e8e1
--- /dev/null
+++ b/test/config/__snapshots__/index.spec.js.snap
@@ -0,0 +1,16 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`config/index .getRepoConfig(config, index) handles object repos 1`] = `
+Object {
+  "global": "b",
+  "repoField": "g",
+  "repository": "e/f",
+}
+`;
+
+exports[`config/index .getRepoConfig(config, index) massages string repos 1`] = `
+Object {
+  "global": "b",
+  "repository": "c/d",
+}
+`;
diff --git a/test/config/index.spec.js b/test/config/index.spec.js
index 585ba8ae4042426c32f957da88d61edc44eee128..b96bd87116bbdf9865c8e859bc33353a348daef6 100644
--- a/test/config/index.spec.js
+++ b/test/config/index.spec.js
@@ -1,5 +1,4 @@
 const argv = require('../_fixtures/config/argv');
-const should = require('chai').should();
 
 describe('config/index', () => {
   describe('.parseConfigs(env, defaultArgv)', () => {
@@ -150,23 +149,37 @@ describe('config/index', () => {
       expect(ghGot.mock.calls.length).toBe(1);
       expect(glGot.mock.calls.length).toBe(0);
     });
-    it('supports repositories in CLI', async () => {
-      const env = {};
-      defaultArgv = defaultArgv.concat(['--token=abc', 'foo']);
+    it('adds a log file', async () => {
+      const env = { GITHUB_TOKEN: 'abc', RENOVATE_LOG_FILE: 'debug.log' };
+      defaultArgv = defaultArgv.concat(['--autodiscover']);
+      ghGot.mockImplementationOnce(() => ({
+        body: [],
+      }));
       await configParser.parseConfigs(env, defaultArgv);
-      const repos = configParser.getRepositories();
-      should.exist(repos);
-      repos.should.have.length(1);
-      repos[0].repository.should.eql('foo');
+      expect(ghGot.mock.calls.length).toBe(1);
+      expect(glGot.mock.calls.length).toBe(0);
     });
-    it('gets cascaded config', async () => {
-      const env = { RENOVATE_CONFIG_FILE: 'test/_fixtures/config/file.js' };
-      await configParser.parseConfigs(env, defaultArgv);
-      const repo = configParser.getRepositories().pop();
-      should.exist(repo);
-      const cascadedConfig = configParser.getCascadedConfig(repo, null);
-      should.exist(cascadedConfig.token);
-      should.exist(cascadedConfig.recreateClosed);
+  });
+  describe('.getRepoConfig(config, index)', () => {
+    let configParser;
+    beforeEach(() => {
+      configParser = require('../../lib/config/index.js');
+    });
+    const config = {
+      global: 'b',
+      repositories: [
+        'c/d',
+        {
+          repository: 'e/f',
+          repoField: 'g',
+        },
+      ],
+    };
+    it('massages string repos', () => {
+      expect(configParser.getRepoConfig(config, 0)).toMatchSnapshot();
+    });
+    it('handles object repos', () => {
+      expect(configParser.getRepoConfig(config, 1)).toMatchSnapshot();
     });
   });
 });
diff --git a/test/helpers/changelog.spec.js b/test/helpers/changelog.spec.js
index 4eb6d6ed3a59c1ea0211b2e35cb082fa7d478cdd..81f3a7d3e89f94c758044221784e7f0a8d26377d 100644
--- a/test/helpers/changelog.spec.js
+++ b/test/helpers/changelog.spec.js
@@ -1,31 +1,38 @@
 const changelog = require('changelog');
 const changelogHelper = require('../../lib/helpers/changelog');
+const bunyan = require('bunyan');
+
+const logger = bunyan.createLogger({
+  name: 'test',
+  stream: process.stdout,
+  level: 'fatal',
+});
 
 jest.mock('changelog');
 
 describe('helpers/changelog', () => {
-  describe('changelogHelper.getChangeLog(depName, fromVersion, newVersion)', () => {
+  describe('changelogHelper.getChangeLog(depName, fromVersion, newVersion, logger)', () => {
     it('returns empty if no fromVersion', async () => {
       expect(
-        await changelogHelper.getChangeLog('renovate', null, '1.0.0')
+        await changelogHelper.getChangeLog('renovate', null, '1.0.0', logger)
       ).toBe('No changelog available');
     });
     it('returns empty if fromVersion equals newVersion', async () => {
       expect(
-        await changelogHelper.getChangeLog('renovate', '1.0.0', '1.0.0')
+        await changelogHelper.getChangeLog('renovate', '1.0.0', '1.0.0', logger)
       ).toBe('No changelog available');
     });
     it('returns empty if generated json is null', async () => {
       changelog.generate.mockReturnValueOnce(null);
       expect(
-        await changelogHelper.getChangeLog('renovate', '1.0.0', '2.0.0')
+        await changelogHelper.getChangeLog('renovate', '1.0.0', '2.0.0', logger)
       ).toBe('No changelog available');
     });
     it('returns header if generated markdown is valid', async () => {
       changelog.generate.mockReturnValueOnce({});
       changelog.markdown.mockReturnValueOnce('dummy');
       expect(
-        await changelogHelper.getChangeLog('renovate', '1.0.0', '2.0.0')
+        await changelogHelper.getChangeLog('renovate', '1.0.0', '2.0.0', logger)
       ).toBe('### Changelog\n\ndummy');
     });
     it('returns empty if error thrown', async () => {
@@ -33,7 +40,7 @@ describe('helpers/changelog', () => {
         throw new Error('foo');
       });
       expect(
-        await changelogHelper.getChangeLog('renovate', '1.0.0', '2.0.0')
+        await changelogHelper.getChangeLog('renovate', '1.0.0', '2.0.0', logger)
       ).toBe('No changelog available');
     });
   });
diff --git a/test/helpers/logger/__snapshots__/config-serializer.spec.js.snap b/test/helpers/logger/__snapshots__/config-serializer.spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..693839e047201dbd253ded8ebab6f2004e2c3ac0
--- /dev/null
+++ b/test/helpers/logger/__snapshots__/config-serializer.spec.js.snap
@@ -0,0 +1,17 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`helpers/logger/config-serializer redacts sensitive fields 1`] = `
+Object {
+  "githubAppKey": "***********",
+  "nottoken": "b",
+  "token": "***********",
+}
+`;
+
+exports[`helpers/logger/config-serializer replaces functions 1`] = `
+Object {
+  "api": "[Function]",
+  "logger": "[Function]",
+  "nottoken": "b",
+}
+`;
diff --git a/test/helpers/logger/__snapshots__/pretty-stdout.spec.js.snap b/test/helpers/logger/__snapshots__/pretty-stdout.spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..b465c2c8735d8176bdf2034bbf7bdd29808dfb21
--- /dev/null
+++ b/test/helpers/logger/__snapshots__/pretty-stdout.spec.js.snap
@@ -0,0 +1,6 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`helpers/logger/pretty-stdout getDetails(rec) supports a config 1`] = `
+"       {\\"a\\": \\"b\\", \\"d\\": [\\"e\\", \\"f\\"]}
+"
+`;
diff --git a/test/helpers/logger/config-serializer.spec.js b/test/helpers/logger/config-serializer.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..44baa875ace3e88260ceb5a33b5bc29835dcc490
--- /dev/null
+++ b/test/helpers/logger/config-serializer.spec.js
@@ -0,0 +1,20 @@
+const configSerializer = require('../../../lib/helpers/logger/config-serializer');
+
+describe('helpers/logger/config-serializer', () => {
+  it('redacts sensitive fields', () => {
+    const config = {
+      token: 'a',
+      nottoken: 'b',
+      githubAppKey: 'c',
+    };
+    expect(configSerializer(config)).toMatchSnapshot();
+  });
+  it('replaces functions', () => {
+    const config = {
+      api: 'a',
+      nottoken: 'b',
+      logger: {},
+    };
+    expect(configSerializer(config)).toMatchSnapshot();
+  });
+});
diff --git a/test/helpers/logger/pretty-stdout.spec.js b/test/helpers/logger/pretty-stdout.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..61c5086ede3cc773b0748adb423f53b98fabc905
--- /dev/null
+++ b/test/helpers/logger/pretty-stdout.spec.js
@@ -0,0 +1,62 @@
+const prettyStdout = require('../../../lib/helpers/logger/pretty-stdout');
+const chalk = require('chalk');
+
+describe('helpers/logger/pretty-stdout', () => {
+  describe('getMeta(rec)', () => {
+    it('returns empty string if null rec', () => {
+      expect(prettyStdout.getMeta(null)).toEqual('');
+    });
+    it('returns empty string if empty rec', () => {
+      expect(prettyStdout.getMeta({})).toEqual('');
+    });
+    it('returns empty string if no meta fields', () => {
+      const rec = {
+        foo: 'bar',
+      };
+      expect(prettyStdout.getMeta(rec)).toEqual('');
+    });
+    it('supports single meta', () => {
+      const rec = {
+        foo: 'bar',
+        repository: 'a/b',
+      };
+      expect(prettyStdout.getMeta(rec)).toEqual(
+        chalk.gray(' (repository=a/b)')
+      );
+    });
+    it('supports multi meta', () => {
+      const rec = {
+        foo: 'bar',
+        branch: 'c',
+        repository: 'a/b',
+      };
+      expect(prettyStdout.getMeta(rec)).toEqual(
+        chalk.gray(' (repository=a/b, branch=c)')
+      );
+    });
+  });
+  describe('getDetails(rec)', () => {
+    it('returns empty string if null rec', () => {
+      expect(prettyStdout.getDetails(null)).toEqual('');
+    });
+    it('returns empty string if empty rec', () => {
+      expect(prettyStdout.getDetails({})).toEqual('');
+    });
+    it('returns empty string if no meta fields', () => {
+      const rec = {
+        foo: 'bar',
+      };
+      expect(prettyStdout.getDetails(rec)).toEqual('');
+    });
+    it('supports a config', () => {
+      const rec = {
+        foo: 'bar',
+        config: {
+          a: 'b',
+          d: ['e', 'f'],
+        },
+      };
+      expect(prettyStdout.getDetails(rec)).toMatchSnapshot();
+    });
+  });
+});
diff --git a/test/helpers/package-json.spec.js b/test/helpers/package-json.spec.js
index 6d8befe7b19ae2c36c081e1fabbf1fe36e398b2e..a973c30091b196d56117d27fd75b5448fa850a44 100644
--- a/test/helpers/package-json.spec.js
+++ b/test/helpers/package-json.spec.js
@@ -1,6 +1,13 @@
 const fs = require('fs');
 const path = require('path');
 const packageJson = require('../../lib/helpers/package-json');
+const bunyan = require('bunyan');
+
+const logger = bunyan.createLogger({
+  name: 'test',
+  stream: process.stdout,
+  level: 'fatal',
+});
 
 const defaultTypes = [
   'dependencies',
@@ -46,14 +53,15 @@ describe('helpers/package-json', () => {
       extractedDependencies.should.have.length(6);
     });
   });
-  describe('.setNewValue(currentFileContent, depType, depName, newVersion)', () => {
+  describe('.setNewValue(currentFileContent, depType, depName, newVersion, logger)', () => {
     it('replaces a dependency value', () => {
       const outputContent = readFixture('outputs/011.json');
       const testContent = packageJson.setNewValue(
         input01Content,
         'dependencies',
         'cheerio',
-        '0.22.1'
+        '0.22.1',
+        logger
       );
       testContent.should.equal(outputContent);
     });
@@ -63,7 +71,8 @@ describe('helpers/package-json', () => {
         input01Content,
         'devDependencies',
         'angular-touch',
-        '1.6.1'
+        '1.6.1',
+        logger
       );
       testContent.should.equal(outputContent);
     });
@@ -73,7 +82,8 @@ describe('helpers/package-json', () => {
         input01Content,
         'devDependencies',
         'angular-sanitize',
-        '1.6.1'
+        '1.6.1',
+        logger
       );
       testContent.should.equal(outputContent);
     });
@@ -82,7 +92,8 @@ describe('helpers/package-json', () => {
         input01Content,
         'devDependencies',
         'angular-touch',
-        '1.5.8'
+        '1.5.8',
+        logger
       );
       testContent.should.equal(input01Content);
     });
diff --git a/test/__snapshots__/worker.spec.js.snap b/test/workers/__snapshots__/package-file.spec.js.snap
similarity index 64%
rename from test/__snapshots__/worker.spec.js.snap
rename to test/workers/__snapshots__/package-file.spec.js.snap
index 8fe51724e719808cf4050c1f56614fdd38e8066c..0c7271025ec41f4ad7367532ec2004ee9ff07e24 100644
--- a/test/__snapshots__/worker.spec.js.snap
+++ b/test/workers/__snapshots__/package-file.spec.js.snap
@@ -1,6 +1,6 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`worker assignDepConfigs(inputConfig, deps) handles depType config with override 1`] = `
+exports[`packageFileWorker assignDepConfigs(inputConfig, deps) handles depType config with override 1`] = `
 Array [
   Object {
     "config": Object {
@@ -12,7 +12,7 @@ Array [
 ]
 `;
 
-exports[`worker assignDepConfigs(inputConfig, deps) handles depType config without override 1`] = `
+exports[`packageFileWorker assignDepConfigs(inputConfig, deps) handles depType config without override 1`] = `
 Array [
   Object {
     "config": Object {
@@ -25,7 +25,7 @@ Array [
 ]
 `;
 
-exports[`worker assignDepConfigs(inputConfig, deps) handles multiple deps 1`] = `
+exports[`packageFileWorker assignDepConfigs(inputConfig, deps) handles multiple deps 1`] = `
 Array [
   Object {
     "config": Object {
@@ -42,7 +42,7 @@ Array [
 ]
 `;
 
-exports[`worker assignDepConfigs(inputConfig, deps) handles non-regex package name 1`] = `
+exports[`packageFileWorker assignDepConfigs(inputConfig, deps) handles non-regex package name 1`] = `
 Array [
   Object {
     "config": Object {
@@ -74,7 +74,7 @@ Array [
 ]
 `;
 
-exports[`worker assignDepConfigs(inputConfig, deps) handles package config 1`] = `
+exports[`packageFileWorker assignDepConfigs(inputConfig, deps) handles package config 1`] = `
 Array [
   Object {
     "config": Object {
@@ -88,7 +88,7 @@ Array [
 ]
 `;
 
-exports[`worker assignDepConfigs(inputConfig, deps) handles regex package pattern 1`] = `
+exports[`packageFileWorker assignDepConfigs(inputConfig, deps) handles regex package pattern 1`] = `
 Array [
   Object {
     "config": Object {
@@ -126,7 +126,7 @@ Array [
 ]
 `;
 
-exports[`worker assignDepConfigs(inputConfig, deps) handles regex wildcard package pattern 1`] = `
+exports[`packageFileWorker assignDepConfigs(inputConfig, deps) handles regex wildcard package pattern 1`] = `
 Array [
   Object {
     "config": Object {
@@ -161,7 +161,7 @@ Array [
 ]
 `;
 
-exports[`worker assignDepConfigs(inputConfig, deps) handles string deps 1`] = `
+exports[`packageFileWorker assignDepConfigs(inputConfig, deps) handles string deps 1`] = `
 Array [
   Object {
     "config": Object {
@@ -172,7 +172,7 @@ Array [
 ]
 `;
 
-exports[`worker assignDepConfigs(inputConfig, deps) nested package config overrides depType and general config 1`] = `
+exports[`packageFileWorker assignDepConfigs(inputConfig, deps) nested package config overrides depType and general config 1`] = `
 Array [
   Object {
     "config": Object {
@@ -184,7 +184,7 @@ Array [
 ]
 `;
 
-exports[`worker assignDepConfigs(inputConfig, deps) package config overrides depType and general config 1`] = `
+exports[`packageFileWorker assignDepConfigs(inputConfig, deps) package config overrides depType and general config 1`] = `
 Array [
   Object {
     "config": Object {
@@ -195,3 +195,21 @@ Array [
   },
 ]
 `;
+
+exports[`packageFileWorker processPackageFile(config) extracts dependencies for each depType 1`] = `
+Array [
+  Array [
+    Object {},
+    Array [
+      "dependencies",
+      "devDependencies",
+    ],
+  ],
+]
+`;
+
+exports[`packageFileWorker processPackageFile(config) filters dependencies 1`] = `
+Array [
+  "a",
+]
+`;
diff --git a/test/workers/__snapshots__/pr.spec.js.snap b/test/workers/__snapshots__/pr.spec.js.snap
index fddec193f4506803d6f0f30a5ebdd0b9ded18cf7..c1af747f56abdec088d05044cf6b7a0df0f87683 100644
--- a/test/workers/__snapshots__/pr.spec.js.snap
+++ b/test/workers/__snapshots__/pr.spec.js.snap
@@ -1,3 +1,3 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`workers/pr ensurePr(upgrades) should return unmodified existing PR 1`] = `Array []`;
+exports[`workers/pr ensurePr(upgrades, logger) should return unmodified existing PR 1`] = `Array []`;
diff --git a/test/workers/branch.spec.js b/test/workers/branch.spec.js
index 9faf69c6fa5106a9f329e60dbc9b90a8bfa9e045..d21e20dafe3f1246138fcee5ca0b0a71a61d388d 100644
--- a/test/workers/branch.spec.js
+++ b/test/workers/branch.spec.js
@@ -1,9 +1,18 @@
 const branchWorker = require('../../lib/workers/branch');
+const prWorker = require('../../lib/workers/pr');
 const npmHelper = require('../../lib/helpers/npm');
 const yarnHelper = require('../../lib/helpers/yarn');
 const defaultConfig = require('../../lib/config/defaults').getConfig();
 const packageJsonHelper = require('../../lib/helpers/package-json');
 
+const bunyan = require('bunyan');
+
+const logger = bunyan.createLogger({
+  name: 'test',
+  stream: process.stdout,
+  level: 'fatal',
+});
+
 jest.mock('../../lib/helpers/yarn');
 jest.mock('../../lib/helpers/package-json');
 
@@ -304,4 +313,48 @@ describe('workers/branch', () => {
       expect(config.api.commitFilesToBranch.mock.calls.length).toBe(0);
     });
   });
+  describe('updateBranch(upgrades)', () => {
+    let config;
+    beforeEach(() => {
+      config = Object.assign({}, defaultConfig);
+      config.api = {
+        checkForClosedPr: jest.fn(),
+      };
+      branchWorker.ensureBranch = jest.fn();
+      prWorker.ensurePr = jest.fn();
+    });
+    it('returns immediately if closed PR found', async () => {
+      config.api.checkForClosedPr.mockReturnValue(true);
+      await branchWorker.updateBranch([config], logger);
+      expect(branchWorker.ensureBranch.mock.calls.length).toBe(0);
+    });
+    it('does not return immediately if recreateClosed true', async () => {
+      config.api.checkForClosedPr.mockReturnValue(true);
+      config.recreateClosed = true;
+      await branchWorker.updateBranch([config], logger);
+      expect(branchWorker.ensureBranch.mock.calls.length).toBe(1);
+    });
+    it('pins', async () => {
+      config.upgradeType = 'pin';
+      await branchWorker.updateBranch([config], logger);
+      expect(branchWorker.ensureBranch.mock.calls.length).toBe(1);
+    });
+    it('majors', async () => {
+      config.upgradeType = 'major';
+      await branchWorker.updateBranch([config], logger);
+      expect(branchWorker.ensureBranch.mock.calls.length).toBe(1);
+    });
+    it('minors', async () => {
+      config.upgradeType = 'minor';
+      await branchWorker.updateBranch([config], logger);
+      expect(branchWorker.ensureBranch.mock.calls.length).toBe(1);
+    });
+    it('handles errors', async () => {
+      config.api.checkForClosedPr = jest.fn(() => {
+        throw new Error('oops');
+      });
+      await branchWorker.updateBranch([config], logger);
+      expect(branchWorker.ensureBranch.mock.calls.length).toBe(0);
+    });
+  });
 });
diff --git a/test/index.spec.js b/test/workers/index.spec.js
similarity index 76%
rename from test/index.spec.js
rename to test/workers/index.spec.js
index 9d45856e255746d3501cd4d3a01447236af63995..5cacbbdab3210991efd15c13f3d1652544a2b210 100644
--- a/test/index.spec.js
+++ b/test/workers/index.spec.js
@@ -1,4 +1,4 @@
-require('../lib/index');
+require('../../lib/workers/index');
 
 it('placeholder', () => {
   // TODO: write tests for this module - this is here so the file shows up in coverage
diff --git a/test/worker.spec.js b/test/workers/package-file.spec.js
similarity index 56%
rename from test/worker.spec.js
rename to test/workers/package-file.spec.js
index c4b63818358cd832989d8fe6902e966562dae0f9..0fc62553f0f1b88f216ba8f38d159a6defe7a38b 100644
--- a/test/worker.spec.js
+++ b/test/workers/package-file.spec.js
@@ -1,81 +1,29 @@
-const worker = require('../lib/worker');
-const branchWorker = require('../lib/workers/branch');
-const prWorker = require('../lib/workers/pr');
-const defaultConfig = require('../lib/config/defaults').getConfig();
-const npmApi = require('../lib/api/npm');
-const versionsHelper = require('../lib/helpers/versions');
+const packageFileWorker = require('../../lib/workers/package-file');
+const npmApi = require('../../lib/api/npm');
+const versionsHelper = require('../../lib/helpers/versions');
+const packageJsonHelper = require('../../lib/helpers/package-json');
+const bunyan = require('bunyan');
 
-jest.mock('../lib/workers/branch');
-jest.mock('../lib/workers/pr');
-jest.mock('../lib/api/npm');
-jest.mock('../lib/helpers/versions');
+const logger = bunyan.createLogger({
+  name: 'test',
+  stream: process.stdout,
+  level: 'fatal',
+});
 
-describe('worker', () => {
-  describe('updateDependency(upgrade)', () => {
-    let config;
-    beforeEach(() => {
-      config = Object.assign({}, defaultConfig);
-      config.api = {
-        checkForClosedPr: jest.fn(),
-      };
-      branchWorker.ensureBranch = jest.fn();
-      prWorker.ensurePr = jest.fn();
-    });
-    it('returns immediately if closed PR found', async () => {
-      config.api.checkForClosedPr.mockReturnValue(true);
-      await worker.updateBranch([config]);
-      expect(branchWorker.ensureBranch.mock.calls.length).toBe(0);
-    });
-    it('does not return immediately if recreateClosed true', async () => {
-      config.api.checkForClosedPr.mockReturnValue(true);
-      config.recreateClosed = true;
-      await worker.updateBranch([config]);
-      expect(branchWorker.ensureBranch.mock.calls.length).toBe(1);
-    });
-    it('pins', async () => {
-      config.upgradeType = 'pin';
-      await worker.updateBranch([config]);
-      expect(branchWorker.ensureBranch.mock.calls.length).toBe(1);
-    });
-    it('majors', async () => {
-      config.upgradeType = 'major';
-      await worker.updateBranch([config]);
-      expect(branchWorker.ensureBranch.mock.calls.length).toBe(1);
-    });
-    it('minors', async () => {
-      config.upgradeType = 'minor';
-      await worker.updateBranch([config]);
-      expect(branchWorker.ensureBranch.mock.calls.length).toBe(1);
-    });
-    it('handles errors', async () => {
-      config.api.checkForClosedPr = jest.fn(() => {
-        throw new Error('oops');
-      });
-      await worker.updateBranch([config]);
-      expect(branchWorker.ensureBranch.mock.calls.length).toBe(0);
-    });
-  });
-  describe('processUpgrades(upgrades)', () => {
-    beforeEach(() => {
-      worker.updateBranch = jest.fn();
-    });
-    it('handles zero upgrades', async () => {
-      await worker.processUpgrades([]);
-      expect(worker.updateBranch.mock.calls.length).toBe(0);
-    });
-    it('handles non-zero upgrades', async () => {
-      await worker.processUpgrades([{ branchName: 'a' }, { branchName: 'b' }]);
-      expect(worker.updateBranch.mock.calls.length).toBe(2);
-    });
-  });
+jest.mock('../../lib/workers/branch');
+jest.mock('../../lib/workers/pr');
+jest.mock('../../lib/api/npm');
+jest.mock('../../lib/helpers/versions');
+
+describe('packageFileWorker', () => {
   describe('findUpgrades(dependencies, config)', () => {
     let config;
     beforeEach(() => {
       config = {};
-      worker.updateBranch = jest.fn();
+      packageFileWorker.updateBranch = jest.fn();
     });
     it('handles null', async () => {
-      const allUpgrades = await worker.findUpgrades([], config);
+      const allUpgrades = await packageFileWorker.findUpgrades([], config);
       expect(allUpgrades).toMatchObject([]);
     });
     it('handles one dep', async () => {
@@ -86,9 +34,21 @@ describe('worker', () => {
       const upgrade = { newVersion: '1.1.0' };
       npmApi.getDependency = jest.fn(() => ({}));
       versionsHelper.determineUpgrades = jest.fn(() => [upgrade]);
-      const allUpgrades = await worker.findUpgrades([dep], config);
+      const allUpgrades = await packageFileWorker.findUpgrades([dep], config);
       expect(allUpgrades).toMatchObject([Object.assign({}, dep, upgrade)]);
     });
+    it('handles no return', async () => {
+      const dep = {
+        depName: 'foo',
+        currentVersion: '1.0.0',
+      };
+      const upgrade = { newVersion: '1.1.0' };
+      npmApi.getDependency = jest.fn(() => ({}));
+      npmApi.getDependency.mockReturnValueOnce(null);
+      versionsHelper.determineUpgrades = jest.fn(() => [upgrade]);
+      const allUpgrades = await packageFileWorker.findUpgrades([dep], config);
+      expect(allUpgrades).toMatchObject([]);
+    });
     it('handles no upgrades', async () => {
       const dep = {
         depName: 'foo',
@@ -96,7 +56,7 @@ describe('worker', () => {
       };
       npmApi.getDependency = jest.fn(() => ({}));
       versionsHelper.determineUpgrades = jest.fn(() => []);
-      const allUpgrades = await worker.findUpgrades([dep], config);
+      const allUpgrades = await packageFileWorker.findUpgrades([dep], config);
       expect(allUpgrades).toMatchObject([]);
     });
   });
@@ -108,7 +68,7 @@ describe('worker', () => {
       deps = [];
     });
     it('handles empty deps', () => {
-      const updatedDeps = worker.assignDepConfigs(config, deps);
+      const updatedDeps = packageFileWorker.assignDepConfigs(config, deps);
       expect(updatedDeps).toMatchObject([]);
     });
     it('handles string deps', () => {
@@ -117,7 +77,7 @@ describe('worker', () => {
       deps.push({
         depName: 'a',
       });
-      const updatedDeps = worker.assignDepConfigs(config, deps);
+      const updatedDeps = packageFileWorker.assignDepConfigs(config, deps);
       expect(updatedDeps).toMatchSnapshot();
     });
     it('handles multiple deps', () => {
@@ -128,7 +88,7 @@ describe('worker', () => {
       deps.push({
         depName: 'b',
       });
-      const updatedDeps = worker.assignDepConfigs(config, deps);
+      const updatedDeps = packageFileWorker.assignDepConfigs(config, deps);
       expect(updatedDeps).toMatchSnapshot();
     });
     it('handles depType config without override', () => {
@@ -143,7 +103,7 @@ describe('worker', () => {
         depName: 'a',
         depType: 'dependencies',
       });
-      const updatedDeps = worker.assignDepConfigs(config, deps);
+      const updatedDeps = packageFileWorker.assignDepConfigs(config, deps);
       expect(updatedDeps).toMatchSnapshot();
     });
     it('handles depType config with override', () => {
@@ -158,7 +118,7 @@ describe('worker', () => {
         depName: 'a',
         depType: 'dependencies',
       });
-      const updatedDeps = worker.assignDepConfigs(config, deps);
+      const updatedDeps = packageFileWorker.assignDepConfigs(config, deps);
       expect(updatedDeps).toMatchSnapshot();
     });
     it('handles package config', () => {
@@ -172,7 +132,7 @@ describe('worker', () => {
       deps.push({
         depName: 'a',
       });
-      const updatedDeps = worker.assignDepConfigs(config, deps);
+      const updatedDeps = packageFileWorker.assignDepConfigs(config, deps);
       expect(updatedDeps).toMatchSnapshot();
     });
     it('package config overrides depType and general config', () => {
@@ -193,7 +153,7 @@ describe('worker', () => {
         depName: 'a',
         depType: 'dependencies',
       });
-      const updatedDeps = worker.assignDepConfigs(config, deps);
+      const updatedDeps = packageFileWorker.assignDepConfigs(config, deps);
       expect(updatedDeps).toMatchSnapshot();
     });
     it('nested package config overrides depType and general config', () => {
@@ -214,7 +174,7 @@ describe('worker', () => {
         depName: 'a',
         depType: 'dependencies',
       });
-      const updatedDeps = worker.assignDepConfigs(config, deps);
+      const updatedDeps = packageFileWorker.assignDepConfigs(config, deps);
       expect(updatedDeps).toMatchSnapshot();
     });
     it('handles regex package pattern', () => {
@@ -237,7 +197,7 @@ describe('worker', () => {
       deps.push({
         depName: 'also-eslint',
       });
-      const updatedDeps = worker.assignDepConfigs(config, deps);
+      const updatedDeps = packageFileWorker.assignDepConfigs(config, deps);
       expect(updatedDeps).toMatchSnapshot();
     });
     it('handles regex wildcard package pattern', () => {
@@ -260,7 +220,7 @@ describe('worker', () => {
       deps.push({
         depName: 'also-eslint',
       });
-      const updatedDeps = worker.assignDepConfigs(config, deps);
+      const updatedDeps = packageFileWorker.assignDepConfigs(config, deps);
       expect(updatedDeps).toMatchSnapshot();
     });
     it('handles non-regex package name', () => {
@@ -283,18 +243,24 @@ describe('worker', () => {
       deps.push({
         depName: 'also-eslint',
       });
-      const updatedDeps = worker.assignDepConfigs(config, deps);
+      const updatedDeps = packageFileWorker.assignDepConfigs(config, deps);
       expect(updatedDeps).toMatchSnapshot();
     });
   });
   describe('getDepTypeConfig(depTypes, depTypeName)', () => {
     it('handles empty depTypes', () => {
-      const depTypeConfig = worker.getDepTypeConfig([], 'dependencies');
+      const depTypeConfig = packageFileWorker.getDepTypeConfig(
+        [],
+        'dependencies'
+      );
       expect(depTypeConfig).toMatchObject({});
     });
     it('handles all strings', () => {
       const depTypes = ['dependencies', 'devDependencies'];
-      const depTypeConfig = worker.getDepTypeConfig(depTypes, 'dependencies');
+      const depTypeConfig = packageFileWorker.getDepTypeConfig(
+        depTypes,
+        'dependencies'
+      );
       expect(depTypeConfig).toMatchObject({});
     });
     it('handles missed object', () => {
@@ -305,7 +271,10 @@ describe('worker', () => {
           foo: 'bar',
         },
       ];
-      const depTypeConfig = worker.getDepTypeConfig(depTypes, 'dependencies');
+      const depTypeConfig = packageFileWorker.getDepTypeConfig(
+        depTypes,
+        'dependencies'
+      );
       expect(depTypeConfig).toMatchObject({});
     });
     it('handles hit object', () => {
@@ -316,11 +285,72 @@ describe('worker', () => {
         },
         'devDependencies',
       ];
-      const depTypeConfig = worker.getDepTypeConfig(depTypes, 'dependencies');
+      const depTypeConfig = packageFileWorker.getDepTypeConfig(
+        depTypes,
+        'dependencies'
+      );
       const expectedResult = {
         foo: 'bar',
       };
       expect(depTypeConfig).toMatchObject(expectedResult);
     });
   });
+  describe('processPackageFile(config)', () => {
+    let config;
+    beforeEach(() => {
+      packageFileWorker.assignDepConfigs = jest.fn(() => []);
+      packageFileWorker.findUpgrades = jest.fn(() => []);
+      packageJsonHelper.extractDependencies = jest.fn(() => []);
+      config = require('../../lib/config/defaults').getConfig();
+      config.api = {
+        getFileJson: jest.fn(() => ({})),
+      };
+      config.logger = logger;
+    });
+    it('returns empty array if no package content', async () => {
+      config.api.getFileJson.mockReturnValueOnce(null);
+      const res = await packageFileWorker.processPackageFile(config);
+      expect(res).toEqual([]);
+    });
+    it('returns empty array if config disabled', async () => {
+      config.api.getFileJson.mockReturnValueOnce({
+        renovate: {
+          enabled: false,
+        },
+      });
+      const res = await packageFileWorker.processPackageFile(config);
+      expect(res).toEqual([]);
+    });
+    it('extracts dependencies for each depType', async () => {
+      config.depTypes = [
+        'dependencies',
+        {
+          depType: 'devDependencies',
+          foo: 'bar',
+        },
+      ];
+      const res = await packageFileWorker.processPackageFile(config);
+      expect(res).toEqual([]);
+      expect(
+        packageJsonHelper.extractDependencies.mock.calls
+      ).toMatchSnapshot();
+    });
+    it('filters dependencies', async () => {
+      packageJsonHelper.extractDependencies.mockReturnValueOnce([
+        {
+          depName: 'a',
+        },
+      ]);
+      packageFileWorker.assignDepConfigs.mockReturnValueOnce(['a']);
+      packageFileWorker.findUpgrades.mockReturnValueOnce(['a']);
+      const res = await packageFileWorker.processPackageFile(config);
+      expect(res).toHaveLength(1);
+      expect(res).toMatchSnapshot();
+    });
+    it('maintains yarn.lock', async () => {
+      config.maintainYarnLock = true;
+      const res = await packageFileWorker.processPackageFile(config);
+      expect(res).toHaveLength(1);
+    });
+  });
 });
diff --git a/test/workers/pr.spec.js b/test/workers/pr.spec.js
index 19d18db1be82508a1aae76858dffcddcc8318fa9..5383334116c83136b124eee648b13ee59c3b84fc 100644
--- a/test/workers/pr.spec.js
+++ b/test/workers/pr.spec.js
@@ -2,6 +2,14 @@ const prWorker = require('../../lib/workers/pr');
 const changelogHelper = require('../../lib/helpers/changelog');
 const defaultConfig = require('../../lib/config/defaults').getConfig();
 
+const bunyan = require('bunyan');
+
+const logger = bunyan.createLogger({
+  name: 'test',
+  stream: process.stdout,
+  level: 'fatal',
+});
+
 jest.mock('../../lib/helpers/changelog');
 changelogHelper.getChangeLog = jest.fn();
 changelogHelper.getChangeLog.mockReturnValue('Mocked changelog');
@@ -27,7 +35,7 @@ changelogHelper.getChangeLogJSON.mockReturnValue({
 });
 
 describe('workers/pr', () => {
-  describe('checkAutoMerge(pr, config)', () => {
+  describe('checkAutoMerge(pr, config, logger)', () => {
     let config;
     let pr;
     beforeEach(() => {
@@ -43,38 +51,38 @@ describe('workers/pr', () => {
       };
     });
     it('should not automerge if not configured', async () => {
-      await prWorker.checkAutoMerge(pr, config);
+      await prWorker.checkAutoMerge(pr, config, logger);
       expect(config.api.mergePr.mock.calls.length).toBe(0);
     });
     it('should automerge if enabled and pr is mergeable', async () => {
       config.automergeEnabled = true;
       pr.mergeable = true;
       config.api.getBranchStatus.mockReturnValueOnce('success');
-      await prWorker.checkAutoMerge(pr, config);
+      await prWorker.checkAutoMerge(pr, config, logger);
       expect(config.api.mergePr.mock.calls.length).toBe(1);
     });
     it('should not automerge if enabled and pr is mergeable but branch status is not success', async () => {
       config.automergeEnabled = true;
       pr.mergeable = true;
       config.api.getBranchStatus.mockReturnValueOnce('pending');
-      await prWorker.checkAutoMerge(pr, config);
+      await prWorker.checkAutoMerge(pr, config, logger);
       expect(config.api.mergePr.mock.calls.length).toBe(0);
     });
     it('should not automerge if enabled and pr is mergeable but unstable', async () => {
       config.automergeEnabled = true;
       pr.mergeable = true;
       pr.mergeable_state = 'unstable';
-      await prWorker.checkAutoMerge(pr, config);
+      await prWorker.checkAutoMerge(pr, config, logger);
       expect(config.api.mergePr.mock.calls.length).toBe(0);
     });
     it('should not automerge if enabled and pr is unmergeable', async () => {
       config.automergeEnabled = true;
       pr.mergeable = false;
-      await prWorker.checkAutoMerge(pr, config);
+      await prWorker.checkAutoMerge(pr, config, logger);
       expect(config.api.mergePr.mock.calls.length).toBe(0);
     });
   });
-  describe('ensurePr(upgrades)', () => {
+  describe('ensurePr(upgrades, logger)', () => {
     let config;
     let existingPr;
     beforeEach(() => {
@@ -106,45 +114,45 @@ This PR has been generated by [Renovate Bot](https://keylocation.sg/our-tech/ren
       config.api.getBranchPr = jest.fn(() => {
         throw new Error('oops');
       });
-      const pr = await prWorker.ensurePr([config]);
+      const pr = await prWorker.ensurePr([config], logger);
       expect(pr).toBe(null);
     });
     it('should return null if waiting for success', async () => {
       config.api.getBranchStatus = jest.fn(() => 'failed');
       config.prCreation = 'status-success';
-      const pr = await prWorker.ensurePr([config]);
+      const pr = await prWorker.ensurePr([config], logger);
       expect(pr).toBe(null);
     });
     it('should create PR if success', async () => {
       config.api.getBranchStatus = jest.fn(() => 'success');
       config.api.getBranchPr = jest.fn();
       config.prCreation = 'status-success';
-      const pr = await prWorker.ensurePr([config]);
+      const pr = await prWorker.ensurePr([config], logger);
       expect(pr).toMatchObject({ displayNumber: 'New Pull Request' });
     });
     it('should return null if waiting for not pending', async () => {
       config.api.getBranchStatus = jest.fn(() => 'pending');
       config.prCreation = 'not-pending';
-      const pr = await prWorker.ensurePr([config]);
+      const pr = await prWorker.ensurePr([config], logger);
       expect(pr).toBe(null);
     });
     it('should create PR if no longer pending', async () => {
       config.api.getBranchStatus = jest.fn(() => 'failed');
       config.api.getBranchPr = jest.fn();
       config.prCreation = 'not-pending';
-      const pr = await prWorker.ensurePr([config]);
+      const pr = await prWorker.ensurePr([config], logger);
       expect(pr).toMatchObject({ displayNumber: 'New Pull Request' });
     });
     it('should create new branch if none exists', async () => {
       config.api.getBranchPr = jest.fn();
-      const pr = await prWorker.ensurePr([config]);
+      const pr = await prWorker.ensurePr([config], logger);
       expect(pr).toMatchObject({ displayNumber: 'New Pull Request' });
     });
     it('should add labels to new PR', async () => {
       config.api.getBranchPr = jest.fn();
       config.api.addLabels = jest.fn();
       config.labels = ['foo'];
-      const pr = await prWorker.ensurePr([config]);
+      const pr = await prWorker.ensurePr([config], logger);
       expect(pr).toMatchObject({ displayNumber: 'New Pull Request' });
       expect(config.api.addLabels.mock.calls.length).toBe(1);
     });
@@ -152,7 +160,7 @@ This PR has been generated by [Renovate Bot](https://keylocation.sg/our-tech/ren
       config.api.getBranchPr = jest.fn();
       config.api.addLabels = jest.fn();
       config.labels = [];
-      const pr = await prWorker.ensurePr([config]);
+      const pr = await prWorker.ensurePr([config], logger);
       expect(pr).toMatchObject({ displayNumber: 'New Pull Request' });
       expect(config.api.addLabels.mock.calls.length).toBe(0);
     });
@@ -162,7 +170,7 @@ This PR has been generated by [Renovate Bot](https://keylocation.sg/our-tech/ren
       config.api.addReviewers = jest.fn();
       config.assignees = ['bar'];
       config.reviewers = ['baz'];
-      const pr = await prWorker.ensurePr([config]);
+      const pr = await prWorker.ensurePr([config], logger);
       expect(pr).toMatchObject({ displayNumber: 'New Pull Request' });
       expect(config.api.addAssignees.mock.calls.length).toBe(1);
       expect(config.api.addReviewers.mock.calls.length).toBe(1);
@@ -174,7 +182,7 @@ This PR has been generated by [Renovate Bot](https://keylocation.sg/our-tech/ren
       config.assignees = ['bar'];
       config.reviewers = ['baz'];
       config.automergeEnabled = true;
-      const pr = await prWorker.ensurePr([config]);
+      const pr = await prWorker.ensurePr([config], logger);
       expect(pr).toMatchObject({ displayNumber: 'New Pull Request' });
       expect(config.api.addAssignees.mock.calls.length).toBe(0);
       expect(config.api.addReviewers.mock.calls.length).toBe(0);
@@ -185,7 +193,7 @@ This PR has been generated by [Renovate Bot](https://keylocation.sg/our-tech/ren
       config.newVersion = '1.1.0';
       config.api.getBranchPr = jest.fn(() => existingPr);
       config.api.updatePr = jest.fn();
-      const pr = await prWorker.ensurePr([config]);
+      const pr = await prWorker.ensurePr([config], logger);
       expect(config.api.updatePr.mock.calls).toMatchSnapshot();
       expect(config.api.updatePr.mock.calls.length).toBe(0);
       expect(pr).toMatchObject(existingPr);
@@ -196,7 +204,7 @@ This PR has been generated by [Renovate Bot](https://keylocation.sg/our-tech/ren
       config.newVersion = '1.2.0';
       config.api.getBranchPr = jest.fn(() => existingPr);
       config.api.updatePr = jest.fn();
-      const pr = await prWorker.ensurePr([config]);
+      const pr = await prWorker.ensurePr([config], logger);
       const updatedPr = Object.assign(existingPr, {
         body:
           'This Pull Request updates dependency dummy from version `1.0.0` to `1.2.0`\n\nNo changelog available',
@@ -208,14 +216,14 @@ This PR has been generated by [Renovate Bot](https://keylocation.sg/our-tech/ren
       config.automergeType = 'branch-push';
       config.api.getBranchStatus.mockReturnValueOnce('failure');
       config.api.getBranchPr = jest.fn();
-      const pr = await prWorker.ensurePr([config]);
+      const pr = await prWorker.ensurePr([config], logger);
       expect(pr).toMatchObject({ displayNumber: 'New Pull Request' });
     });
     it('should return null if branch automerging not failed', async () => {
       config.automergeEnabled = true;
       config.automergeType = 'branch-push';
       config.api.getBranchStatus.mockReturnValueOnce('pending');
-      const pr = await prWorker.ensurePr([config]);
+      const pr = await prWorker.ensurePr([config], logger);
       expect(pr).toBe(null);
     });
   });
diff --git a/test/workers/repository.spec.js b/test/workers/repository.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..f99ddc6252400d96cf281cbeaa5220df85d971b5
--- /dev/null
+++ b/test/workers/repository.spec.js
@@ -0,0 +1,26 @@
+const repositoryWorker = require('../../lib/workers/repository');
+const branchWorker = require('../../lib/workers/branch');
+
+jest.mock('../../lib/workers/branch');
+jest.mock('../../lib/workers/pr');
+jest.mock('../../lib/api/npm');
+jest.mock('../../lib/helpers/versions');
+
+describe('repositoryWorker', () => {
+  describe('processUpgrades(upgrades)', () => {
+    beforeEach(() => {
+      repositoryWorker.updateBranch = jest.fn();
+    });
+    it('handles zero upgrades', async () => {
+      // await repositoryWorker.processUpgrades([]);
+      expect(branchWorker.updateBranch.mock.calls.length).toBe(0);
+    });
+    it('handles non-zero upgrades', async () => {
+      await repositoryWorker.processUpgrades([
+        { branchName: 'a' },
+        { branchName: 'b' },
+      ]);
+      expect(branchWorker.updateBranch.mock.calls.length).toBe(2);
+    });
+  });
+});
diff --git a/yarn.lock b/yarn.lock
index 3678bca9eced0986ee56dc59eea59438ea0dffa6..d589e045d5cbd861a1c8122f41e5bc6533d375ea 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3475,6 +3475,10 @@ tr46@~0.0.3:
   version "0.0.3"
   resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
 
+traverse@0.6.6:
+  version "0.6.6"
+  resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137"
+
 trim-newlines@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"