From 9a56bb96fbba41b2cc4a849eaaaefff801a0051e Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@keylocation.sg>
Date: Tue, 17 Jan 2017 09:11:42 +0100
Subject: [PATCH] Support package.json autodiscovery (default)

Closes #53

commit e80dbfd9caa56edc3c9980622e6ebffecbc6e104
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Tue Jan 17 09:11:03 2017 +0100

    Update docs

commit 78830806efe35150e9ec4cb5416d43092ccf0fc7
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Tue Jan 17 08:52:23 2017 +0100

    Refactor

commit c44979212f7c51d395a4b4dbffe831af5467b4fc
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Tue Jan 17 07:25:28 2017 +0100

    Set default packageFiles to []

commit c830f0b37928460ed857260e6c4b4c3c23443f1e
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Tue Jan 17 07:14:33 2017 +0100

    Add github.fileFile
---
 docs/configuration.md    |  2 +-
 docs/design-decisions.md |  6 ++++++
 lib/api/github.js        | 15 ++++++++++++++
 lib/config/default.js    |  1 +
 lib/config/index.js      |  4 ++--
 lib/index.js             | 44 +++++++++++++++++++++++++++++++++++-----
 lib/worker.js            |  5 +----
 readme.md                |  3 ++-
 8 files changed, 67 insertions(+), 13 deletions(-)

diff --git a/docs/configuration.md b/docs/configuration.md
index b96fb23ab9..38f56fb998 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -92,7 +92,7 @@ Obviously, you can't set repository or package file location with this method.
 |---------------------|---------------------------------------------------------|-----------------------------------------------------------|---------------------------|--------------------------|---------------------------|
 | Auth Token | GitHub Personal Access Token |  | `token` | `GITHUB_TOKEN` | `--token` |
 | Repositories | List of Repositories |  | `repositories` | `RENOVATE_REPOS` | Space-delimited arguments |
-| Package Files | Package file location(s) | `package.json` | `repository.packageFiles` | `RENOVATE_PACKAGE_FILES` | `--package-files` |
+| Package Files | Package file location(s) | `[]` | `repository.packageFiles` | `RENOVATE_PACKAGE_FILES` | `--package-files` |
 | Dependency Types | Sections of package.json to renovate | `dependencies`, `devDependencies`, `optionalDependencies` | `depTypes` | `RENOVATE_DEP_TYPES` | `--dep-types` |
 | Ignore Dependencies | Dependencies to be ignored |  | `ignoreDeps` | `RENOVATE_IGNORE_DEPS` | `--ignore-deps` |
 | Labels | Labels to add to Pull Requests |  | `labels` | `RENOVATE_LABELS` | `--labels` |
diff --git a/docs/design-decisions.md b/docs/design-decisions.md
index 1a9afb0e57..1e7f456c7a 100644
--- a/docs/design-decisions.md
+++ b/docs/design-decisions.md
@@ -36,6 +36,12 @@ The following options apply globally:
 
 - Log Level
 
+## Automatic discovery of package.json locations
+
+Default behaviour is to auto-discover all `package.json` locations in a repository and process them all.
+Doing so means that "monorepos" are supported by default.
+This can be overridden by the configuration option `packageFiles`, where you list the file paths manually (e.g. limit to just `package.json` in root of repository).
+
 ## Separate Branches per dependency
 
 `renovate` will maintain separate branches per-dependency. So if 20 dependencies need updating, there will be at least 20 branches/PRs. Although this may seem undesirable, it was considered even less desirable if all 20 were in the same Pull Request and it's up to the users to determine which dependency upgrade(s) caused the build to fail.
diff --git a/lib/api/github.js b/lib/api/github.js
index 1cd1392b16..f8df50dff5 100644
--- a/lib/api/github.js
+++ b/lib/api/github.js
@@ -5,6 +5,8 @@ const config = {};
 
 module.exports = {
   initRepo,
+  // Search
+  findFilePaths,
   // Branch
   createBranch,
   deleteBranch,
@@ -24,6 +26,7 @@ module.exports = {
 
 // Initialize GitHub by getting base branch and SHA
 function initRepo(repoName) {
+  logger.debug(`initRepo(${repoName})`);
   config.repoName = repoName;
 
   return getRepoMetadata().then(getRepoBaseSHA)
@@ -58,6 +61,18 @@ function initRepo(repoName) {
   }
 }
 
+// Search
+
+// Returns an array of file paths in current repo matching the fileName
+function findFilePaths(fileName) {
+  return ghGot(`search/code?q=repo:${config.repoName}+filename:${fileName}`)
+  .then((res) => {
+    const exactMatches = res.body.items.filter(item => item.name === fileName);
+    const filePaths = exactMatches.map(item => item.path);
+    return filePaths;
+  });
+}
+
 // Branch
 function createBranch(branchName) {
   return ghGot.post(`repos/${config.repoName}/git/refs`, {
diff --git a/lib/config/default.js b/lib/config/default.js
index 334d632500..f3f105041a 100644
--- a/lib/config/default.js
+++ b/lib/config/default.js
@@ -3,6 +3,7 @@ const logger = require('winston');
 const config = {
   logLevel: 'info',
   depTypes: ['dependencies', 'devDependencies', 'optionalDependencies'],
+  packageFiles: [], // Autodiscover
   ignoreDeps: [],
   labels: [],
   templates: {
diff --git a/lib/config/index.js b/lib/config/index.js
index eabcd9b04a..ce80b8f409 100644
--- a/lib/config/index.js
+++ b/lib/config/index.js
@@ -41,10 +41,10 @@ function parseConfigs() {
       config.repositories[index] = { repository: repo };
     }
   });
-  // Add 'package.json' if missing
+  // Set default packageFiles
   config.repositories.forEach((repo, index) => {
     if (!repo.packageFiles || !repo.packageFiles.length) {
-      config.repositories[index].packageFiles = ['package.json'];
+      config.repositories[index].packageFiles = config.packageFiles;
     }
   });
   // Expand packageFile format
diff --git a/lib/index.js b/lib/index.js
index 2701965ad2..f2202a89f7 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -1,5 +1,6 @@
 const logger = require('./logger');
 const config = require('./config');
+const github = require('./api/github');
 
 // Require main source
 const worker = require('./worker');
@@ -18,13 +19,46 @@ function start() {
   // Get global config
   const globalConfig = config.getGlobalConfig();
 
-  // Queue up each repo/package combination
+  // Queue repositories in sequence
   globalConfig.repositories.forEach((repo) => {
-    repo.packageFiles.forEach((packageFile) => {
-      const cascadedConfig = config.getCascadedConfig(repo, packageFile);
-      p = p.then(() => worker(repo.repository, packageFile.fileName, cascadedConfig));
-    });
+    p = p.then(() => processRepo(repo));
   });
+
+  // Queue package files in sequence within a repo
+  function processRepo(repo) {
+    logger.debug(`Processing repository: ${JSON.stringify(repo)}`);
+    // Set GitHub token for this repo
+    process.env.GITHUB_TOKEN = repo.token || globalConfig.token;
+    // Initialize repo
+    return github.initRepo(repo.repository)
+    .then(() => ensurePackageFiles(repo))
+    .then(getWorkers);
+  }
+
+  // Ensure repo contains packageFiles
+  function ensurePackageFiles(repo) {
+    // Check for manually configured package files
+    if (repo.packageFiles.length) {
+      return Promise.resolve(repo);
+    }
+    // Otherwise, autodiscover filenames
+    return github.findFilePaths('package.json')
+    .then((fileNames) => {
+      const packageFiles =
+        fileNames.map(fileName => ({ fileName }));
+      return Object.assign(repo, { packageFiles });
+    });
+  }
+
+  // Return a promise queue of package file workers
+  function getWorkers(repo) {
+    return repo.packageFiles.reduce((promise, packageFile) => {
+      const cascadedConfig = config.getCascadedConfig(repo, packageFile);
+      return promise.then(() => worker(repo.repository, packageFile.fileName, cascadedConfig));
+    }, Promise.resolve());
+  }
+
+  // Process all promises
   p.then(() => { // eslint-disable-line promise/always-return
     logger.info('Renovate finished');
   })
diff --git a/lib/worker.js b/lib/worker.js
index 3f7c793717..fec7492aff 100644
--- a/lib/worker.js
+++ b/lib/worker.js
@@ -14,14 +14,11 @@ function renovate(repoName, packageFile, setConfig) {
   // Initialize globals
   config = Object.assign({}, setConfig);
   config.packageFile = packageFile;
-  // We export the token back to ENV, on a per-repo basis
-  process.env.GITHUB_TOKEN = config.token;
 
   logger.info(`Processing ${repoName} ${packageFile}`);
 
   // Start the chain
-  return github.initRepo(repoName)
-    .then(() => github.getFileContents(packageFile))
+  return github.getFileContents(packageFile)
     .then(checkforRenovateConfig)
     .then(contents => packageJson.extractDependencies(contents, config.depTypes))
     .then(filterIgnoredDependencies)
diff --git a/readme.md b/readme.md
index fd8f98bbd5..03757e4d28 100644
--- a/readme.md
+++ b/readme.md
@@ -5,7 +5,7 @@
 ##  Why
 
 - Creates or updates Pull Requests for each dependency that needs updating
-- Supports multiple `package.json` files per repository
+- Discovers and processes all `package.json` files in repository (supports monorepo architecture)
 - Supports multiple major versions per-dependency at once
 - Configurable via file, environment, CLI, and `package.json`
 - Self-hosted
@@ -25,6 +25,7 @@ You need to select a GitHub user for `renovate` to assume the identity of. It's
 The script will need a GitHub Personal Access Token with "repo" permissions. You can find instructions for generating it here: https://help.github.com/articles/creating-an-access-token-for-command-line-use/
 
 This token needs to be configured via file, environment variable, or CLI. See [docs/configuration.md](docs/configuration.md) for details.
+The simplest way is to expose it as `GITHUB_TOKEN`.
 
 ## Usage (CLI)
 
-- 
GitLab