diff --git a/lib/datasource/github.js b/lib/datasource/github.js index 3b39c6513ef4b58e3267ad6d33e482defa562f7d..2ce70a436822d49076a629288f1c2225224ff8bf 100644 --- a/lib/datasource/github.js +++ b/lib/datasource/github.js @@ -4,7 +4,8 @@ module.exports = { getDependency, }; -async function getDependency(repo, options = {}) { +async function getDependency(purl) { + const { fullname: repo, qualifiers: options } = purl; let versions; let endpoint; let token; diff --git a/lib/datasource/index.js b/lib/datasource/index.js new file mode 100644 index 0000000000000000000000000000000000000000..00de7df572e93f27f569fad85fde1a25dffe0959 --- /dev/null +++ b/lib/datasource/index.js @@ -0,0 +1,29 @@ +const { parse } = require('../util/purl'); + +const github = require('./github'); +const npm = require('./npm'); +const packagist = require('./packagist'); +const pypi = require('./pypi'); + +const datasources = { + github, + npm, + packagist, + pypi, +}; + +function getDependency(purlStr, config) { + const purl = parse(purlStr); + if (!purl) { + return null; + } + if (!datasources[purl.type]) { + logger.warn('Unknown purl type: ' + purl.type); + return null; + } + return datasources[purl.type].getDependency(purl, config); +} + +module.exports = { + getDependency, +}; diff --git a/lib/datasource/npm.js b/lib/datasource/npm.js index 9ed502554101bc668541b31b23a053a1d37998cc..2c287699aa427974df4d5b960a51f4fff1b23d1e 100644 --- a/lib/datasource/npm.js +++ b/lib/datasource/npm.js @@ -88,7 +88,21 @@ function envReplace(value, env = process.env) { }); } -async function getDependency(name, retries = 5) { +function getDependency(input, config) { + const retries = config ? config.retries : undefined; + if (is.string(input)) { + const depName = input; + return getDependencyInner(depName, retries); + } + if (config) { + const exposeEnv = config.global ? config.global.exposeEnv : false; + setNpmrc(config.npmrc, exposeEnv); + } + const purl = input; + return getDependencyInner(purl.fullname, retries); +} + +async function getDependencyInner(name, retries = 5) { logger.trace(`getDependency(${name})`); if (memcache[name]) { logger.trace('Returning cached result'); @@ -135,7 +149,7 @@ async function getDependency(name, retries = 5) { } logger.info('No versions returned, retrying'); await delay(5000 / retries); - return getDependency(name, 0); + return getDependencyInner(name, 0); } const latestVersion = res.versions[res['dist-tags'].latest]; @@ -217,7 +231,7 @@ async function getDependency(name, retries = 5) { } logger.info({ err }, 'npm registry failure: ParseError, retrying'); await delay(5000 / retries); - return getDependency(name, retries - 1); + return getDependencyInner(name, retries - 1); } if (err.statusCode === 429) { if (retries <= 0) { @@ -229,7 +243,7 @@ async function getDependency(name, retries = 5) { `npm too many requests. retrying after ${retryAfter} seconds` ); await delay(1000 * (retryAfter + 1)); - return getDependency(name, retries - 1); + return getDependencyInner(name, retries - 1); } if (err.statusCode === 408) { if (retries <= 0) { @@ -238,7 +252,7 @@ async function getDependency(name, retries = 5) { } logger.info({ err }, 'npm registry failure: timeout, retrying'); await delay(5000 / retries); - return getDependency(name, retries - 1); + return getDependencyInner(name, retries - 1); } if (err.statusCode >= 500 && err.statusCode < 600) { if (retries <= 0) { @@ -247,7 +261,7 @@ async function getDependency(name, retries = 5) { } logger.info({ err }, 'npm registry failure: internal error, retrying'); await delay(5000 / retries); - return getDependency(name, retries - 1); + return getDependencyInner(name, retries - 1); } logger.warn({ err, name }, 'npm registry failure: Unknown error'); throw new Error('registry-failure'); diff --git a/lib/datasource/packagist.js b/lib/datasource/packagist.js index 3f52a2406284c47aff8fccc485e0962311cfee14..79f49b2e704634f05f9d14ebfd56b7b83d6c75fc 100644 --- a/lib/datasource/packagist.js +++ b/lib/datasource/packagist.js @@ -7,7 +7,8 @@ module.exports = { getDependency, }; -async function getDependency(name) { +async function getDependency(purl) { + const { fullname: name } = purl; logger.trace(`getDependency(${name})`); const regUrl = 'https://packagist.org'; diff --git a/lib/datasource/pypi.js b/lib/datasource/pypi.js index 998e1bf4a285e0826d03e424cb0cca979f0e2bbd..b9a2f1c270e85e03b76878e45172be3af18f38b0 100644 --- a/lib/datasource/pypi.js +++ b/lib/datasource/pypi.js @@ -4,7 +4,8 @@ module.exports = { getDependency, }; -async function getDependency(depName) { +async function getDependency(purl) { + const { fullname: depName } = purl; try { const dependency = {}; const rep = await got(`https://pypi.org/pypi/${depName}/json`, { diff --git a/lib/workers/repository/process/lookup/index.js b/lib/workers/repository/process/lookup/index.js index 0c905b0dc32db1bedebe3438bbf55071a1c52bcb..3609e99e4495aa78f2886dda59a25f4c0d8abb15 100644 --- a/lib/workers/repository/process/lookup/index.js +++ b/lib/workers/repository/process/lookup/index.js @@ -2,11 +2,7 @@ const versioning = require('../../../../versioning'); const { getRollbackUpdate } = require('./rollback'); const { getRangeStrategy } = require('../../../../manager'); const { filterVersions } = require('./filter'); -const npmApi = require('../../../../datasource/npm'); -const github = require('../../../../datasource/github'); -const packagist = require('../../../../datasource/packagist'); -const pypi = require('../../../../datasource/pypi'); -const { parse } = require('../../../../../lib/util/purl'); +const { getDependency } = require('../../../../datasource'); module.exports = { lookupUpdates, @@ -24,29 +20,7 @@ async function lookupUpdates(config) { matches, getNewValue, } = versioning(config.versionScheme); - const purl = parse(config.purl); - if (!purl) { - logger.error('Missing purl'); - return []; - } - let dependency; - if (purl.type === 'npm') { - // TODO: move this into datasource - npmApi.setNpmrc( - config.npmrc, - config.global ? config.global.exposeEnv : false - ); - dependency = await npmApi.getDependency(purl.fullname); - } else if (purl.type === 'github') { - dependency = await github.getDependency(purl.fullname, purl.qualifiers); - } else if (purl.type === 'pypi') { - dependency = await pypi.getDependency(purl.fullname); - } else if (purl.type === 'packagist') { - dependency = await packagist.getDependency(purl.fullname); - } else { - logger.warn({ config }, 'Unknown purl'); - return []; - } + const dependency = await getDependency(config.purl, config); if (!dependency) { // If dependency lookup fails then warn and return const result = { diff --git a/test/datasource/__snapshots__/npm.spec.js.snap b/test/datasource/__snapshots__/npm.spec.js.snap index 5de6c2a098b0f4e1163869fd2a5963b5b37d3739..abdf0438f802418de3a05b7e0ba0d1bcc6a3529e 100644 --- a/test/datasource/__snapshots__/npm.spec.js.snap +++ b/test/datasource/__snapshots__/npm.spec.js.snap @@ -66,6 +66,29 @@ Object { } `; +exports[`api/npm should handle purl 1`] = ` +Object { + "dist-tags": Object { + "latest": "0.0.1", + }, + "homepage": undefined, + "latestVersion": "0.0.1", + "name": undefined, + "renovate-config": undefined, + "repositoryUrl": "https://github.com/renovateapp/dummy", + "versions": Object { + "0.0.1": Object { + "canBeUnpublished": false, + "time": "2018-05-06T07:21:53+02:00", + }, + "0.0.2": Object { + "canBeUnpublished": false, + "time": "2018-05-07T07:21:53+02:00", + }, + }, +} +`; + exports[`api/npm should replace any environment variable in npmrc 1`] = ` Object { "dist-tags": Object { diff --git a/test/datasource/github.spec.js b/test/datasource/github.spec.js index 09b248082bbf07a2ed848aec75899ae926a66c8c..7f80095c7362b31d155e307714d733d03557ab87 100644 --- a/test/datasource/github.spec.js +++ b/test/datasource/github.spec.js @@ -1,4 +1,4 @@ -const github = require('../../lib/datasource/github'); +const datasource = require('../../lib/datasource'); const ghGot = require('../../lib/platform/github/gh-got-wrapper'); jest.mock('../../lib/platform/github/gh-got-wrapper'); @@ -14,7 +14,9 @@ describe('datasource/github', () => { { ref: 'refs/tags/v1.1.0' }, ]; ghGot.mockReturnValueOnce({ headers: {}, body }); - const res = await github.getDependency('some/dep', { clean: 'true' }); + const res = await datasource.getDependency( + 'pkg:github/some/dep?clean=true' + ); expect(res).toMatchSnapshot(); expect(Object.keys(res.versions)).toHaveLength(4); expect(res.versions['1.1.0']).toBeDefined(); @@ -27,14 +29,16 @@ describe('datasource/github', () => { { tag_name: 'v1.1.0' }, ]; ghGot.mockReturnValueOnce({ headers: {}, body }); - const res = await github.getDependency('some/dep', { ref: 'release' }); + const res = await datasource.getDependency( + 'pkg:github/some/dep?ref=release' + ); expect(res).toMatchSnapshot(); expect(Object.keys(res.versions)).toHaveLength(4); expect(res.versions['v1.1.0']).toBeDefined(); }); it('returns null for invalid ref', async () => { expect( - await github.getDependency('some/dep', { ref: 'invalid' }) + await datasource.getDependency('pkg:github/some/dep?ref=invalid') ).toBeNull(); }); }); diff --git a/test/datasource/index.spec.js b/test/datasource/index.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..86c46f7874bb2eb28fb012b8ef14a3082075e21a --- /dev/null +++ b/test/datasource/index.spec.js @@ -0,0 +1,7 @@ +const datasource = require('../../lib/datasource'); + +describe('datasource/index', () => { + it('returns null for invalid purl', async () => { + expect(await datasource.getDependency('pkggithub/some/dep')).toBeNull(); + }); +}); diff --git a/test/datasource/npm.spec.js b/test/datasource/npm.spec.js index 70b1e52b2f517b992c3fcad6a91c21a393e3e3df..a1d288445905cfff284b3bb3a6cd06b8915c8655 100644 --- a/test/datasource/npm.spec.js +++ b/test/datasource/npm.spec.js @@ -44,7 +44,7 @@ describe('api/npm', () => { nock('https://registry.npmjs.org') .get('/foobar') .reply(200, missingVersions); - const res = await npm.getDependency('foobar', 1); + const res = await npm.getDependency('foobar', { retries: 1 }); expect(res).toBe(null); }); it('should fetch package info from npm', async () => { @@ -56,6 +56,13 @@ describe('api/npm', () => { expect(res.versions['0.0.1'].canBeUnpublished).toBe(false); expect(res.versions['0.0.2'].canBeUnpublished).toBe(false); }); + it('should handle purl', async () => { + nock('https://registry.npmjs.org') + .get('/foobar') + .reply(200, npmResponse); + const res = await npm.getDependency({ fullname: 'foobar' }); + expect(res).toMatchSnapshot(); + }); it('should handle no time', async () => { delete npmResponse.time['0.0.2']; nock('https://registry.npmjs.org') @@ -110,7 +117,7 @@ describe('api/npm', () => { .reply(200, 'oops'); let e; try { - await npm.getDependency('foobar', 1); + await npm.getDependency('foobar', { retries: 1 }); } catch (err) { e = err; } @@ -125,7 +132,7 @@ describe('api/npm', () => { .reply(429); let e; try { - await npm.getDependency('foobar', 1); + await npm.getDependency('foobar', { retries: 1 }); } catch (err) { e = err; } @@ -137,7 +144,7 @@ describe('api/npm', () => { .reply(503); let e; try { - await npm.getDependency('foobar', 0); + await npm.getDependency('foobar', { retries: 0 }); } catch (err) { e = err; } @@ -149,7 +156,7 @@ describe('api/npm', () => { .reply(408); let e; try { - await npm.getDependency('foobar', 0); + await npm.getDependency('foobar', { retries: 0 }); } catch (err) { e = err; } @@ -165,7 +172,7 @@ describe('api/npm', () => { nock('https://registry.npmjs.org') .get('/foobar') .reply(200); - const res = await npm.getDependency('foobar', 2); + const res = await npm.getDependency('foobar', { retries: 2 }); expect(res).toMatchSnapshot(); }); it('should throw error for others', async () => { diff --git a/test/datasource/packagist.spec.js b/test/datasource/packagist.spec.js index 80f96ddd6b05ac2c3ed49ec6a600f8cae44bd1bf..991c8975b37e0827da951b7b46545dc583973a22 100644 --- a/test/datasource/packagist.spec.js +++ b/test/datasource/packagist.spec.js @@ -1,5 +1,5 @@ const fs = require('fs'); -const packagist = require('../../lib/datasource/packagist'); +const datasource = require('../../lib/datasource'); const got = require('got'); jest.mock('got'); @@ -10,7 +10,9 @@ describe('datasource/packagist', () => { describe('getDependency', () => { it('returns null for empty result', async () => { got.mockReturnValueOnce({}); - expect(await packagist.getDependency('something')).toBeNull(); + expect( + await datasource.getDependency('pkg:packagist/something') + ).toBeNull(); }); it('returns null for 404', async () => { got.mockImplementationOnce(() => @@ -18,20 +20,24 @@ describe('datasource/packagist', () => { statusCode: 404, }) ); - expect(await packagist.getDependency('something')).toBeNull(); + expect( + await datasource.getDependency('pkg:packagist/something') + ).toBeNull(); }); it('returns null for unknown error', async () => { got.mockImplementationOnce(() => { throw new Error(); }); - expect(await packagist.getDependency('something')).toBeNull(); + expect( + await datasource.getDependency('pkg:packagist/something') + ).toBeNull(); }); it('processes real data', async () => { got.mockReturnValueOnce({ body: JSON.parse(res1), }); expect( - await packagist.getDependency('cristianvuolo/uploader') + await datasource.getDependency('pkg:packagist/cristianvuolo/uploader') ).toMatchSnapshot(); }); }); diff --git a/test/datasource/pypi.spec.js b/test/datasource/pypi.spec.js index fc3b232296e7b93c7b2d6d8820bff43a9470d090..8d2a85c1373d17ae23f313550004f8d2dd0a6ea7 100644 --- a/test/datasource/pypi.spec.js +++ b/test/datasource/pypi.spec.js @@ -1,5 +1,5 @@ const fs = require('fs'); -const pypi = require('../../lib/datasource/pypi'); +const datasource = require('../../lib/datasource'); const got = require('got'); jest.mock('got'); @@ -10,19 +10,21 @@ describe('datasource/pypi', () => { describe('getDependency', () => { it('returns null for empty result', async () => { got.mockReturnValueOnce({}); - expect(await pypi.getDependency('something')).toBeNull(); + expect(await datasource.getDependency('pkg:pypi/something')).toBeNull(); }); it('returns null for 404', async () => { got.mockImplementationOnce(() => { throw new Error(); }); - expect(await pypi.getDependency('something')).toBeNull(); + expect(await datasource.getDependency('pkg:pypi/something')).toBeNull(); }); it('processes real data', async () => { got.mockReturnValueOnce({ body: JSON.parse(res1), }); - expect(await pypi.getDependency('azure-cli-monitor')).toMatchSnapshot(); + expect( + await datasource.getDependency('pkg:pypi/azure-cli-monitor') + ).toMatchSnapshot(); }); it('returns non-github home_page', async () => { got.mockReturnValueOnce({ @@ -32,7 +34,9 @@ describe('datasource/pypi', () => { }, }, }); - expect(await pypi.getDependency('something')).toMatchSnapshot(); + expect( + await datasource.getDependency('pkg:pypi/something') + ).toMatchSnapshot(); }); }); }); diff --git a/test/workers/repository/process/lookup/__snapshots__/index.spec.js.snap b/test/workers/repository/process/lookup/__snapshots__/index.spec.js.snap index f5c2e4c8c4ea21676c9868ba226e6c38d9036ec3..bce2c9f576edd9ea3dc9a1b69ffb8a4e40bc0d9f 100644 --- a/test/workers/repository/process/lookup/__snapshots__/index.spec.js.snap +++ b/test/workers/repository/process/lookup/__snapshots__/index.spec.js.snap @@ -114,7 +114,14 @@ Array [ ] `; -exports[`manager/npm/lookup .lookupUpdates() handles unknown purl 1`] = `Array []`; +exports[`manager/npm/lookup .lookupUpdates() handles unknown purl 1`] = ` +Array [ + Object { + "message": "Failed to look up dependency foo", + "type": "warning", + }, +] +`; exports[`manager/npm/lookup .lookupUpdates() ignores pinning for ranges when other upgrade exists 1`] = ` Array [ diff --git a/test/workers/repository/process/lookup/index.spec.js b/test/workers/repository/process/lookup/index.spec.js index 1b2dc4e01f841976c8cd42d7d14e4916bd1c8d40..96a8c4b131af49d74125a5db61c9d46ad3d097b4 100644 --- a/test/workers/repository/process/lookup/index.spec.js +++ b/test/workers/repository/process/lookup/index.spec.js @@ -705,6 +705,7 @@ describe('manager/npm/lookup', () => { config.currentValue = '^4.4.0-canary.3'; config.rangeStrategy = 'replace'; config.depName = 'next'; + config.purl = 'pkg:npm/next'; nock('https://registry.npmjs.org') .get('/next') .reply(200, nextJson);