diff --git a/docs/shareable-configs.md b/docs/shareable-configs.md
new file mode 100644
index 0000000000000000000000000000000000000000..86bae5dcb6ca8f406476955df8238239123b923d
--- /dev/null
+++ b/docs/shareable-configs.md
@@ -0,0 +1,114 @@
+# Preset configs
+
+Renovate uses the term "presets" to refer to shareable config snippets, similar to eslint. Unlike eslint though:
+
+-   Presets may be as small as a list of package names, or as large as a full config
+-   Shared config files can contain many presets
+
+## Preset config URIs
+
+In human-understandable form, the rules are:
+
+-   A full preset URI consists of package name, preset name, and preset parameters, such as `package:preset(param)`
+-   If a package scope is specified and no package, then the package name is assumed to be `renovate-config`, e.g. `@rarkins:webapp` is expanded to `@rarkins/renovate-config:webapp`
+-   If a non-scoped package is specified then it is assumed to have prefix `renovate-config-`. e.g. `rarkins:webapp` is expanded to `renovate-config-rarkins:webapp`
+-   If a package name is specified and no preset name, then `default` is assumed, e.g. `@rarkins` expands in full to `@rarkins/renovate-config:default` and `rarkins` expands in full to `renovate-config-rarkins:default`
+-   There is a special "default" namespace where no package name is necessary. e.g. `:webapp` (not the leading `:`) expands to `renovate-config-default:webapp`
+
+## Supported config syntax
+
+### Scoped
+
+```
+@somescope
+```
+
+This will resolve to use the preset `default` in the npm package `@somescope/renovate-config`.
+
+### Scoped with package name
+
+```
+@somescope/somepackagename
+```
+
+This will resolve to use the preset `default` in the npm package `@somescope/somepackagename`.
+
+### Scoped with preset name
+
+```
+@somescope:webapp
+```
+
+This will resolve to use the preset `webapp` in the npm package `@somescope/renovate-config`.
+
+### Scoped with params
+
+```
+@somescope(eslint, stylelint)
+```
+
+This will resolve to use the preset `default` in the npm package `@somescope/renovate-config` and pass the parameters `eslint` and `stylelint`.
+
+### Scoped with preset name and params
+
+```
+@somescope:webapp(eslint, stylelint)
+```
+
+This will resolve to use the preset `webapp` in the npm package `@somescope/renovate-config` and pass the parameters `eslint` and `stylelint`.
+
+### Scoped with package name and preset name
+
+```
+@somescope/somepackagename:webapp
+```
+
+This will resolve to use the preset `webapp` in the npm package `@somescope/somepackagename`.
+
+### Scoped with package name and preset name and params
+
+```
+@somescope/somepackagename:webapp(eslint, stylelint)
+```
+
+This will resolve to use the preset `webapp` in the npm package `@somescope/somepackagename` and pass the parameters `eslint` and `stylelint`.
+
+### Non-scoped short with preset name
+
+Note: if using non-scoped packages, a preset name is mandatory.
+
+```
+somepackagename:default
+```
+
+This will resolve to use the preset `default` in the npm package `renovate-config-somepackagename`.
+
+### Non-scoped short with preset name and params
+
+Note: if using non-scoped packages, a preset name is mandatory.
+
+```
+somepackagename:default(eslint)
+```
+
+This will resolve to use the preset `default` in the npm package `renovate-config-somepackagename` with param `eslint`.
+
+### Non-scoped full with preset name
+
+Note: if using non-scoped packages, a preset name is mandatory.
+
+```
+renovate-config-somepackagename:default
+```
+
+This will resolve to use the preset `default` in the npm package `renovate-config-somepackagename`.
+
+### Non-scoped full with preset name and params
+
+Note: if using non-scoped packages, a preset name is mandatory.
+
+```
+renovate-config-somepackagename:default(eslint)
+```
+
+This will resolve to use the preset `default` in the npm package `renovate-config-somepackagename` and param `eslint`.
diff --git a/lib/api/npm.js b/lib/api/npm.js
index 7eba27749e18d722653891957ed12490ed314aa9..aafe80f7b6460b433eb854ff6b6c34f2fd14be73 100644
--- a/lib/api/npm.js
+++ b/lib/api/npm.js
@@ -70,6 +70,8 @@ async function getDependency(name, logger) {
       repositoryUrl,
       versions: res.body.versions,
       'dist-tags': res.body['dist-tags'],
+      'renovate-config':
+        res.body.versions[res.body['dist-tags'].latest]['renovate-config'],
     };
     Object.keys(dep.versions).forEach(version => {
       // We don't use any of the version payload currently
diff --git a/lib/config/definitions.js b/lib/config/definitions.js
index 7409ca3d34501fb456a126fc181675591dc35550..7d78c42c01aba3938e97518972ff59304ff3a4a7 100644
--- a/lib/config/definitions.js
+++ b/lib/config/definitions.js
@@ -17,6 +17,23 @@ module.exports = {
 };
 
 const options = [
+  {
+    name: 'extends',
+    description: 'Configuration presets to use/extend',
+    stage: 'package',
+    type: 'list',
+    allowString: true,
+    cli: false,
+  },
+  {
+    name: 'description',
+    description: 'Plain text description for a config or preset',
+    type: 'list',
+    allowString: true,
+    mergeable: true,
+    cli: false,
+    env: false,
+  },
   {
     name: 'enabled',
     description: 'Enable or disable renovate',
@@ -204,6 +221,7 @@ const options = [
     type: 'list',
     allowString: true,
     stage: 'depType',
+    mergeable: true,
     cli: false,
     env: false,
   },
@@ -214,6 +232,7 @@ const options = [
     type: 'list',
     allowString: true,
     stage: 'depType',
+    mergeable: true,
     cli: false,
     env: false,
   },
@@ -224,6 +243,7 @@ const options = [
     type: 'list',
     allowString: true,
     stage: 'depType',
+    mergeable: true,
     cli: false,
     env: false,
   },
@@ -234,6 +254,7 @@ const options = [
     type: 'list',
     allowString: true,
     stage: 'depType',
+    mergeable: true,
     cli: false,
     env: false,
   },
@@ -243,7 +264,6 @@ const options = [
     description: 'Convert ranged versions in package.json to pinned versions',
     stage: 'package',
     type: 'boolean',
-    onboarding: true,
   },
   {
     name: 'separateMajorReleases',
diff --git a/lib/config/index.js b/lib/config/index.js
index 50429d6e51d2f32b8b4084cb8100812536325a02..72085ab93469ae6522e1433c4e531aefdaa3a8b9 100644
--- a/lib/config/index.js
+++ b/lib/config/index.js
@@ -15,7 +15,6 @@ module.exports = {
   parseConfigs,
   mergeChildConfig,
   filterConfig,
-  getOnboardingConfig,
 };
 
 async function parseConfigs(env, argv) {
@@ -116,8 +115,12 @@ function mergeChildConfig(parentConfig, childConfig) {
   logger.trace({ parentConfig, childConfig }, `mergeChildConfig`);
   const config = { ...parentConfig, ...childConfig };
   for (const option of definitions.getOptions()) {
-    if (option.mergeable && childConfig[option.name]) {
-      logger.debug(`mergeable option: ${option.name}`);
+    if (
+      option.mergeable &&
+      childConfig[option.name] &&
+      parentConfig[option.name]
+    ) {
+      logger.trace(`mergeable option: ${option.name}`);
       if (option.type === 'list') {
         config[option.name] = (parentConfig[option.name] || [])
           .concat(config[option.name] || []);
@@ -127,7 +130,10 @@ function mergeChildConfig(parentConfig, childConfig) {
           ...childConfig[option.name],
         };
       }
-      logger.debug({ option: config[option.name] }, `config.${option.name}`);
+      logger.trace(
+        { result: config[option.name] },
+        `Merged config.${option.name}`
+      );
     }
   }
   return config;
@@ -154,13 +160,3 @@ function filterConfig(inputConfig, targetStage) {
   }
   return outputConfig;
 }
-
-function getOnboardingConfig(repoConfig) {
-  const config = {};
-  for (const option of definitions.getOptions()) {
-    if (option.stage !== 'global' && option.onboarding === true) {
-      config[option.name] = repoConfig[option.name];
-    }
-  }
-  return config;
-}
diff --git a/lib/config/presets.js b/lib/config/presets.js
new file mode 100644
index 0000000000000000000000000000000000000000..3f373491be82e910a55226568ae7facd019048d0
--- /dev/null
+++ b/lib/config/presets.js
@@ -0,0 +1,193 @@
+const configParser = require('./index');
+const massage = require('./massage');
+const npm = require('../api/npm');
+
+module.exports = {
+  resolveConfigPresets,
+  replaceArgs,
+  parsePreset,
+  getPreset,
+};
+
+async function resolveConfigPresets(
+  inputConfig,
+  logger = inputConfig.logger,
+  existingPresets = []
+) {
+  logger.trace(
+    { config: inputConfig, existingPresets },
+    'resolveConfigPresets'
+  );
+  let config = {};
+  // First, merge all the preset configs from left to right
+  if (inputConfig.extends) {
+    logger.debug('Found presets');
+    for (const preset of inputConfig.extends) {
+      // istanbul ignore if
+      if (existingPresets.indexOf(preset) !== -1) {
+        logger.warn(`Already seen preset ${preset} in ${existingPresets}`);
+      } else {
+        logger.debug(`Resolving preset "${preset}"`);
+        const presetConfig = await resolveConfigPresets(
+          await getPreset(preset, logger),
+          logger,
+          existingPresets.concat([preset])
+        );
+        config = configParser.mergeChildConfig(config, presetConfig);
+      }
+    }
+  }
+  logger.trace({ config }, `Post-preset resolve config`);
+  // Now assign "regular" config on top
+  config = configParser.mergeChildConfig(config, inputConfig);
+  delete config.extends;
+  logger.trace({ config }, `Post-merge resolve config`);
+  for (const key of Object.keys(config)) {
+    const val = config[key];
+    if (isObject(val) && key !== 'logger') {
+      // Resolve nested objects
+      logger.trace(`Resolving object "${key}"`);
+      config[key] = await resolveConfigPresets(val, logger, existingPresets);
+    } else if (Array.isArray(val)) {
+      // Resolve nested objects inside arrays
+      config[key] = [];
+      for (const element of val) {
+        if (isObject(element)) {
+          config[key].push(
+            await resolveConfigPresets(element, logger, existingPresets)
+          );
+        } else {
+          config[key].push(element);
+        }
+      }
+    }
+  }
+  logger.debug({ config: inputConfig }, 'Input config');
+  logger.debug({ config }, 'Resolved config');
+  return config;
+}
+
+function replaceArgs(obj, argMapping) {
+  if (typeof obj === 'string') {
+    let returnStr = obj;
+    for (const arg of Object.keys(argMapping)) {
+      const re = new RegExp(`{{${arg}}}`, 'g');
+      returnStr = returnStr.replace(re, argMapping[arg]);
+    }
+    return returnStr;
+  }
+  if (isObject(obj)) {
+    const returnObj = {};
+    for (const key of Object.keys(obj)) {
+      returnObj[key] = replaceArgs(obj[key], argMapping);
+    }
+    return returnObj;
+  }
+  if (Array.isArray(obj)) {
+    const returnArray = [];
+    for (const item of obj) {
+      returnArray.push(replaceArgs(item, argMapping));
+    }
+    return returnArray;
+  }
+  return obj;
+}
+
+function parsePreset(input) {
+  let str = input;
+  let packageName;
+  let presetName;
+  let params;
+  if (str.includes('(')) {
+    params = str
+      .slice(str.indexOf('(') + 1, -1)
+      .split(',')
+      .map(elem => elem.trim());
+    str = str.slice(0, str.indexOf('('));
+  }
+  if (str[0] === ':') {
+    // default namespace
+    packageName = 'renovate-config-default';
+    presetName = str.slice(1);
+  } else if (str[0] === '@') {
+    // scoped namespace
+    packageName = str.match(/(@.*?)(:|$)/)[1];
+    str = str.slice(packageName.length);
+    if (!packageName.includes('/')) {
+      packageName += '/renovate-config';
+    }
+    if (str === '') {
+      presetName = 'default';
+    } else {
+      presetName = str.slice(1);
+    }
+  } else {
+    // non-scoped namespace
+    packageName = str.match(/(.*?)(:|$)/)[1];
+    presetName = str.slice(packageName.length + 1);
+    if (packageName.indexOf('renovate-config-') !== 0) {
+      packageName = `renovate-config-${packageName}`;
+    }
+    if (presetName === '') {
+      presetName = 'default';
+    }
+  }
+  return { packageName, presetName, params };
+}
+
+async function getPreset(preset, logger) {
+  logger.debug(`getPreset(${preset})`);
+  const { packageName, presetName, params } = parsePreset(preset);
+  let presetConfig;
+  try {
+    const dep = await npm.getDependency(packageName, logger);
+    if (!dep) {
+      logger.warn(`Failed to look up preset packageName ${packageName}`);
+      return {};
+    }
+    if (!dep['renovate-config']) {
+      logger.warn(`Package ${packageName} has no renovate-config`);
+      return {};
+    }
+    presetConfig = dep['renovate-config'][presetName];
+  } catch (err) {
+    logger.warn({ err }, `Failed to look up package ${packageName}`);
+  }
+  if (!presetConfig) {
+    logger.warn(`Cannot find preset ${preset}`);
+    return {};
+  }
+  logger.debug({ presetConfig }, `Found preset ${preset}`);
+  if (params) {
+    const argMapping = {};
+    for (const [index, value] of params.entries()) {
+      argMapping[`arg${index}`] = value;
+    }
+    presetConfig = replaceArgs(presetConfig, argMapping);
+  }
+  logger.debug({ presetConfig }, `Applied params to preset ${preset}`);
+  const presetKeys = Object.keys(presetConfig);
+  if (
+    presetKeys.length === 2 &&
+    presetKeys.includes('description') &&
+    presetKeys.includes('extends')
+  ) {
+    // preset is just a collection of other presets
+    delete presetConfig.description;
+  }
+  const packageListKeys = [
+    'description',
+    'packageNames',
+    'excludePackageNames',
+    'packagePatterns',
+    'excludePackagePatterns',
+  ];
+  if (presetKeys.every(key => packageListKeys.includes(key))) {
+    delete presetConfig.description;
+  }
+  return massage.massageConfig(presetConfig);
+}
+
+function isObject(obj) {
+  return Object.prototype.toString.call(obj) === '[object Object]';
+}
diff --git a/lib/workers/repository/index.js b/lib/workers/repository/index.js
index a7cf0732d7cbd18c68cc52d6c098b915d81bdbae..b0ef9c42444a3686bd43d9144bb48231c7bd68ff 100644
--- a/lib/workers/repository/index.js
+++ b/lib/workers/repository/index.js
@@ -1,3 +1,4 @@
+const presets = require('../../config/presets');
 // Workers
 const branchWorker = require('../branch');
 // children
@@ -56,8 +57,14 @@ async function renovateRepository(repoConfig, token) {
           .length} package files: ${config.packageFiles}`
       );
     }
+    logger.debug('Resolving package files and content');
     config = await apis.resolvePackageFiles(config);
     logger.trace({ config }, 'post-packageFiles config');
+    logger.debug('Resolving config presets');
+    config = await presets.resolveConfigPresets(config);
+    logger.trace({ config }, 'post-presets config');
+    // TODO: why is this fix needed?!
+    config.logger = logger;
     config.repoIsOnboarded = await onboarding.getOnboardingStatus(config);
     if (!config.repoIsOnboarded) {
       config.contentBaseBranch = `${config.branchPrefix}configure`;
@@ -84,10 +91,12 @@ async function renovateRepository(repoConfig, token) {
         }
       }
       config = await apis.resolvePackageFiles(config);
+      config = await presets.resolveConfigPresets(config);
+      config.logger = logger;
       logger.trace({ config }, 'onboarding config');
     }
     const allUpgrades = await upgrades.determineRepoUpgrades(config);
-    const res = await upgrades.branchifyUpgrades(allUpgrades, config.logger);
+    const res = await upgrades.branchifyUpgrades(allUpgrades, logger);
     config.errors = config.errors.concat(res.errors);
     config.warnings = config.warnings.concat(res.warnings);
     const branchUpgrades = res.upgrades;
@@ -113,7 +122,7 @@ async function renovateRepository(repoConfig, token) {
   } catch (err) {
     // Swallow this error so that other repositories can be processed
     if (err.message === 'uninitiated') {
-      logger.info('Repository is unitiated - skipping');
+      logger.info('Repository is uninitiated - skipping');
     } else {
       logger.error(`Failed to process repository: ${err.message}`);
       logger.debug({ err });
diff --git a/lib/workers/repository/onboarding.js b/lib/workers/repository/onboarding.js
index 664eaf2dfaaa61e5b072da0e707bac6476be489e..881c1daf212ac3825c5571618cb8cba129c69956 100644
--- a/lib/workers/repository/onboarding.js
+++ b/lib/workers/repository/onboarding.js
@@ -1,7 +1,5 @@
 const stringify = require('json-stringify-pretty-compact');
 
-const configParser = require('../../config');
-
 const onboardPrTitle = 'Configure Renovate';
 
 module.exports = {
@@ -24,18 +22,14 @@ async function isRepoPrivate(config) {
 
 async function createBranch(config) {
   const onboardBranchName = `${config.branchPrefix}configure`;
-  const onboardingConfig = configParser.getOnboardingConfig(config);
+  const onboardingConfig = {};
   const repoIsPrivate = await module.exports.isRepoPrivate(config);
   if (repoIsPrivate) {
-    config.logger.debug('Repo is private - pinning dependencies versions');
+    config.logger.debug('Repo is private - setting to app type');
+    onboardingConfig.extends = ['app'];
   } else {
-    config.logger.debug('Repo is not private - unpinning versions');
-    onboardingConfig.dependencies = {
-      pinVersions: false,
-    };
-  }
-  if (config.foundNodeModules) {
-    onboardingConfig.ignoreNodeModules = true;
+    config.logger.debug('Repo is not private - setting to library');
+    onboardingConfig.extends = ['library'];
   }
   const onboardingConfigString = `${stringify(onboardingConfig)}\n`;
   const existingContent = await config.api.getFileContent(
@@ -70,6 +64,26 @@ async function createBranch(config) {
 async function ensurePr(config, branchUpgrades) {
   const warnings = config.warnings;
   const errors = config.errors;
+  const description = config.description || [];
+  if (config.assignees && config.assignees.length) {
+    const assignees = config.assignees.map(
+      assignee => (assignee[0] === '@' ? assignee : `@${assignee}`)
+    );
+    description.push(`Assign PRs to ${assignees.join(' and ')}`);
+  }
+  if (config.labels && config.labels.length) {
+    let desc = 'Apply label';
+    if (config.labels.length > 1) {
+      desc += 's';
+    }
+    desc += ` ${config.labels
+      .map(label => `<code>${label}</code>`)
+      .join(' and ')} to PRs`;
+    description.push(desc);
+  }
+  if (config.schedule && config.schedule.length) {
+    description.push(`Run Renovate on following schedule: ${config.schedule}`);
+  }
   let prBody = `Welcome to [Renovate](https://renovateapp.com)!
 
 This is an onboarding PR to help you understand and configure Renovate before any regular Pull Requests begin. Once you close this Pull Request, Renovate will begin keeping your dependencies up-to-date via automated Pull Requests.
@@ -105,7 +119,7 @@ Alternatively, you can add the same configuration settings into a "renovate" sec
     prBody = prBody.replace('---', prWarnings);
   }
   if (errors.length) {
-    let prErrors = `---\n\n## Errors (${errors.length})\n\n`;
+    let prErrors = `---\n\n### Errors (${errors.length})\n\n`;
     prErrors += `Renovate has raised errors when processing this repository that you should fix before merging or closing this PR.
 
 Please make any fixes in _this branch_.
@@ -118,6 +132,16 @@ Please make any fixes in _this branch_.
     prErrors += '\n\n---';
     prBody = prBody.replace('---', prErrors);
   }
+  if (description.length) {
+    let configDesc = `---\n\n## Configuration Summary\n\nBased on the currently configured presets, Renovate will:\n<ul>\n`;
+    configDesc +=
+      '  <li>Start dependency updates once this Configure Renovate PR is merged or closed</li>\n';
+    description.forEach(desc => {
+      configDesc += `  <li>${desc}</li>\n`;
+    });
+    configDesc += '\n</ul>\n\n---';
+    prBody = prBody.replace('---', configDesc);
+  }
 
   // Describe base branch only if it's configured
   let baseBranchDesc = '';
diff --git a/test/_fixtures/npm/renovate-config-default.json b/test/_fixtures/npm/renovate-config-default.json
new file mode 100644
index 0000000000000000000000000000000000000000..fd8911cae9d6c0b8a6532e92ac85640e31695c37
--- /dev/null
+++ b/test/_fixtures/npm/renovate-config-default.json
@@ -0,0 +1 @@
+{"_id":"renovate-config-default","_rev":"2-ec1f8666a9a8ece32143067c41ddceb5","name":"renovate-config-default","description":"Default preset configs for Renovate","dist-tags":{"latest":"0.0.1"},"versions":{"0.0.0":{"name":"renovate-config-default","description":"Default preset configs for Renovate","author":{"name":"Rhys Arkins","email":"rhys@arkins.net"},"license":"MIT","homepage":"https://github.com/singapore/renovate-config","repository":{"type":"git","url":"git+https://rarkins@github.com/singapore/renovate-config.git"},"bugs":{"url":"https://github.com/singapore/renovate-config/issues"},"version":"0.0.0","scripts":{"np":"np","pretty":"prettier-package-json --write ./package.json"},"devDependencies":{"np":"2.16.0","prettier-package-json":"1.4.0"},"renovate-config":{"enableRenovate":{"description":"Enable renovate","enabled":true},"disableRenovate":{"description":"Disable renovate","enabled":false},"scheduleMondayMornings":{"description":"Schedule to run Mondays before 5am","schedule":"On mondays before 5am"},"includeNodeModules":{"description":"Include <code>package.json</code> files found within <code>node_modules</code> folders","ignoreNodeModules":false},"pinVersions":{"description":"Use version pinning (maintain a single version only and not semver ranges)","pinVersions":true},"preserveSemverRanges":{"description":"Preserve (but continue to upgrade) any existing semver ranges","pinVersions":false},"pinOnlyDevDependencies":{"description":"Pin dependency versions for <code>devDependencies</code> and retain semver ranges for others","dependencies":{"extends":":preserveSemverRanges"},"devDependencies":{"extends":":pinVersions"},"optionalDependencies":{"extends":":preserveSemverRanges"},"peerDependencies":{"extends":":preserveSemverRanges"}},"separateMajorReleases":{"description":"Separate major versions of dependencies into individual branches/PRs","separateMajorReleases":true},"separatePatchReleases":{"description":"Separate patch and minor releases of dependencies into separate PRs","separatePatchReleases":true},"combinePatchMinorReleases":{"description":"Use the same branch/PR for both patch and minor upgrades of a dependency","separatePatchReleases":false},"renovatePrefix":{"description":"Use <code>renovate/</code> as prefix for all branch names","branchprefix":"renovate/"},"semanticPrefixChore":{"description":"Use <code>chore(deps):</code> as semantic prefix for commit messages and PR titles","semanticPrefix":"chore(deps):"},"semanticPrefixFix":{"description":"Use <code>fix(deps):</code> as semantic prefix for commit messages and PR titles","semanticPrefix":"fix(deps):"},"disablePeerDependencies":{"description":"Do not renovate <code>peerDependencies</code> versions/ranges","peerDependencies":{"enabled":false}},"semanticPrefixFixDepsChoreOthers":{"description":"If semantic commits detected, use <code>fix(deps):</code> for dependencies and <code>chore(deps):</code> for all others","dependencies":{"extends":":semanticPrefixFix"},"devDependencies":{"extends":":semanticPrefixChore"},"optionalDependencies":{"extends":":semanticPrefixChore"},"peerDependencies":{"extends":":semanticPrefixChore"}},"unpublishSafe":{"description":"Set a status check to warn when upgrades <  24 hours old might get unpublished","unpublishSafe":true},"unpublishSafeDisabled":{"description":"Create branches/PRs for dependency upgrades as soon as they're available","unpublishSafe":false},"prImmediately":{"description":"Raise PRs immediately (after branch is created)","prCreation":"immediate"},"prNotPending":{"description":"Wait until branch tests have passed or failed before creating the PR","prCreation":"not-pending"},"automergeDisabled":{"description":"Do not automerge any upgrades - wait for humans to merge PRs","automerge":"none"},"automergePatch":{"description":"Automerge patch upgrades if they pass tests","automerge":"patch"},"automergeMinor":{"description":"Automerge patch or minor upgrades if they pass tests","automerge":"minor"},"automergeMajor":{"description":"Automerge all upgrades (inluding major) if they pass tests","automerge":"any"},"automergeBranchMergeCommit":{"description":"If automerging, perform a merge-commit on branch (no PR)","automergeType":"branch-merge-commit"},"automergeBranchPush":{"description":"If automerging, push the new commit directly to base branch (no PR)","automergeType":"branch-push"},"automergePr":{"description":"Raise a PR first before any automerging","automergeType":"pr"},"automergeRequireAllStatusChecks":{"description":"Require all status checks to pass before any automerging","requiredStatusChecks":[]},"maintainLockFilesDisabled":{"description":"Update existing lock files only when <code>package.json</code> is modified","lockFileMaintenance":{"enabled":false}},"maintainLockFilesWeekly":{"description":"Run lock file maintenance (updates) early Monday mornings","lockFileMaintenance":{"enabled":true,"extends":":scheduleMondayMornings"}},"ignoreUnstable":{"description":"Only upgrade to stable npm versions","ignoreUnstable":true},"respectLatest":{"description":"Do not upgrade versions past the \"latest\" tag in npm registry","respectLatest":true},"automergeLinters":{"description":"Update lint packages automatically if tests pass","packageRules":[{"extends":["packages:linters"],"automerge":"any"}]},"doNotPinPackage":{"description":"Disable version pinning for <code>{{arg0}}</code>","packageRules":[{"packageNames":["{{arg0}}"],"pinVersions":false}]},"group":{"description":"Group {{arg1}} packages into same branch/PR","packageRules":[{"extends":["{{arg0}}"],"groupName":"{{arg1}}"}]},"base":{"description":"Default base configuration for repositories","extends":[":separateMajorReleases",":combinePatchMinorReleases",":ignoreUnstable",":respectLatest",":unpublishSafeDisabled",":prNotPending",":renovatePrefix",":semanticPrefixFixDepsChoreOthers",":automergeDisabled",":maintainLockFilesDisabled"]},"app":{"description":"Default configuration for webapps","extends":[":pinVersions",":base"]},"library":{"description":"Default configuration for libraries","extends":[":pinOnlyDevDependencies",":base"]}},"_id":"renovate-config-default@0.0.0","_npmVersion":"5.3.0","_nodeVersion":"6.11.1","_npmUser":{"name":"rarkins","email":"rhys@keylocation.sg"},"dist":{"integrity":"sha512-ajKx6jy0PecwxQa2UrR3PXmHMYBbPc2l4qVXOzshC5tWe5KKVA5/ny1Yjv5rTBJ62xuUh2d6ikXd0qeYeKOfqQ==","shasum":"5b88bf0c205b72579098926cf338c53271e97455","tarball":"https://registry.npmjs.org/renovate-config-default/-/renovate-config-default-0.0.0.tgz"},"maintainers":[{"name":"rarkins","email":"rhys@keylocation.sg"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/renovate-config-default-0.0.0.tgz_1502884367812_0.5453649370465428"},"directories":{}},"0.0.1":{"name":"renovate-config-default","description":"Default preset configs for Renovate","author":{"name":"Rhys Arkins","email":"rhys@arkins.net"},"license":"MIT","homepage":"https://github.com/singapore/renovate-config","repository":{"type":"git","url":"git+https://rarkins@github.com/singapore/renovate-config.git"},"bugs":{"url":"https://github.com/singapore/renovate-config/issues"},"version":"0.0.1","scripts":{"np":"np","pretty":"prettier-package-json --write ./package.json"},"devDependencies":{"np":"2.16.0","prettier-package-json":"1.4.0"},"renovate-config":{"enableRenovate":{"description":"Enable renovate","enabled":true},"disableRenovate":{"description":"Disable renovate","enabled":false},"scheduleMondayMornings":{"description":"Schedule to run Mondays before 5am","schedule":"On mondays before 5am"},"includeNodeModules":{"description":"Include <code>package.json</code> files found within <code>node_modules</code> folders","ignoreNodeModules":false},"pinVersions":{"description":"Use version pinning (maintain a single version only and not semver ranges)","pinVersions":true},"preserveSemverRanges":{"description":"Preserve (but continue to upgrade) any existing semver ranges","pinVersions":false},"pinOnlyDevDependencies":{"description":"Pin dependency versions for <code>devDependencies</code> and retain semver ranges for others","dependencies":{"extends":":preserveSemverRanges"},"devDependencies":{"extends":":pinVersions"},"optionalDependencies":{"extends":":preserveSemverRanges"},"peerDependencies":{"extends":":preserveSemverRanges"}},"separateMajorReleases":{"description":"Separate major versions of dependencies into individual branches/PRs","separateMajorReleases":true},"separatePatchReleases":{"description":"Separate patch and minor releases of dependencies into separate PRs","separatePatchReleases":true},"combinePatchMinorReleases":{"description":"Use the same branch/PR for both patch and minor upgrades of a dependency","separatePatchReleases":false},"renovatePrefix":{"description":"Use <code>renovate/</code> as prefix for all branch names","branchprefix":"renovate/"},"semanticPrefixChore":{"description":"Use <code>chore(deps):</code> as semantic prefix for commit messages and PR titles","semanticPrefix":"chore(deps):"},"semanticPrefixFix":{"description":"Use <code>fix(deps):</code> as semantic prefix for commit messages and PR titles","semanticPrefix":"fix(deps):"},"disablePeerDependencies":{"description":"Do not renovate <code>peerDependencies</code> versions/ranges","peerDependencies":{"enabled":false}},"semanticPrefixFixDepsChoreOthers":{"description":"If semantic commits detected, use <code>fix(deps):</code> for dependencies and <code>chore(deps):</code> for all others","dependencies":{"extends":":semanticPrefixFix"},"devDependencies":{"extends":":semanticPrefixChore"},"optionalDependencies":{"extends":":semanticPrefixChore"},"peerDependencies":{"extends":":semanticPrefixChore"}},"unpublishSafe":{"description":"Set a status check to warn when upgrades <  24 hours old might get unpublished","unpublishSafe":true},"unpublishSafeDisabled":{"description":"Create branches/PRs for dependency upgrades as soon as they're available","unpublishSafe":false},"prImmediately":{"description":"Raise PRs immediately (after branch is created)","prCreation":"immediate"},"prNotPending":{"description":"Wait until branch tests have passed or failed before creating the PR","prCreation":"not-pending"},"automergeDisabled":{"description":"Do not automerge any upgrades - wait for humans to merge PRs","automerge":"none"},"automergePatch":{"description":"Automerge patch upgrades if they pass tests","automerge":"patch"},"automergeMinor":{"description":"Automerge patch or minor upgrades if they pass tests","automerge":"minor"},"automergeMajor":{"description":"Automerge all upgrades (inluding major) if they pass tests","automerge":"any"},"automergeBranchMergeCommit":{"description":"If automerging, perform a merge-commit on branch (no PR)","automergeType":"branch-merge-commit"},"automergeBranchPush":{"description":"If automerging, push the new commit directly to base branch (no PR)","automergeType":"branch-push"},"automergePr":{"description":"Raise a PR first before any automerging","automergeType":"pr"},"automergeRequireAllStatusChecks":{"description":"Require all status checks to pass before any automerging","requiredStatusChecks":[]},"maintainLockFilesDisabled":{"description":"Update existing lock files only when <code>package.json</code> is modified","lockFileMaintenance":{"enabled":false}},"maintainLockFilesWeekly":{"description":"Run lock file maintenance (updates) early Monday mornings","lockFileMaintenance":{"enabled":true,"extends":":scheduleMondayMornings"}},"ignoreUnstable":{"description":"Only upgrade to stable npm versions","ignoreUnstable":true},"respectLatest":{"description":"Do not upgrade versions past the \"latest\" tag in npm registry","respectLatest":true},"automergeLinters":{"description":"Update lint packages automatically if tests pass","packageRules":[{"extends":["packages:linters"],"automerge":"any"}]},"doNotPinPackage":{"description":"Disable version pinning for <code>{{arg0}}</code>","packageRules":[{"packageNames":["{{arg0}}"],"pinVersions":false}]},"group":{"description":"Group {{arg1}} packages into same branch/PR","packageRules":[{"extends":["{{arg0}}"],"groupName":"{{arg1}}"}]},"base":{"description":"Default base configuration for repositories","extends":[":separateMajorReleases",":combinePatchMinorReleases",":ignoreUnstable",":respectLatest",":unpublishSafeDisabled",":prNotPending",":renovatePrefix",":semanticPrefixFixDepsChoreOthers",":automergeDisabled",":maintainLockFilesDisabled"]},"app":{"description":"Default configuration for webapps","extends":[":pinVersions",":base"]},"library":{"description":"Default configuration for libraries","extends":[":pinOnlyDevDependencies",":base"]}},"_id":"renovate-config-default@0.0.1","_npmVersion":"5.3.0","_nodeVersion":"6.11.1","_npmUser":{"name":"rarkins","email":"rhys@keylocation.sg"},"dist":{"integrity":"sha512-FWr3t/2hO5LEP2SvAFCpGh753jNK01aJf8kyhsRlfCSrvJVajh26ArVm/lKj2FmfKTV6F8ddLyYXxp05s+j/kQ==","shasum":"cb64e8980f499325d3a58fb43d35527866ab1639","tarball":"https://registry.npmjs.org/renovate-config-default/-/renovate-config-default-0.0.1.tgz"},"maintainers":[{"name":"rarkins","email":"rhys@keylocation.sg"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/renovate-config-default-0.0.1.tgz_1502884421840_0.5322694149799645"},"directories":{}}},"readme":"ERROR: No README data found!","maintainers":[{"name":"rarkins","email":"rhys@keylocation.sg"}],"time":{"modified":"2017-08-16T11:53:42.816Z","created":"2017-08-16T11:52:48.679Z","0.0.0":"2017-08-16T11:52:48.679Z","0.0.1":"2017-08-16T11:53:42.816Z"},"homepage":"https://github.com/singapore/renovate-config","repository":{"type":"git","url":"git+https://rarkins@github.com/singapore/renovate-config.git"},"author":{"name":"Rhys Arkins","email":"rhys@arkins.net"},"bugs":{"url":"https://github.com/singapore/renovate-config/issues"},"license":"MIT","readmeFilename":"","_attachments":{}}
\ No newline at end of file
diff --git a/test/_fixtures/npm/renovate-config-packages.json b/test/_fixtures/npm/renovate-config-packages.json
new file mode 100644
index 0000000000000000000000000000000000000000..d14d52f3146cf4be44cc5c5953265665bd91ea17
--- /dev/null
+++ b/test/_fixtures/npm/renovate-config-packages.json
@@ -0,0 +1 @@
+{"_id":"renovate-config-packages","_rev":"1-e8da83ccca88e942b34ef9dcb600f5e1","name":"renovate-config-packages","description":"Preset packages configs for Renovate","dist-tags":{"latest":"0.0.1"},"versions":{"0.0.1":{"name":"renovate-config-packages","description":"Preset packages configs for Renovate","author":{"name":"Rhys Arkins","email":"rhys@arkins.net"},"license":"MIT","homepage":"https://github.com/singapore/renovate-config","bugs":{"url":"https://github.com/singapore/renovate-config/issues"},"version":"0.0.1","scripts":{"np":"np","pretty":"prettier-package-json --write ./package.json"},"devDependencies":{"np":"2.16.0","prettier-package-json":"1.4.0"},"renovate-config":{"angularJs":{"description":"All angular.js packages","packageNames":["angular","angular-animate","angular-scroll","angular-sanitize"]},"mapbox":{"description":"All mapbox-related packages","packagePatterns":["^(leaflet|mapbox)"]},"eslint":{"description":"All eslint packages","packagePatterns":["^eslint"]},"stylelint":{"description":"All stylelint packages","packagePatterns":["^stylelint"]},"linters":{"description":"All lint-related packages","extends":["packages:eslint","packages:stylelint"],"packageNames":["remark-lint"]}},"_id":"renovate-config-packages@0.0.1","_npmVersion":"5.3.0","_nodeVersion":"6.11.1","_npmUser":{"name":"rarkins","email":"rhys@keylocation.sg"},"dist":{"integrity":"sha512-I0aPz23zHCkzHVgv9Nuums91a81H7kyVeunuP6I6Y/xpUqQaNTSJw1/YRJKIItMMpyvH0WKIkDlug4i0+npHcQ==","shasum":"3f3499e6c17be3c7bc5c22eed0cae8d493211f94","tarball":"https://registry.npmjs.org/renovate-config-packages/-/renovate-config-packages-0.0.1.tgz"},"maintainers":[{"name":"rarkins","email":"rhys@keylocation.sg"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/renovate-config-packages-0.0.1.tgz_1502884315914_0.8533324550371617"},"directories":{}}},"readme":"ERROR: No README data found!","maintainers":[{"name":"rarkins","email":"rhys@keylocation.sg"}],"time":{"modified":"2017-08-16T11:51:56.793Z","created":"2017-08-16T11:51:56.793Z","0.0.1":"2017-08-16T11:51:56.793Z"},"homepage":"https://github.com/singapore/renovate-config","author":{"name":"Rhys Arkins","email":"rhys@arkins.net"},"bugs":{"url":"https://github.com/singapore/renovate-config/issues"},"license":"MIT","readmeFilename":"","_attachments":{}}
diff --git a/test/api/__snapshots__/npm.spec.js.snap b/test/api/__snapshots__/npm.spec.js.snap
index 9618f3e9d86c26ac4032be192beb58cbffc05a68..6beaef77161449a9cb73182fd88ec39f3c4f11d6 100644
--- a/test/api/__snapshots__/npm.spec.js.snap
+++ b/test/api/__snapshots__/npm.spec.js.snap
@@ -2,9 +2,12 @@
 
 exports[`api/npm should fetch package info from npm 1`] = `
 Object {
-  "dist-tags": undefined,
+  "dist-tags": Object {
+    "latest": "0.0.1",
+  },
   "homepage": undefined,
   "name": undefined,
+  "renovate-config": undefined,
   "repositoryUrl": "https://github.com/renovateapp/dummy",
   "versions": Object {
     "0.0.1": Object {
@@ -26,9 +29,12 @@ Array [
 
 exports[`api/npm should send an authorization header if provided 1`] = `
 Object {
-  "dist-tags": undefined,
+  "dist-tags": Object {
+    "latest": "0.0.1",
+  },
   "homepage": "https://google.com",
   "name": undefined,
+  "renovate-config": undefined,
   "repositoryUrl": "https://google.com",
   "versions": Object {
     "0.0.1": Object {
@@ -52,9 +58,12 @@ Array [
 
 exports[`api/npm should use NPM_TOKEN if provided 1`] = `
 Object {
-  "dist-tags": undefined,
+  "dist-tags": Object {
+    "latest": "0.0.1",
+  },
   "homepage": "https://google.com",
   "name": undefined,
+  "renovate-config": undefined,
   "repositoryUrl": "https://google.com",
   "versions": Object {
     "0.0.1": Object {
@@ -78,9 +87,12 @@ Array [
 
 exports[`api/npm should use homepage 1`] = `
 Object {
-  "dist-tags": undefined,
+  "dist-tags": Object {
+    "latest": "0.0.1",
+  },
   "homepage": "https://google.com",
   "name": undefined,
+  "renovate-config": undefined,
   "repositoryUrl": "https://google.com",
   "versions": Object {
     "0.0.1": Object {
diff --git a/test/api/npm.spec.js b/test/api/npm.spec.js
index 48263f190e5168dbc22d2e7765ab362d7d154764..168d1ec065e52d4a32b54bf7dd78bd5865912836 100644
--- a/test/api/npm.spec.js
+++ b/test/api/npm.spec.js
@@ -19,6 +19,9 @@ const npmResponse = {
       type: 'git',
       url: 'git://github.com/renovateapp/dummy.git',
     },
+    'dist-tags': {
+      latest: '0.0.1',
+    },
     time: {
       '0.0.1': '',
     },
diff --git a/test/config/__snapshots__/presets.spec.js.snap b/test/config/__snapshots__/presets.spec.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..5adad40d3d49f17c92591d726260c408f7e37631
--- /dev/null
+++ b/test/config/__snapshots__/presets.spec.js.snap
@@ -0,0 +1,446 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`config/presets getPreset gets linters 1`] = `
+Object {
+  "description": Array [
+    "All lint-related packages",
+  ],
+  "extends": Array [
+    "packages:eslint",
+    "packages:stylelint",
+  ],
+  "packageNames": Array [
+    "remark-lint",
+  ],
+}
+`;
+
+exports[`config/presets getPreset gets parameterised configs 1`] = `
+Object {
+  "description": Array [
+    "Group eslint packages into same branch/PR",
+  ],
+  "packageRules": Array [
+    Object {
+      "extends": Array [
+        "packages:eslint",
+      ],
+      "groupName": "eslint",
+    },
+  ],
+}
+`;
+
+exports[`config/presets getPreset handles 404 packages 1`] = `Object {}`;
+
+exports[`config/presets getPreset handles missing params 1`] = `
+Object {
+  "description": Array [
+    "Group {{arg1}} packages into same branch/PR",
+  ],
+  "packageRules": Array [
+    Object {
+      "extends": Array [
+        "",
+      ],
+      "groupName": "{{arg1}}",
+    },
+  ],
+}
+`;
+
+exports[`config/presets getPreset handles no config 1`] = `Object {}`;
+
+exports[`config/presets getPreset handles preset not found 1`] = `Object {}`;
+
+exports[`config/presets getPreset handles throw errors 1`] = `Object {}`;
+
+exports[`config/presets getPreset ignores irrelevant params 1`] = `
+Object {
+  "description": Array [
+    "Use version pinning (maintain a single version only and not semver ranges)",
+  ],
+  "pinVersions": true,
+}
+`;
+
+exports[`config/presets parsePreset returns default package name 1`] = `
+Object {
+  "packageName": "renovate-config-default",
+  "params": undefined,
+  "presetName": "base",
+}
+`;
+
+exports[`config/presets parsePreset returns default package name with params 1`] = `
+Object {
+  "packageName": "renovate-config-default",
+  "params": Array [
+    "packages/eslint",
+    "eslint",
+  ],
+  "presetName": "group",
+}
+`;
+
+exports[`config/presets parsePreset returns non-scoped default 1`] = `
+Object {
+  "packageName": "renovate-config-somepackage",
+  "params": undefined,
+  "presetName": "default",
+}
+`;
+
+exports[`config/presets parsePreset returns non-scoped package name 1`] = `
+Object {
+  "packageName": "renovate-config-somepackage",
+  "params": undefined,
+  "presetName": "webapp",
+}
+`;
+
+exports[`config/presets parsePreset returns non-scoped package name full 1`] = `
+Object {
+  "packageName": "renovate-config-somepackage",
+  "params": undefined,
+  "presetName": "webapp",
+}
+`;
+
+exports[`config/presets parsePreset returns non-scoped package name with params 1`] = `
+Object {
+  "packageName": "renovate-config-somepackage",
+  "params": Array [
+    "param1",
+  ],
+  "presetName": "webapp",
+}
+`;
+
+exports[`config/presets parsePreset returns scope with packageName and default 1`] = `
+Object {
+  "packageName": "@somescope/somepackagename",
+  "params": undefined,
+  "presetName": "default",
+}
+`;
+
+exports[`config/presets parsePreset returns scope with packageName and params and default 1`] = `
+Object {
+  "packageName": "@somescope/somepackagename",
+  "params": Array [
+    "param1",
+    "param2",
+    "param3",
+  ],
+  "presetName": "default",
+}
+`;
+
+exports[`config/presets parsePreset returns scope with packageName and presetName 1`] = `
+Object {
+  "packageName": "@somescope/somepackagename",
+  "params": undefined,
+  "presetName": "somePresetName",
+}
+`;
+
+exports[`config/presets parsePreset returns scope with packageName and presetName and params 1`] = `
+Object {
+  "packageName": "@somescope/somepackagename",
+  "params": Array [
+    "param1",
+    "param2",
+  ],
+  "presetName": "somePresetName",
+}
+`;
+
+exports[`config/presets parsePreset returns scope with presetName 1`] = `
+Object {
+  "packageName": "@somescope/renovate-config",
+  "params": undefined,
+  "presetName": "somePresetName",
+}
+`;
+
+exports[`config/presets parsePreset returns scope with presetName and params 1`] = `
+Object {
+  "packageName": "@somescope/renovate-config",
+  "params": Array [
+    "param1",
+  ],
+  "presetName": "somePresetName",
+}
+`;
+
+exports[`config/presets parsePreset returns simple scope 1`] = `
+Object {
+  "packageName": "@somescope/renovate-config",
+  "params": undefined,
+  "presetName": "default",
+}
+`;
+
+exports[`config/presets parsePreset returns simple scope and params 1`] = `
+Object {
+  "packageName": "@somescope/renovate-config",
+  "params": Array [
+    "param1",
+  ],
+  "presetName": "default",
+}
+`;
+
+exports[`config/presets replaceArgs replaces args in strings 1`] = `"c foo ab"`;
+
+exports[`config/presets replaceArgs replaces arrays 1`] = `
+Object {
+  "foo": Array [
+    "a",
+    Object {
+      "bar": "b",
+      "baz": 5,
+    },
+  ],
+}
+`;
+
+exports[`config/presets replaceArgs replaces objects 1`] = `
+Object {
+  "bar": Object {
+    "aaa": Object {
+      "bbb": "woo c",
+    },
+    "baz": "b boo",
+  },
+  "foo": "ha a",
+}
+`;
+
+exports[`config/presets resolvePreset combines two package alls 1`] = `
+Object {
+  "logger": Object {
+    "child": [Function],
+    "debug": [Function],
+    "error": [Function],
+    "fatal": [Function],
+    "info": [Function],
+    "trace": [Function],
+    "warn": [Function],
+  },
+  "packagePatterns": Array [
+    "^eslint",
+    "^stylelint",
+  ],
+}
+`;
+
+exports[`config/presets resolvePreset resolves app preset 1`] = `
+Object {
+  "automerge": "none",
+  "branchprefix": "renovate/",
+  "dependencies": Object {
+    "description": Array [
+      "Use <code>fix(deps):</code> as semantic prefix for commit messages and PR titles",
+    ],
+    "semanticPrefix": "fix(deps):",
+  },
+  "description": Array [
+    "Use version pinning (maintain a single version only and not semver ranges)",
+    "Separate major versions of dependencies into individual branches/PRs",
+    "Use the same branch/PR for both patch and minor upgrades of a dependency",
+    "Only upgrade to stable npm versions",
+    "Do not upgrade versions past the \\"latest\\" tag in npm registry",
+    "Create branches/PRs for dependency upgrades as soon as they're available",
+    "Wait until branch tests have passed or failed before creating the PR",
+    "Use <code>renovate/</code> as prefix for all branch names",
+    "If semantic commits detected, use <code>fix(deps):</code> for dependencies and <code>chore(deps):</code> for all others",
+    "Do not automerge any upgrades - wait for humans to merge PRs",
+    "Update existing lock files only when <code>package.json</code> is modified",
+  ],
+  "devDependencies": Object {
+    "description": Array [
+      "Use <code>chore(deps):</code> as semantic prefix for commit messages and PR titles",
+    ],
+    "semanticPrefix": "chore(deps):",
+  },
+  "ignoreUnstable": true,
+  "lockFileMaintenance": Object {
+    "enabled": false,
+  },
+  "logger": Object {
+    "child": [Function],
+    "debug": [Function],
+    "error": [Function],
+    "fatal": [Function],
+    "info": [Function],
+    "trace": [Function],
+    "warn": [Function],
+  },
+  "optionalDependencies": Object {
+    "description": Array [
+      "Use <code>chore(deps):</code> as semantic prefix for commit messages and PR titles",
+    ],
+    "semanticPrefix": "chore(deps):",
+  },
+  "peerDependencies": Object {
+    "description": Array [
+      "Use <code>chore(deps):</code> as semantic prefix for commit messages and PR titles",
+    ],
+    "semanticPrefix": "chore(deps):",
+  },
+  "pinVersions": true,
+  "prCreation": "not-pending",
+  "respectLatest": true,
+  "separateMajorReleases": true,
+  "separatePatchReleases": false,
+  "unpublishSafe": false,
+}
+`;
+
+exports[`config/presets resolvePreset resolves linters 1`] = `
+Object {
+  "description": Array [
+    "All lint-related packages",
+  ],
+  "logger": Object {
+    "child": [Function],
+    "debug": [Function],
+    "error": [Function],
+    "fatal": [Function],
+    "info": [Function],
+    "trace": [Function],
+    "warn": [Function],
+  },
+  "packageNames": Array [
+    "remark-lint",
+  ],
+  "packagePatterns": Array [
+    "^eslint",
+    "^stylelint",
+  ],
+}
+`;
+
+exports[`config/presets resolvePreset resolves nested groups 1`] = `
+Object {
+  "description": Array [
+    "Update lint packages automatically if tests pass",
+  ],
+  "logger": Object {
+    "child": [Function],
+    "debug": [Function],
+    "error": [Function],
+    "fatal": [Function],
+    "info": [Function],
+    "trace": [Function],
+    "warn": [Function],
+  },
+  "packageRules": Array [
+    Object {
+      "automerge": "any",
+      "description": Array [
+        "All lint-related packages",
+      ],
+      "packageNames": Array [
+        "remark-lint",
+      ],
+      "packagePatterns": Array [
+        "^eslint",
+        "^stylelint",
+      ],
+    },
+  ],
+}
+`;
+
+exports[`config/presets resolvePreset resolves packageRule 1`] = `
+Object {
+  "logger": Object {
+    "child": [Function],
+    "debug": [Function],
+    "error": [Function],
+    "fatal": [Function],
+    "info": [Function],
+    "trace": [Function],
+    "warn": [Function],
+  },
+  "packageRules": Array [
+    Object {
+      "groupName": "eslint",
+      "packagePatterns": Array [
+        "^eslint",
+      ],
+    },
+  ],
+}
+`;
+
+exports[`config/presets resolvePreset returns same if invalid preset 1`] = `
+Object {
+  "foo": 1,
+  "logger": Object {
+    "child": [Function],
+    "debug": [Function],
+    "error": [Function],
+    "fatal": [Function],
+    "info": [Function],
+    "trace": [Function],
+    "warn": [Function],
+  },
+}
+`;
+
+exports[`config/presets resolvePreset returns same if no presets 1`] = `
+Object {
+  "foo": 1,
+  "logger": Object {
+    "child": [Function],
+    "debug": [Function],
+    "error": [Function],
+    "fatal": [Function],
+    "info": [Function],
+    "trace": [Function],
+    "warn": [Function],
+  },
+}
+`;
+
+exports[`config/presets resolvePreset works with valid 1`] = `
+Object {
+  "description": Array [
+    "Use version pinning (maintain a single version only and not semver ranges)",
+  ],
+  "foo": 1,
+  "logger": Object {
+    "child": [Function],
+    "debug": [Function],
+    "error": [Function],
+    "fatal": [Function],
+    "info": [Function],
+    "trace": [Function],
+    "warn": [Function],
+  },
+  "pinVersions": true,
+}
+`;
+
+exports[`config/presets resolvePreset works with valid and invalid 1`] = `
+Object {
+  "description": Array [
+    "Use version pinning (maintain a single version only and not semver ranges)",
+  ],
+  "foo": 1,
+  "logger": Object {
+    "child": [Function],
+    "debug": [Function],
+    "error": [Function],
+    "fatal": [Function],
+    "info": [Function],
+    "trace": [Function],
+    "warn": [Function],
+  },
+  "pinVersions": true,
+}
+`;
diff --git a/test/config/presets.spec.js b/test/config/presets.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..529802422054016879b3a625c69de8ea15093315
--- /dev/null
+++ b/test/config/presets.spec.js
@@ -0,0 +1,255 @@
+const npm = require('../../lib/api/npm');
+const presets = require('../../lib/config/presets');
+const logger = require('../_fixtures/logger');
+const presetDefaults = require('../_fixtures/npm/renovate-config-default');
+const presetPackages = require('../_fixtures/npm/renovate-config-packages');
+
+npm.getDependency = jest.fn(dep => {
+  if (dep === 'renovate-config-default') {
+    return {
+      'renovate-config': presetDefaults.versions['0.0.1']['renovate-config'],
+    };
+  }
+  if (dep === 'renovate-config-packages') {
+    return {
+      'renovate-config': presetPackages.versions['0.0.1']['renovate-config'],
+    };
+  }
+  if (dep === 'renovate-config-noconfig') {
+    return {};
+  }
+  if (dep === 'renovate-config-throw') {
+    throw new Error('whoops');
+  }
+  if (dep === 'renovate-config-wrongpreset') {
+    return {
+      'renovate-config': {},
+    };
+  }
+  return null;
+});
+
+describe('config/presets', () => {
+  describe('resolvePreset', () => {
+    let config;
+    beforeEach(() => {
+      config = {
+        logger,
+      };
+    });
+    it('returns same if no presets', async () => {
+      config.foo = 1;
+      config.extends = [];
+      const res = await presets.resolveConfigPresets(config);
+      expect(config).toMatchObject(res);
+      expect(res).toMatchSnapshot();
+    });
+    it('returns same if invalid preset', async () => {
+      config.foo = 1;
+      config.extends = [':invalid-preset'];
+      const res = await presets.resolveConfigPresets(config);
+      expect(res).toMatchSnapshot();
+    });
+    it('works with valid', async () => {
+      config.foo = 1;
+      config.extends = [':pinVersions'];
+      const res = await presets.resolveConfigPresets(config);
+      expect(res).toMatchSnapshot();
+      expect(res.pinVersions).toBe(true);
+    });
+    it('works with valid and invalid', async () => {
+      config.foo = 1;
+      config.extends = [':invalid-preset', ':pinVersions'];
+      const res = await presets.resolveConfigPresets(config);
+      expect(res).toMatchSnapshot();
+      expect(res.pinVersions).toBe(true);
+    });
+    it('resolves app preset', async () => {
+      config.extends = [':app'];
+      const res = await presets.resolveConfigPresets(config);
+      expect(res).toMatchSnapshot();
+    });
+    it('combines two package alls', async () => {
+      config.extends = ['packages:eslint', 'packages:stylelint'];
+      const res = await presets.resolveConfigPresets(config);
+      expect(res).toMatchSnapshot();
+    });
+    it('resolves packageRule', async () => {
+      config.packageRules = [
+        {
+          extends: ['packages:eslint'],
+          groupName: 'eslint',
+        },
+      ];
+      const res = await presets.resolveConfigPresets(config);
+      expect(res).toMatchSnapshot();
+    });
+    it('resolves linters', async () => {
+      config.extends = ['packages:linters'];
+      const res = await presets.resolveConfigPresets(config);
+      expect(res).toMatchSnapshot();
+      expect(res.packageNames).toHaveLength(1);
+      expect(res.packagePatterns).toHaveLength(2);
+    });
+    it('resolves nested groups', async () => {
+      config.extends = [':automergeLinters'];
+      const res = await presets.resolveConfigPresets(config);
+      expect(res).toMatchSnapshot();
+      const rule = res.packageRules[0];
+      expect(rule.automerge).toEqual('any');
+      expect(rule.packageNames).toHaveLength(1);
+      expect(rule.packagePatterns).toHaveLength(2);
+    });
+  });
+  describe('replaceArgs', () => {
+    const argMappings = {
+      arg0: 'a',
+      arg1: 'b',
+      arg2: 'c',
+    };
+    it('replaces args in strings', async () => {
+      const str = '{{arg2}} foo {{arg0}}{{arg1}}';
+      const res = presets.replaceArgs(str, argMappings);
+      expect(res).toMatchSnapshot();
+    });
+    it('replaces args twice in same string', async () => {
+      const str = '{{arg2}}{{arg0}} foo {{arg0}}{{arg1}}';
+      const res = presets.replaceArgs(str, argMappings);
+      expect(res).toEqual('ca foo ab');
+    });
+    it('replaces objects', async () => {
+      const obj = {
+        foo: 'ha {{arg0}}',
+        bar: {
+          baz: '{{arg1}} boo',
+          aaa: {
+            bbb: 'woo {{arg2}}',
+          },
+        },
+      };
+      const res = presets.replaceArgs(obj, argMappings);
+      expect(res).toMatchSnapshot();
+    });
+    it('replaces arrays', async () => {
+      const obj = {
+        foo: [
+          '{{arg0}}',
+          {
+            bar: '{{arg1}}',
+            baz: 5,
+          },
+        ],
+      };
+      const res = presets.replaceArgs(obj, argMappings);
+      expect(res).toMatchSnapshot();
+    });
+  });
+  describe('parsePreset', () => {
+    // default namespace
+    it('returns default package name', async () => {
+      expect(presets.parsePreset(':base')).toMatchSnapshot();
+    });
+    it('returns default package name with params', async () => {
+      expect(
+        presets.parsePreset(':group(packages/eslint, eslint)')
+      ).toMatchSnapshot();
+    });
+    // scoped namespace
+    it('returns simple scope', async () => {
+      expect(presets.parsePreset('@somescope')).toMatchSnapshot();
+    });
+    it('returns simple scope and params', async () => {
+      expect(presets.parsePreset('@somescope(param1)')).toMatchSnapshot();
+    });
+    it('returns scope with packageName and default', async () => {
+      expect(
+        presets.parsePreset('@somescope/somepackagename')
+      ).toMatchSnapshot();
+    });
+    it('returns scope with packageName and params and default', async () => {
+      expect(
+        presets.parsePreset(
+          '@somescope/somepackagename(param1, param2, param3)'
+        )
+      ).toMatchSnapshot();
+    });
+    it('returns scope with presetName', async () => {
+      expect(
+        presets.parsePreset('@somescope:somePresetName')
+      ).toMatchSnapshot();
+    });
+    it('returns scope with presetName and params', async () => {
+      expect(
+        presets.parsePreset('@somescope:somePresetName(param1)')
+      ).toMatchSnapshot();
+    });
+    it('returns scope with packageName and presetName', async () => {
+      expect(
+        presets.parsePreset('@somescope/somepackagename:somePresetName')
+      ).toMatchSnapshot();
+    });
+    it('returns scope with packageName and presetName and params', async () => {
+      expect(
+        presets.parsePreset(
+          '@somescope/somepackagename:somePresetName(param1, param2)'
+        )
+      ).toMatchSnapshot();
+    });
+    // non-scoped namespace
+    it('returns non-scoped default', async () => {
+      expect(presets.parsePreset('somepackage')).toMatchSnapshot();
+    });
+    it('returns non-scoped package name', async () => {
+      expect(presets.parsePreset('somepackage:webapp')).toMatchSnapshot();
+    });
+    it('returns non-scoped package name full', async () => {
+      expect(
+        presets.parsePreset('renovate-config-somepackage:webapp')
+      ).toMatchSnapshot();
+    });
+    it('returns non-scoped package name with params', async () => {
+      expect(
+        presets.parsePreset('somepackage:webapp(param1)')
+      ).toMatchSnapshot();
+    });
+  });
+  describe('getPreset', () => {
+    it('gets linters', async () => {
+      const res = await presets.getPreset('packages:linters', logger);
+      expect(res).toMatchSnapshot();
+      expect(res.packageNames).toHaveLength(1);
+      expect(res.extends).toHaveLength(2);
+    });
+    it('gets parameterised configs', async () => {
+      const res = await presets.getPreset(
+        ':group(packages:eslint, eslint)',
+        logger
+      );
+      expect(res).toMatchSnapshot();
+    });
+    it('handles missing params', async () => {
+      const res = await presets.getPreset(':group()', logger);
+      expect(res).toMatchSnapshot();
+    });
+    it('ignores irrelevant params', async () => {
+      const res = await presets.getPreset(':pinVersions(foo, bar)', logger);
+      expect(res).toMatchSnapshot();
+    });
+    it('handles 404 packages', async () => {
+      const res = await presets.getPreset('notfound:foo', logger);
+      expect(res).toMatchSnapshot();
+    });
+    it('handles no config', async () => {
+      const res = await presets.getPreset('noconfig:foo', logger);
+      expect(res).toMatchSnapshot();
+    });
+    it('handles throw errors', async () => {
+      const res = await presets.getPreset('throw:foo', logger);
+      expect(res).toMatchSnapshot();
+    });
+    it('handles preset not found', async () => {
+      const res = await presets.getPreset('wrongpreset:foo', logger);
+      expect(res).toMatchSnapshot();
+    });
+  });
+});
diff --git a/test/workers/package/__snapshots__/index.spec.js.snap b/test/workers/package/__snapshots__/index.spec.js.snap
index c08eb58c68aebdf708cd2290cc504fd0c4f05c4f..268f953189d9730088cf770a9cfb49812087c1a0 100644
--- a/test/workers/package/__snapshots__/index.spec.js.snap
+++ b/test/workers/package/__snapshots__/index.spec.js.snap
@@ -2,6 +2,7 @@
 
 exports[`lib/workers/package/index renovatePackage(config) merges type 1`] = `
 Array [
+  "description",
   "timezone",
   "schedule",
   "branchPrefix",
@@ -34,6 +35,7 @@ Array [
 
 exports[`lib/workers/package/index renovatePackage(config) returns array if upgrades found 1`] = `
 Array [
+  "description",
   "timezone",
   "schedule",
   "branchPrefix",
@@ -74,6 +76,7 @@ Array [
     "commitMessage": "Update dependency {{depName}} to v{{newVersion}}",
     "currentVersion": "1.0.0",
     "depName": "foo",
+    "description": Array [],
     "group": Object {
       "branchName": "{{branchPrefix}}{{groupSlug}}",
       "commitMessage": "Renovate {{groupName}} packages",
diff --git a/test/workers/repository/__snapshots__/onboarding.spec.js.snap b/test/workers/repository/__snapshots__/onboarding.spec.js.snap
index be94274307834bbce3934eac64a9a5714730237d..236a5c705e244729bd999156edaecccca572249d 100644
--- a/test/workers/repository/__snapshots__/onboarding.spec.js.snap
+++ b/test/workers/repository/__snapshots__/onboarding.spec.js.snap
@@ -77,6 +77,95 @@ Alternatively, you can add the same configuration settings into a \\"renovate\\"
 ]
 `;
 
+exports[`lib/workers/repository/onboarding ensurePr(config, branchUpgrades) creates pr with dynamic descriptions 1`] = `
+Array [
+  "renovate/configure",
+  "Configure Renovate",
+  "Welcome to [Renovate](https://renovateapp.com)!
+
+This is an onboarding PR to help you understand and configure Renovate before any regular Pull Requests begin. Once you close this Pull Request, Renovate will begin keeping your dependencies up-to-date via automated Pull Requests.
+
+If you have any questions, try reading our [Getting Started Configuring Renovate](https://renovateapp.com/docs/getting-started/configure-renovate) page first.
+
+---
+
+## Configuration Summary
+
+Based on the currently configured presets, Renovate will:
+<ul>
+  <li>Start dependency updates once this Configure Renovate PR is merged or closed</li>
+  <li>Assign PRs to @rarkins</li>
+  <li>Apply labels <code>renovate</code> and <code>upgrades</code> to PRs</li>
+  <li>Run Renovate on following schedule: before 5am</li>
+
+</ul>
+
+---
+
+It looks like your repository dependencies are already up-to-date and no initial Pull Requests will be necessary.
+
+Sometimes you may see multiple options for the same dependency (e.g. pinning in one branch and upgrading in another). This is expected and allows you the flexibility to choose which to merge first. Once you merge any PR, others will be updated or removed the next time Renovate runs.
+
+Would you like to change the way Renovate is upgrading your dependencies? Simply edit the \`renovate.json\` in this branch and this Pull Request description will be updated the next time Renovate runs.
+
+Our [Configuration Docs](https://renovateapp.com/docs/) should be helpful if you wish to modify any behaviour.
+
+---
+
+#### Don't want a \`renovate.json\` file?
+
+You are not required to *merge* this Pull Request - Renovate will begin even if this \\"Configure Renovate\\" PR is closed *unmerged* and without a \`renovate.json\` file. However, it's recommended that you add configuration to your repository to ensure behaviour matches what you see described here.
+
+Alternatively, you can add the same configuration settings into a \\"renovate\\" section of your \`package.json\` file(s) in this branch and delete the \`renovate.json\` from this PR. If you make these configuration changes in this branch then the results will be described in this PR after the next time Renovate runs.
+",
+  true,
+]
+`;
+
+exports[`lib/workers/repository/onboarding ensurePr(config, branchUpgrades) creates pr with preset descriptions 1`] = `
+Array [
+  "renovate/configure",
+  "Configure Renovate",
+  "Welcome to [Renovate](https://renovateapp.com)!
+
+This is an onboarding PR to help you understand and configure Renovate before any regular Pull Requests begin. Once you close this Pull Request, Renovate will begin keeping your dependencies up-to-date via automated Pull Requests.
+
+If you have any questions, try reading our [Getting Started Configuring Renovate](https://renovateapp.com/docs/getting-started/configure-renovate) page first.
+
+---
+
+## Configuration Summary
+
+Based on the currently configured presets, Renovate will:
+<ul>
+  <li>Start dependency updates once this Configure Renovate PR is merged or closed</li>
+  <li>Description 1</li>
+  <li>Description 2</li>
+
+</ul>
+
+---
+
+It looks like your repository dependencies are already up-to-date and no initial Pull Requests will be necessary.
+
+Sometimes you may see multiple options for the same dependency (e.g. pinning in one branch and upgrading in another). This is expected and allows you the flexibility to choose which to merge first. Once you merge any PR, others will be updated or removed the next time Renovate runs.
+
+Would you like to change the way Renovate is upgrading your dependencies? Simply edit the \`renovate.json\` in this branch and this Pull Request description will be updated the next time Renovate runs.
+
+Our [Configuration Docs](https://renovateapp.com/docs/) should be helpful if you wish to modify any behaviour.
+
+---
+
+#### Don't want a \`renovate.json\` file?
+
+You are not required to *merge* this Pull Request - Renovate will begin even if this \\"Configure Renovate\\" PR is closed *unmerged* and without a \`renovate.json\` file. However, it's recommended that you add configuration to your repository to ensure behaviour matches what you see described here.
+
+Alternatively, you can add the same configuration settings into a \\"renovate\\" section of your \`package.json\` file(s) in this branch and delete the \`renovate.json\` from this PR. If you make these configuration changes in this branch then the results will be described in this PR after the next time Renovate runs.
+",
+  true,
+]
+`;
+
 exports[`lib/workers/repository/onboarding ensurePr(config, branchUpgrades) creates shows warnings and errors 1`] = `
 Array [
   Array [
@@ -90,7 +179,7 @@ If you have any questions, try reading our [Getting Started Configuring Renovate
 
 ---
 
-## Errors (1)
+### Errors (1)
 
 Renovate has raised errors when processing this repository that you should fix before merging or closing this PR.
 
@@ -221,11 +310,7 @@ Array [
   "renovate/configure",
   Array [
     Object {
-      "contents": "{
-  \\"pinVersions\\": true,
-  \\"dependencies\\": {\\"pinVersions\\": false},
-  \\"ignoreNodeModules\\": true
-}
+      "contents": "{\\"extends\\": [\\"library\\"]}
 ",
       "name": "renovate.json",
     },
@@ -239,11 +324,7 @@ Array [
   "renovate/configure",
   Array [
     Object {
-      "contents": "{
-  \\"pinVersions\\": true,
-  \\"dependencies\\": {\\"pinVersions\\": false},
-  \\"ignoreNodeModules\\": true
-}
+      "contents": "{\\"extends\\": [\\"library\\"]}
 ",
       "name": "renovate.json",
     },
@@ -257,7 +338,7 @@ Array [
   "renovate/configure",
   Array [
     Object {
-      "contents": "{\\"pinVersions\\": true, \\"ignoreNodeModules\\": true}
+      "contents": "{\\"extends\\": [\\"app\\"]}
 ",
       "name": "renovate.json",
     },
diff --git a/test/workers/repository/onboarding.spec.js b/test/workers/repository/onboarding.spec.js
index 6c697b2a959837f99f28767d308af46f8f40a8c4..7ba914441c4ae78923ee816dc91e93dc3b258c58 100644
--- a/test/workers/repository/onboarding.spec.js
+++ b/test/workers/repository/onboarding.spec.js
@@ -53,6 +53,28 @@ describe('lib/workers/repository/onboarding', () => {
       expect(config.api.updatePr.mock.calls.length).toBe(0);
       expect(config.api.createPr.mock.calls).toMatchSnapshot();
     });
+    it('creates pr with preset descriptions', async () => {
+      config.description = ['Description 1', 'Description 2'];
+      await onboarding.ensurePr(config, branchUpgrades);
+      expect(config.api.createPr.mock.calls.length).toBe(1);
+      expect(config.api.updatePr.mock.calls.length).toBe(0);
+      expect(
+        config.api.createPr.mock.calls[0][2].indexOf('## Configuration Summary')
+      ).not.toBe(-1);
+      expect(config.api.createPr.mock.calls[0]).toMatchSnapshot();
+    });
+    it('creates pr with dynamic descriptions', async () => {
+      config.labels = ['renovate', 'upgrades'];
+      config.assignees = ['rarkins'];
+      config.schedule = ['before 5am'];
+      await onboarding.ensurePr(config, branchUpgrades);
+      expect(config.api.createPr.mock.calls.length).toBe(1);
+      expect(config.api.updatePr.mock.calls.length).toBe(0);
+      expect(
+        config.api.createPr.mock.calls[0][2].indexOf('## Configuration Summary')
+      ).not.toBe(-1);
+      expect(config.api.createPr.mock.calls[0]).toMatchSnapshot();
+    });
     it('updates pr', async () => {
       config.api.getBranchPr.mockReturnValueOnce({});
       await onboarding.ensurePr(config, branchUpgrades);
@@ -274,8 +296,7 @@ describe('lib/workers/repository/onboarding', () => {
       expect(config.api.commitFilesToBranch.mock.calls[0]).toMatchSnapshot();
     });
     it('skips commit files if existing content matches', async () => {
-      const existingContent =
-        '{\n  "pinVersions": true,\n  "dependencies": {"pinVersions": false},\n  "ignoreNodeModules": true\n}\n';
+      const existingContent = '{"extends": ["library"]}\n';
       config.api.getFileContent.mockReturnValueOnce(existingContent);
       const res = await onboarding.getOnboardingStatus(config);
       expect(res).toEqual(false);