diff --git a/docs/configuration.md b/docs/configuration.md index 889e7282317b28c37ca48b2c4e96795b7937c8aa..3f8528dba0410fda59e3e94ff22c54308221f50c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -75,6 +75,7 @@ $ node renovate --help --platform <string> Platform type of repository --endpoint <string> Custom endpoint to use --token <string> Repository Auth Token + --autodiscover [boolean] Autodiscover all repositories --package-files <list> Package file paths --dep-types <list> Dependency types --separate-major-releases [boolean] If set to false, it will upgrade dependencies to latest release only, and not separate major/minor branches @@ -130,6 +131,7 @@ Obviously, you can't set repository or package file location with this method. | `platform` | Platform type of repository | string | `"github"` | `RENOVATE_PLATFORM` | `--platform` | | `endpoint` | Custom endpoint to use | string | `null` | `RENOVATE_ENDPOINT` | `--endpoint` | | `token` | Repository Auth Token | string | `null` | `RENOVATE_TOKEN` | `--token` | +| `autodiscover` | Autodiscover all repositories | boolean | `false` | `RENOVATE_AUTODISCOVER` | `--autodiscover` | | `repositories` | List of Repositories | list | `[]` | `RENOVATE_REPOSITORIES` | | | `packageFiles` | Package file paths | list | `[]` | `RENOVATE_PACKAGE_FILES` | `--package-files` | | `depTypes` | Dependency types | list | `["dependencies", "devDependencies", "optionalDependencies"]` | `RENOVATE_DEP_TYPES` | `--dep-types` | diff --git a/lib/api/github.js b/lib/api/github.js index c33c3fd7d19e2706755c67b2ffcdefb2b0fa3199..b7ab35fced8c6c2489220ecd79a2ca5291a638e3 100644 --- a/lib/api/github.js +++ b/lib/api/github.js @@ -4,6 +4,7 @@ const ghGot = require('gh-got'); const config = {}; module.exports = { + getRepos, initRepo, // Search findFilePaths, @@ -29,6 +30,26 @@ module.exports = { getFileJson, }; +// Get all repositories that the user has access to +async function getRepos(token, endpoint) { + logger.debug('getRepos(token, endpoint)'); + if (token) { + process.env.GITHUB_TOKEN = token; + } else if (!process.env.GITHUB_TOKEN) { + throw new Error('No token found for getRepos'); + } + if (endpoint) { + process.env.GITHUB_ENDPOINT = endpoint; + } + try { + const res = await ghGot('user/repos'); + return res.body.map(repo => repo.full_name); + } catch (err) /* istanbul ignore next */ { + logger.error(`GitHub getRepos error: ${JSON.stringify(err)}`); + throw err; + } +} + // Initialize GitHub by getting base branch and SHA async function initRepo(repoName, token, endpoint) { logger.debug(`initRepo(${repoName})`); diff --git a/lib/api/gitlab.js b/lib/api/gitlab.js index 64889312b35cb0b1103fcf46cb26631944d060f5..b884235c758587167ef57f7d7e21158a90092257 100644 --- a/lib/api/gitlab.js +++ b/lib/api/gitlab.js @@ -4,6 +4,7 @@ const glGot = require('gl-got'); const config = {}; module.exports = { + getRepos, initRepo, // Search findFilePaths, @@ -29,6 +30,26 @@ module.exports = { getFileJson, }; +// Get all repositories that the user has access to +async function getRepos(token, endpoint) { + logger.debug('getRepos(token, endpoint)'); + if (token) { + process.env.GITLAB_TOKEN = token; + } else if (!process.env.GITLAB_TOKEN) { + throw new Error('No token found for getRepos'); + } + if (endpoint) { + process.env.GITLAB_ENDPOINT = endpoint; + } + try { + const res = await glGot('projects'); + return res.body.map(repo => repo.path_with_namespace); + } catch (err) { + logger.error(`GitLab getRepos error: ${JSON.stringify(err)}`); + throw err; + } +} + // Initialize GitLab by getting base branch async function initRepo(repoName, token, endpoint) { logger.debug(`initRepo(${repoName})`); diff --git a/lib/config/definitions.js b/lib/config/definitions.js index 700a37585981bfa9937f589e9e89e8004854cabc..bbb07e5af71cf21c055abb6b6d8276689d01f2a0 100644 --- a/lib/config/definitions.js +++ b/lib/config/definitions.js @@ -31,6 +31,12 @@ const options = [ description: 'Repository Auth Token', type: 'string', }, + { + name: 'autodiscover', + description: 'Autodiscover all repositories', + type: 'boolean', + default: false, + }, { name: 'repositories', description: 'List of Repositories', diff --git a/lib/config/index.js b/lib/config/index.js index 450aa58e82cc9b9d48b79739f5749f2692bcd582..5993002ab627e29558ce25e6363e442946fdb3fc 100644 --- a/lib/config/index.js +++ b/lib/config/index.js @@ -16,7 +16,7 @@ module.exports = { getRepositories, }; -function parseConfigs(env, argv) { +async function parseConfigs(env, argv) { logger.debug('Parsing configs'); // Get configs @@ -53,9 +53,23 @@ function parseConfigs(env, argv) { throw new Error(`Unsupported platform: ${config.platform}.`); } - // We need at least one repository defined - if (!config.repositories || config.repositories.length === 0) { - throw new Error('At least one repository must be configured'); + // Autodiscover + if (config.autodiscover) { + if (config.platform === 'github') { + logger.info('Autodiscovering GitHub repositories'); + config.repositories = await githubApi.getRepos(config.token, config.endpoint); + } else if (config.platform === 'gitlab') { + logger.info('Autodiscovering GitLab repositories'); + config.repositories = await gitlabApi.getRepos(config.token, config.endpoint); + } + if (!config.repositories || config.repositories.length === 0) { + // Soft fail (no error thrown) if no accessible repositories + logger.info('The account associated with your token does not have access to any repos'); + return; + } + } else if (!config.repositories || config.repositories.length === 0) { + // We need at least one repository defined + throw new Error('At least one repository must be configured, or use --autodiscover'); } // Configure each repository diff --git a/lib/index.js b/lib/index.js index 82c9725cf000c3f9ce42f3713e35e2ec2e87088d..5595905d32c7d399c233e1ce4dff493b3dd73e56 100644 --- a/lib/index.js +++ b/lib/index.js @@ -19,7 +19,7 @@ let api; async function start() { // Parse config try { - configParser.parseConfigs(process.env, process.argv); + await configParser.parseConfigs(process.env, process.argv); // Iterate through repositories sequentially for (const repo of configParser.getRepositories()) { await processRepo(repo); diff --git a/readme.md b/readme.md index f50a48db38f4c988ffe4a44bd870e97a81e7eb01..b57d7da84a5379c163deae4fb509aa0d155e360a 100644 --- a/readme.md +++ b/readme.md @@ -44,6 +44,7 @@ $ node renovate --help --platform <string> Platform type of repository --endpoint <string> Custom endpoint to use --token <string> Repository Auth Token + --autodiscover [boolean] Autodiscover all repositories --package-files <list> Package file paths --dep-types <list> Dependency types --separate-major-releases [boolean] If set to false, it will upgrade dependencies to latest release only, and not separate major/minor branches diff --git a/test/api/__snapshots__/github.spec.js.snap b/test/api/__snapshots__/github.spec.js.snap index f3308236b93373f26aede528952d25656739ed77..9f42efd0b99651f87743ae084b302e21911f96cc 100644 --- a/test/api/__snapshots__/github.spec.js.snap +++ b/test/api/__snapshots__/github.spec.js.snap @@ -633,6 +633,36 @@ Object { } `; +exports[`api/github getRepos should return an array of repos 1`] = ` +Array [ + Array [ + "user/repos", + ], +] +`; + +exports[`api/github getRepos should return an array of repos 2`] = ` +Array [ + "a/b", + "c/d", +] +`; + +exports[`api/github getRepos should support a custom endpoint 1`] = ` +Array [ + Array [ + "user/repos", + ], +] +`; + +exports[`api/github getRepos should support a custom endpoint 2`] = ` +Array [ + "a/b", + "c/d", +] +`; + exports[`api/github initRepo should initialise the config for the repo - 0 1`] = ` Array [ Array [ diff --git a/test/api/github.spec.js b/test/api/github.spec.js index 0cbae5e55d97788d9227557f69aabdcb6dc01c1a..f8d74a5cbfea7ce573543b07119c6ba20ceb997f 100644 --- a/test/api/github.spec.js +++ b/test/api/github.spec.js @@ -13,6 +13,43 @@ describe('api/github', () => { ghGot = require('gh-got'); }); + async function getRepos(...args) { + // repo info + ghGot.mockImplementationOnce(() => ({ + body: [ + { + full_name: 'a/b', + }, + { + full_name: 'c/d', + }, + ], + })); + return github.getRepos(...args); + } + + describe('getRepos', () => { + it('should throw an error if no token is provided', async () => { + let err; + try { + await github.getRepos(); + } catch (e) { + err = e; + } + expect(err.message).toBe('No token found for getRepos'); + }); + it('should return an array of repos', async () => { + const repos = await getRepos('sometoken'); + expect(ghGot.mock.calls).toMatchSnapshot(); + expect(repos).toMatchSnapshot(); + }); + it('should support a custom endpoint', async () => { + const repos = await getRepos('sometoken', 'someendpoint'); + expect(ghGot.mock.calls).toMatchSnapshot(); + expect(repos).toMatchSnapshot(); + }); + }); + async function initRepo(...args) { // repo info ghGot.mockImplementationOnce(() => ({ diff --git a/test/config/index.spec.js b/test/config/index.spec.js index ff4c40cab0a0e1479868657486f29e3d661d8d2a..05d3b198d5aef0174a9d1f2858f7d74262ea438c 100644 --- a/test/config/index.spec.js +++ b/test/config/index.spec.js @@ -5,75 +5,122 @@ describe('config/index', () => { describe('.parseConfigs(env, defaultArgv)', () => { let configParser; let defaultArgv; + let ghGot; + let glGot; beforeEach(() => { jest.resetModules(); configParser = require('../../lib/config/index.js'); defaultArgv = argv(); + jest.mock('gh-got'); + jest.mock('gl-got'); + ghGot = require('gh-got'); + glGot = require('gl-got'); }); - it('throws for invalid platform', () => { + it('throws for invalid platform', async () => { const env = {}; defaultArgv.push('--platform=foo'); let err; try { - configParser.parseConfigs(env, defaultArgv); + await configParser.parseConfigs(env, defaultArgv); } catch (e) { err = e; } expect(err.message).toBe('Unsupported platform: foo.'); }); - it('throws for no GitHub token', () => { + it('throws for no GitHub token', async () => { const env = {}; let err; try { - configParser.parseConfigs(env, defaultArgv); + await configParser.parseConfigs(env, defaultArgv); } catch (e) { err = e; } expect(err.message).toBe('You need to supply a GitHub token.'); }); - it('throws for no GitLab token', () => { + it('throws for no GitLab token', async () => { const env = { RENOVATE_PLATFORM: 'gitlab' }; let err; try { - configParser.parseConfigs(env, defaultArgv); + await configParser.parseConfigs(env, defaultArgv); } catch (e) { err = e; } expect(err.message).toBe('You need to supply a GitLab token.'); }); - it('supports token in env', () => { + it('supports token in env', async () => { const env = { GITHUB_TOKEN: 'abc' }; let err; try { - configParser.parseConfigs(env, defaultArgv); + await configParser.parseConfigs(env, defaultArgv); } catch (e) { err = e; } - expect(err.message).toBe('At least one repository must be configured'); + expect(err.message).toBe('At least one repository must be configured, or use --autodiscover'); }); - it('supports token in CLI options', () => { + it('supports token in CLI options', async () => { defaultArgv = defaultArgv.concat(['--token=abc']); const env = {}; let err; try { - configParser.parseConfigs(env, defaultArgv); + await configParser.parseConfigs(env, defaultArgv); } catch (e) { err = e; } - expect(err.message).toBe('At least one repository must be configured'); + expect(err.message).toBe('At least one repository must be configured, or use --autodiscover'); }); - it('supports repositories in CLI', () => { + it('autodiscovers github platform', async () => { + const env = {}; + defaultArgv = defaultArgv.concat(['--autodiscover', '--token=abc']); + ghGot.mockImplementationOnce(() => ({ + body: [ + { + full_name: 'a/b', + }, + { + full_name: 'c/d', + }, + ], + })); + await configParser.parseConfigs(env, defaultArgv); + expect(ghGot.mock.calls.length).toBe(1); + expect(glGot.mock.calls.length).toBe(0); + }); + it('autodiscovers gitlab platform', async () => { + const env = {}; + defaultArgv = defaultArgv.concat(['--autodiscover', '--platform=gitlab', '--token=abc']); + glGot.mockImplementationOnce(() => ({ + body: [ + { + path_with_namespace: 'a/b', + }, + ], + })); + await configParser.parseConfigs(env, defaultArgv); + expect(ghGot.mock.calls.length).toBe(0); + expect(glGot.mock.calls.length).toBe(1); + }); + it('logs if no autodiscovered repositories', async () => { + const env = { GITHUB_TOKEN: 'abc' }; + defaultArgv = defaultArgv.concat(['--autodiscover']); + ghGot.mockImplementationOnce(() => ({ + body: [], + })); + await configParser.parseConfigs(env, defaultArgv); + 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']); - configParser.parseConfigs(env, defaultArgv); + await configParser.parseConfigs(env, defaultArgv); const repos = configParser.getRepositories(); should.exist(repos); repos.should.have.length(1); repos[0].repository.should.eql('foo'); }); - it('gets cascaded config', () => { + it('gets cascaded config', async () => { const env = { RENOVATE_CONFIG_FILE: 'test/_fixtures/config/file.js' }; - configParser.parseConfigs(env, defaultArgv); + await configParser.parseConfigs(env, defaultArgv); const repo = configParser.getRepositories().pop(); should.exist(repo); const cascadedConfig = configParser.getCascadedConfig(repo, null);