diff --git a/lib/datasource/__snapshots__/index.spec.ts.snap b/lib/datasource/__snapshots__/index.spec.ts.snap deleted file mode 100644 index 269e3ed3267ddebcc0565ed303e44a124baee401..0000000000000000000000000000000000000000 --- a/lib/datasource/__snapshots__/index.spec.ts.snap +++ /dev/null @@ -1,24 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`datasource/index adds changelogUrl 1`] = ` -Object { - "changelogUrl": "https://github.com/react-native-community/react-native-releases/blob/master/CHANGELOG.md", - "releases": Array [ - Object { - "version": "1.0.0", - }, - ], - "sourceUrl": "https://github.com/react-native-community/react-native-releases", -} -`; - -exports[`datasource/index adds sourceUrl 1`] = ` -Object { - "releases": Array [ - Object { - "version": "1.0.0", - }, - ], - "sourceUrl": "https://github.com/nodejs/node", -} -`; diff --git a/lib/datasource/index.spec.ts b/lib/datasource/index.spec.ts index 165247f86fdbb27a7b31a0e31f38406413ce5199..7cc6ada2700e80c92ed3b0c9b431c4b893500456 100644 --- a/lib/datasource/index.spec.ts +++ b/lib/datasource/index.spec.ts @@ -1,386 +1,536 @@ import fs from 'fs-extra'; -import { mockFn } from 'jest-mock-extended'; -import * as httpMock from '../../test/http-mock'; -import { logger, mocked } from '../../test/util'; +import { logger } from '../../test/util'; import { EXTERNAL_HOST_ERROR, HOST_DISABLED, } from '../constants/error-messages'; import { ExternalHostError } from '../types/errors/external-host-error'; import { loadModules } from '../util/modules'; +import datasources from './api'; import { Datasource } from './datasource'; -import * as datasourceDocker from './docker'; -import { GalaxyDatasource } from './galaxy'; -import * as datasourceGithubTags from './github-tags'; -import * as datasourceMaven from './maven'; -import * as datasourceNpm from './npm'; -import { PackagistDatasource } from './packagist'; -import type { DatasourceApi, GetReleasesConfig } from './types'; -import * as datasource from '.'; - -jest.mock('./docker'); -jest.mock('./maven'); -jest.mock('./npm'); - -const packagistDatasourceGetReleasesMock = - mockFn<DatasourceApi['getReleases']>(); - -jest.mock('./packagist', () => { - return { - __esModule: true, - PackagistDatasource: class extends jest.requireActual< - typeof import('./packagist') - >('./packagist').PackagistDatasource { - override getReleases = (_: GetReleasesConfig) => - packagistDatasourceGetReleasesMock(_); - }, - }; -}); +import type { DatasourceApi, GetReleasesConfig, ReleaseResult } from './types'; +import { + getDatasourceList, + getDatasources, + getDigest, + getPkgReleases, + supportsDigests, +} from '.'; + +const datasource = 'dummy'; +const depName = 'package'; + +type RegistriesMock = Record<string, ReleaseResult | (() => ReleaseResult)>; +const defaultRegistriesMock: RegistriesMock = { + 'https://reg1.com': { releases: [{ version: '1.2.3' }] }, +}; -const dockerDatasource = mocked(datasourceDocker); -const mavenDatasource = mocked(datasourceMaven); -const npmDatasource = mocked(datasourceNpm); +class DummyDatasource extends Datasource { + override defaultRegistryUrls = ['https://reg1.com']; + + constructor(private registriesMock: RegistriesMock = defaultRegistriesMock) { + super(datasource); + } + + override getReleases({ + registryUrl, + }: GetReleasesConfig): Promise<ReleaseResult | null> { + const fn = this.registriesMock[registryUrl]; + if (typeof fn === 'function') { + return Promise.resolve(fn()); + } + return Promise.resolve(fn ?? null); + } +} + +jest.mock('./metadata-manual', () => ({ + manualChangelogUrls: { + dummy: { + package: 'https://foo.bar/package/CHANGELOG.md', + }, + }, + manualSourceUrls: { + dummy: { + package: 'https://foo.bar/package', + }, + }, +})); describe('datasource/index', () => { beforeEach(() => { jest.resetAllMocks(); }); - it('returns datasources', () => { - expect(datasource.getDatasources()).toBeDefined(); - - const managerList = fs - .readdirSync(__dirname, { withFileTypes: true }) - .filter((dirent) => dirent.isDirectory() && !dirent.name.startsWith('_')) - .map((dirent) => dirent.name) - .sort(); - expect(datasource.getDatasourceList()).toEqual(managerList); + + afterEach(() => { + datasources.delete(datasource); }); - it('validates datasource', () => { - function validateDatasource(module: DatasourceApi, name: string): boolean { - if (!module.getReleases) { - return false; + + describe('Validations', () => { + it('returns datasources', () => { + expect(getDatasources()).toBeDefined(); + + const managerList = fs + .readdirSync(__dirname, { withFileTypes: true }) + .filter( + (dirent) => dirent.isDirectory() && !dirent.name.startsWith('_') + ) + .map((dirent) => dirent.name) + .sort(); + expect(getDatasourceList()).toEqual(managerList); + }); + + it('validates datasource', () => { + function validateDatasource( + module: DatasourceApi, + name: string + ): boolean { + if (!module.getReleases) { + return false; + } + return module.id === name; } - return module.id === name; - } - function filterClassBasedDatasources(name: string): boolean { - return !(datasource.getDatasources().get(name) instanceof Datasource); - } - const dss = new Map(datasource.getDatasources()); - for (const ds of dss.values()) { - if (ds instanceof Datasource) { - dss.delete(ds.id); + function filterClassBasedDatasources(name: string): boolean { + return !(getDatasources().get(name) instanceof Datasource); } - } - const loadedDs = loadModules( - __dirname, - validateDatasource, - filterClassBasedDatasources - ); - expect(Array.from(dss.keys())).toEqual(Object.keys(loadedDs)); + const dss = new Map(getDatasources()); - for (const dsName of dss.keys()) { - const ds = dss.get(dsName); - expect(validateDatasource(ds, dsName)).toBeTrue(); - } - }); - it('returns if digests are supported', () => { - expect(datasource.supportsDigests(datasourceGithubTags.id)).toBeTrue(); - }); - it('returns null for no datasource', async () => { - expect( - await datasource.getPkgReleases({ - datasource: null, - depName: 'some/dep', - }) - ).toBeNull(); - }); - it('returns null for no lookupName', async () => { - expect( - await datasource.getPkgReleases({ - datasource: 'npm', - depName: null, - }) - ).toBeNull(); - }); - it('returns null for unknown datasource', async () => { - expect( - await datasource.getPkgReleases({ - datasource: 'gitbucket', - depName: 'some/dep', - }) - ).toBeNull(); - }); - it('returns class datasource', async () => { - expect( - await datasource.getPkgReleases({ - datasource: 'cdnjs', - depName: null, - }) - ).toBeNull(); - }); - it('returns getDigest', async () => { - expect( - await datasource.getDigest({ - datasource: datasourceDocker.id, - depName: 'docker/node', - }) - ).toBeUndefined(); - }); - it('adds changelogUrl', async () => { - npmDatasource.getReleases.mockResolvedValue({ - releases: [{ version: '1.0.0' }], + for (const ds of dss.values()) { + if (ds instanceof Datasource) { + dss.delete(ds.id); + } + } + + const loadedDs = loadModules( + __dirname, + validateDatasource, + filterClassBasedDatasources + ); + expect(Array.from(dss.keys())).toEqual(Object.keys(loadedDs)); + + for (const dsName of dss.keys()) { + const ds = dss.get(dsName); + expect(validateDatasource(ds, dsName)).toBeTrue(); + } }); - const res = await datasource.getPkgReleases({ - datasource: datasourceNpm.id, - depName: 'react-native', + + it('returns null for null datasource', async () => { + expect( + await getPkgReleases({ + datasource: null, + depName: 'some/dep', + }) + ).toBeNull(); }); - expect(res).toMatchSnapshot({ - changelogUrl: - 'https://github.com/react-native-community/react-native-releases/blob/master/CHANGELOG.md', + + it('returns null for no depName', async () => { + datasources.set(datasource, new DummyDatasource()); + expect( + await getPkgReleases({ + datasource: datasource, + depName: null, + }) + ).toBeNull(); }); - }); - it('applies extractVersion', async () => { - npmDatasource.getReleases.mockResolvedValue({ - releases: [ - { version: 'v1.0.0' }, - { version: 'v1.0.1' }, - { version: 'v2' }, - ], + + it('returns null for unknown datasource', async () => { + expect( + await getPkgReleases({ + datasource: 'some-unknown-datasource', + depName: 'some/dep', + }) + ).toBeNull(); }); - const res = await datasource.getPkgReleases({ - datasource: datasourceNpm.id, - depName: 'react-native', - extractVersion: '^(?<version>v\\d+\\.\\d+)', - versioning: 'loose', + + it('ignores and warns for disabled custom registryUrls', async () => { + class TestDatasource extends DummyDatasource { + override readonly customRegistrySupport = false; + } + datasources.set(datasource, new TestDatasource()); + const registryUrls = ['https://foo.bar']; + + const res = await getPkgReleases({ datasource, depName, registryUrls }); + + expect(logger.logger.warn).toHaveBeenCalledWith( + { datasource: 'dummy', registryUrls, defaultRegistryUrls: undefined }, + 'Custom registries are not allowed for this datasource and will be ignored' + ); + expect(res).toMatchObject({ releases: [{ version: '1.2.3' }] }); }); - expect(res.releases).toHaveLength(1); - expect(res.releases[0].version).toBe('v1.0'); }); - it('adds sourceUrl', async () => { - npmDatasource.getReleases.mockResolvedValue({ - releases: [{ version: '1.0.0' }], - }); - const res = await datasource.getPkgReleases({ - datasource: datasourceNpm.id, - depName: 'node', - }); - expect(res).toMatchSnapshot({ - sourceUrl: 'https://github.com/nodejs/node', + + describe('Digest', () => { + it('returns if digests are supported', () => { + datasources.set(datasource, new DummyDatasource()); + expect(supportsDigests(datasource)).toBeFalse(); }); - }); - it('ignores and warns for registryUrls', async () => { - httpMock - .scope('https://galaxy.ansible.com') - .get('/api/v1/roles/') - .query({ owner__username: 'some', name: 'dep' }) - .reply(200, {}); - await datasource.getPkgReleases({ - datasource: GalaxyDatasource.id, - depName: 'some.dep', - registryUrls: ['https://google.com/'], + + it('returns value if defined', async () => { + class TestDatasource extends DummyDatasource { + override getDigest(): Promise<string> { + return Promise.resolve('123'); + } + } + datasources.set(datasource, new TestDatasource()); + + expect(supportsDigests(datasource)).toBeTrue(); + expect(await getDigest({ datasource, depName })).toBe('123'); }); - expect(logger.logger.warn).toHaveBeenCalled(); }); - it('warns if multiple registryUrls for registryStrategy=first', async () => { - dockerDatasource.getReleases.mockResolvedValue(null); - const res = await datasource.getPkgReleases({ - datasource: datasourceDocker.id, - depName: 'something', - registryUrls: ['https://docker.com', 'https://docker.io'], + + describe('Metadata', () => { + beforeEach(() => { + datasources.set(datasource, new DummyDatasource()); }); - expect(res).toBeNull(); - }); - it('hunts registries and returns success', async () => { - packagistDatasourceGetReleasesMock - .mockResolvedValueOnce(null) - .mockResolvedValueOnce({ - releases: [{ version: '1.0.0' }], + + it('adds changelogUrl', async () => { + expect(await getPkgReleases({ datasource, depName })).toMatchObject({ + changelogUrl: 'https://foo.bar/package/CHANGELOG.md', }); - const res = await datasource.getPkgReleases({ - datasource: PackagistDatasource.id, - depName: 'something', - registryUrls: ['https://reg1.com', 'https://reg2.io'], - }); - expect(res).not.toBeNull(); - }); - it('returns null for HOST_DISABLED', async () => { - packagistDatasourceGetReleasesMock.mockImplementationOnce(() => { - throw new ExternalHostError(new Error(HOST_DISABLED)); - }); - expect( - await datasource.getPkgReleases({ - datasource: PackagistDatasource.id, - depName: 'something', - registryUrls: ['https://reg1.com'], - }) - ).toBeNull(); - }); - it('hunts registries and aborts on ExternalHostError', async () => { - packagistDatasourceGetReleasesMock.mockRejectedValue( - new ExternalHostError(new Error()) - ); - await expect( - datasource.getPkgReleases({ - datasource: PackagistDatasource.id, - depName: 'something', - registryUrls: ['https://reg1.com', 'https://reg2.io'], - }) - ).rejects.toThrow(EXTERNAL_HOST_ERROR); - }); - it('hunts registries and returns null', async () => { - packagistDatasourceGetReleasesMock.mockImplementationOnce(() => { - throw new Error('a'); - }); - packagistDatasourceGetReleasesMock.mockImplementationOnce(() => { - throw new Error('b'); - }); - expect( - await datasource.getPkgReleases({ - datasource: PackagistDatasource.id, - depName: 'something', - registryUrls: ['https://reg1.com', 'https://reg2.io'], - }) - ).toBeNull(); - }); - it('merges custom defaultRegistryUrls and returns success', async () => { - mavenDatasource.getReleases.mockResolvedValueOnce({ - releases: [{ version: '1.0.0' }, { version: '1.1.0' }], - }); - mavenDatasource.getReleases.mockResolvedValueOnce({ - releases: [{ version: '1.0.0' }], - }); - const res = await datasource.getPkgReleases({ - datasource: datasourceMaven.id, - depName: 'something', - defaultRegistryUrls: ['https://reg1.com', 'https://reg2.io'], - }); - expect(res).toEqual({ - releases: [ - { - registryUrl: 'https://reg1.com', - version: '1.0.0', - }, - { - registryUrl: 'https://reg1.com', - version: '1.1.0', - }, - ], - }); - }); - it('ignores custom defaultRegistryUrls if registrUrls are set', async () => { - mavenDatasource.getReleases.mockResolvedValueOnce({ - releases: [{ version: '1.0.0' }, { version: '1.1.0' }], - }); - mavenDatasource.getReleases.mockResolvedValueOnce({ - releases: [{ version: '1.0.0' }], }); - const res = await datasource.getPkgReleases({ - datasource: datasourceMaven.id, - depName: 'something', - defaultRegistryUrls: ['https://reg3.com'], - registryUrls: ['https://reg1.com', 'https://reg2.io'], - }); - expect(res).toEqual({ - releases: [ - { - registryUrl: 'https://reg1.com', - version: '1.0.0', - }, - { - registryUrl: 'https://reg1.com', - version: '1.1.0', - }, - ], + + it('adds sourceUrl', async () => { + expect(await getPkgReleases({ datasource, depName })).toMatchObject({ + sourceUrl: 'https://foo.bar/package', + }); }); }); - it('merges registries and returns success', async () => { - mavenDatasource.getReleases.mockResolvedValueOnce({ - releases: [{ version: '1.0.0' }, { version: '1.1.0' }], - }); - mavenDatasource.getReleases.mockResolvedValueOnce({ - releases: [{ version: '1.0.0' }], - }); - const res = await datasource.getPkgReleases({ - datasource: datasourceMaven.id, - depName: 'something', - registryUrls: ['https://reg1.com', 'https://reg2.io'], + + describe('Packages', () => { + it('supports defaultRegistryUrls parameter', async () => { + const registries: RegistriesMock = { + 'https://foo.bar': { releases: [{ version: '0.0.1' }] }, + }; + datasources.set(datasource, new DummyDatasource(registries)); + + const res = await getPkgReleases({ + datasource, + depName, + defaultRegistryUrls: ['https://foo.bar'], + }); + expect(res).toMatchObject({ releases: [{ version: '0.0.1' }] }); }); - expect(res).toEqual({ - releases: [ - { - registryUrl: 'https://reg1.com', - version: '1.0.0', - }, - { - registryUrl: 'https://reg1.com', - version: '1.1.0', + + it('applies extractVersion', async () => { + const registries: RegistriesMock = { + 'https://reg1.com': { + releases: [{ version: 'v4.3.143' }, { version: 'rc4.3.143' }], }, - ], - }); - }); - it('merges registries and aborts on ExternalHostError', async () => { - mavenDatasource.getReleases.mockImplementationOnce(() => { - throw new ExternalHostError(new Error()); - }); - await expect( - datasource.getPkgReleases({ - datasource: datasourceMaven.id, - depName: 'something', - registryUrls: ['https://reg1.com', 'https://reg2.io'], - }) - ).rejects.toThrow(EXTERNAL_HOST_ERROR); - }); - it('merges registries and returns null for error', async () => { - mavenDatasource.getReleases.mockImplementationOnce(() => { - throw new Error('a'); - }); - mavenDatasource.getReleases.mockImplementationOnce(() => { - throw new Error('b'); - }); - expect( - await datasource.getPkgReleases({ - datasource: datasourceMaven.id, - depName: 'something', - registryUrls: ['https://reg1.com', 'https://reg2.io'], - }) - ).toBeNull(); - }); - it('trims sourceUrl', async () => { - npmDatasource.getReleases.mockResolvedValue({ - sourceUrl: ' https://abc.com', - releases: [{ version: '1.0.0' }], - }); - const res = await datasource.getPkgReleases({ - datasource: datasourceNpm.id, - depName: 'abc', + }; + datasources.set(datasource, new DummyDatasource(registries)); + + const res = await getPkgReleases({ + datasource, + depName, + extractVersion: '^(?<version>v\\d+\\.\\d+)', + versioning: 'loose', + }); + expect(res).toMatchObject({ releases: [{ version: 'v4.3' }] }); }); - expect(res.sourceUrl).toBe('https://abc.com'); - }); - it('massages sourceUrl', async () => { - npmDatasource.getReleases.mockResolvedValue({ - sourceUrl: 'scm:git@github.com:Jasig/cas.git', - releases: [{ version: '1.0.0' }], + + it('trims sourceUrl', async () => { + datasources.set( + datasource, + new DummyDatasource({ + 'https://reg1.com': { + sourceUrl: ' https://abc.com ', + releases: [{ version: '1.0.0' }], + }, + }) + ); + const res = await getPkgReleases({ + datasource, + depName: 'foobar', + }); + expect(res).toMatchObject({ sourceUrl: 'https://abc.com' }); }); - const res = await datasource.getPkgReleases({ - datasource: datasourceNpm.id, - depName: 'cas', + + it('massages sourceUrl', async () => { + datasources.set( + datasource, + new DummyDatasource({ + 'https://reg1.com': { + sourceUrl: 'scm:git@github.com:Jasig/cas.git', + releases: [{ version: '1.0.0' }], + }, + }) + ); + const res = await getPkgReleases({ + datasource, + depName: 'foobar', + }); + expect(res).toMatchObject({ sourceUrl: 'https://github.com/Jasig/cas' }); }); - expect(res.sourceUrl).toBe('https://github.com/Jasig/cas'); - }); - it('applies replacements', async () => { - npmDatasource.getReleases.mockResolvedValue({ - releases: [{ version: '1.0.0' }], + it('applies replacements', async () => { + datasources.set(datasource, new DummyDatasource()); + const res = await getPkgReleases({ + datasource, + depName, + replacementName: 'def', + replacementVersion: '2.0.0', + }); + expect(res).toMatchObject({ + replacementName: 'def', + replacementVersion: '2.0.0', + }); }); - const res = await datasource.getPkgReleases({ - datasource: datasourceNpm.id, - depName: 'abc', - replacementName: 'def', - replacementVersion: '2.0.0', + + describe('Registry strategies', () => { + describe('first', () => { + class FirstRegistryDatasource extends DummyDatasource { + override readonly registryStrategy = 'first'; + } + + it('returns value from single registry', async () => { + datasources.set(datasource, new FirstRegistryDatasource()); + + const res = await getPkgReleases({ + datasource, + depName, + registryUrls: ['https://reg1.com'], + }); + + expect(res).toMatchObject({ + releases: [{ version: '1.2.3' }], + registryUrl: 'https://reg1.com', + }); + expect(logger.logger.warn).not.toHaveBeenCalled(); + }); + + it('warns and returns first result', async () => { + const registries: RegistriesMock = { + 'https://reg1.com': { releases: [{ version: '1.0.0' }] }, + 'https://reg2.com': { releases: [{ version: '2.0.0' }] }, + 'https://reg3.com': null, + }; + const registryUrls = Object.keys(registries); + datasources.set(datasource, new FirstRegistryDatasource(registries)); + + const res = await getPkgReleases({ + datasource, + depName, + registryUrls, + }); + + expect(res).toMatchObject({ + releases: [{ version: '1.0.0' }], + registryUrl: 'https://reg1.com', + }); + expect(logger.logger.warn).toHaveBeenCalledWith( + { + datasource: 'dummy', + depName: 'package', + registryUrls, + }, + 'Excess registryUrls found for datasource lookup - using first configured only' + ); + }); + + it('warns and returns first null', async () => { + const registries: RegistriesMock = { + 'https://reg1.com': null, + 'https://reg2.com': { releases: [{ version: '1.2.3' }] }, + }; + const registryUrls = Object.keys(registries); + datasources.set(datasource, new FirstRegistryDatasource(registries)); + + const res = await getPkgReleases({ + datasource, + depName, + registryUrls, + }); + + expect(res).toBeNull(); + expect(logger.logger.warn).toHaveBeenCalledWith( + { datasource, depName, registryUrls }, + 'Excess registryUrls found for datasource lookup - using first configured only' + ); + }); + }); + + describe('merge', () => { + class MergeRegistriesDatasource extends DummyDatasource { + override readonly registryStrategy = 'merge'; + override readonly defaultRegistryUrls = [ + 'https://reg1.com', + 'https://reg2.com', + ]; + } + + const registries: RegistriesMock = { + 'https://reg1.com': () => ({ releases: [{ version: '1.0.0' }] }), + 'https://reg2.com': () => ({ releases: [{ version: '1.1.0' }] }), + 'https://reg3.com': () => { + throw new ExternalHostError(new Error()); + }, + 'https://reg4.com': () => { + throw new Error('a'); + }, + 'https://reg5.com': () => { + throw new Error('b'); + }, + }; + + beforeEach(() => { + datasources.set( + datasource, + new MergeRegistriesDatasource(registries) + ); + }); + + it('merges custom defaultRegistryUrls and returns success', async () => { + const res = await getPkgReleases({ datasource, depName }); + + expect(res).toMatchObject({ + releases: [ + { registryUrl: 'https://reg1.com', version: '1.0.0' }, + { registryUrl: 'https://reg2.com', version: '1.1.0' }, + ], + }); + }); + + it('ignores custom defaultRegistryUrls if registrUrls are set', async () => { + const res = await getPkgReleases({ + datasource, + depName, + defaultRegistryUrls: ['https://reg3.com'], + registryUrls: ['https://reg1.com', 'https://reg2.com'], + }); + + expect(res).toMatchObject({ + releases: [ + { registryUrl: 'https://reg1.com', version: '1.0.0' }, + { registryUrl: 'https://reg2.com', version: '1.1.0' }, + ], + }); + }); + + it('merges registries and returns success', async () => { + const res = await getPkgReleases({ + datasource, + depName, + registryUrls: ['https://reg1.com', 'https://reg2.com'], + }); + expect(res).toMatchObject({ + releases: [ + { registryUrl: 'https://reg1.com', version: '1.0.0' }, + { registryUrl: 'https://reg2.com', version: '1.1.0' }, + ], + }); + }); + + it('merges registries and aborts on ExternalHostError', async () => { + await expect( + getPkgReleases({ + datasource, + depName, + registryUrls: [ + 'https://reg1.com', + 'https://reg2.com', + 'https://reg3.com', + ], + }) + ).rejects.toThrow(EXTERNAL_HOST_ERROR); + }); + + it('merges registries and returns null for error', async () => { + expect( + await getPkgReleases({ + datasource, + depName, + registryUrls: ['https://reg4.com', 'https://reg5.com'], + }) + ).toBeNull(); + }); + }); + + describe('hunt', () => { + class HuntRegistriyDatasource extends DummyDatasource { + override readonly registryStrategy = 'hunt'; + } + + it('returns first successful result', async () => { + const registries: RegistriesMock = { + 'https://reg1.com': null, + 'https://reg2.com': () => { + throw new Error('unknown'); + }, + 'https://reg3.com': { releases: [{ version: '1.0.0' }] }, + 'https://reg4.com': { releases: [{ version: '2.0.0' }] }, + 'https://reg5.com': { releases: [{ version: '3.0.0' }] }, + }; + const registryUrls = Object.keys(registries); + datasources.set(datasource, new HuntRegistriyDatasource(registries)); + + const res = await getPkgReleases({ + datasource, + depName, + registryUrls, + }); + + expect(res).toMatchObject({ + registryUrl: 'https://reg3.com', + releases: [{ version: '1.0.0' }], + }); + }); + + it('returns null for HOST_DISABLED', async () => { + const registries: RegistriesMock = { + 'https://reg1.com': () => { + throw new ExternalHostError(new Error(HOST_DISABLED)); + }, + 'https://reg2.com': { releases: [{ version: '1.0.0' }] }, + }; + const registryUrls = Object.keys(registries); + datasources.set(datasource, new HuntRegistriyDatasource(registries)); + + const res = await getPkgReleases({ + datasource, + depName, + registryUrls, + }); + + expect(res).toBeNull(); + }); + + it('aborts on ExternalHostError', async () => { + const registries: RegistriesMock = { + 'https://reg1.com': () => { + throw new ExternalHostError(new Error('something unknown')); + }, + 'https://reg2.com': { releases: [{ version: '1.0.0' }] }, + }; + const registryUrls = Object.keys(registries); + datasources.set(datasource, new HuntRegistriyDatasource(registries)); + + await expect( + getPkgReleases({ datasource, depName, registryUrls }) + ).rejects.toThrow(EXTERNAL_HOST_ERROR); + }); + + it('returns null if no releases are found', async () => { + const registries: RegistriesMock = { + 'https://reg1.com': () => { + throw new Error('a'); + }, + 'https://reg2.com': () => { + throw new Error('b'); + }, + }; + const registryUrls = Object.keys(registries); + datasources.set(datasource, new HuntRegistriyDatasource(registries)); + + const res = await getPkgReleases({ + datasource, + depName, + registryUrls, + }); + + expect(res).toBeNull(); + }); + }); }); - expect(res.replacementName).toBe('def'); - expect(res.replacementVersion).toBe('2.0.0'); }); }); diff --git a/lib/datasource/metadata-manual.ts b/lib/datasource/metadata-manual.ts new file mode 100644 index 0000000000000000000000000000000000000000..0144de98d5ea7cb6f56f0febfb15f1077ad3880d --- /dev/null +++ b/lib/datasource/metadata-manual.ts @@ -0,0 +1,103 @@ +// Use this object to define changelog URLs for packages +// Only necessary when the changelog data cannot be found in the package's source repository +export const manualChangelogUrls: Record<string, Record<string, string>> = { + npm: { + 'babel-preset-react-app': + 'https://github.com/facebook/create-react-app/releases', + firebase: 'https://firebase.google.com/support/release-notes/js', + 'flow-bin': 'https://github.com/facebook/flow/blob/master/Changelog.md', + gatsby: + 'https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby/CHANGELOG.md', + 'react-native': + 'https://github.com/react-native-community/react-native-releases/blob/master/CHANGELOG.md', + sharp: 'https://github.com/lovell/sharp/blob/master/docs/changelog.md', + 'tailwindcss-classnames': + 'https://github.com/muhammadsammy/tailwindcss-classnames/blob/master/CHANGELOG.md', + 'zone.js': + 'https://github.com/angular/angular/blob/master/packages/zone.js/CHANGELOG.md', + }, + pypi: { + alembic: 'https://alembic.sqlalchemy.org/en/latest/changelog.html', + beautifulsoup4: + 'https://bazaar.launchpad.net/~leonardr/beautifulsoup/bs4/view/head:/CHANGELOG', + django: 'https://github.com/django/django/tree/master/docs/releases', + djangorestframework: + 'https://www.django-rest-framework.org/community/release-notes/', + flake8: 'http://flake8.pycqa.org/en/latest/release-notes/index.html', + 'django-storages': + 'https://github.com/jschneier/django-storages/blob/master/CHANGELOG.rst', + hypothesis: + 'https://github.com/HypothesisWorks/hypothesis/blob/master/hypothesis-python/docs/changes.rst', + lxml: 'https://git.launchpad.net/lxml/plain/CHANGES.txt', + mypy: 'https://mypy-lang.blogspot.com/', + phonenumbers: + 'https://github.com/daviddrysdale/python-phonenumbers/blob/dev/python/HISTORY.md', + psycopg2: 'http://initd.org/psycopg/articles/tag/release/', + 'psycopg2-binary': 'http://initd.org/psycopg/articles/tag/release/', + pycountry: + 'https://github.com/flyingcircusio/pycountry/blob/master/HISTORY.txt', + 'django-debug-toolbar': + 'https://django-debug-toolbar.readthedocs.io/en/latest/changes.html', + 'firebase-admin': + 'https://firebase.google.com/support/release-notes/admin/python', + requests: 'https://github.com/psf/requests/blob/master/HISTORY.md', + sqlalchemy: 'https://docs.sqlalchemy.org/en/latest/changelog/', + uwsgi: 'https://uwsgi-docs.readthedocs.io/en/latest/#release-notes', + wagtail: 'https://github.com/wagtail/wagtail/tree/master/docs/releases', + }, + docker: { + 'gitlab/gitlab-ce': + 'https://gitlab.com/gitlab-org/gitlab-foss/-/blob/master/CHANGELOG.md', + 'gitlab/gitlab-runner': + 'https://gitlab.com/gitlab-org/gitlab-runner/-/blob/master/CHANGELOG.md', + 'google/cloud-sdk': 'https://cloud.google.com/sdk/docs/release-notes', + neo4j: 'https://neo4j.com/release-notes/', + 'whitesource/renovate': 'https://github.com/whitesource/renovate-on-prem', + }, +}; + +// Use this object to define manual source URLs for packages +// Only necessary if the datasource is unable to locate the source URL itself +export const manualSourceUrls: Record<string, Record<string, string>> = { + orb: { + 'cypress-io/cypress': 'https://github.com/cypress-io/circleci-orb', + 'hutson/library-release-workflows': + 'https://github.com/hyper-expanse/library-release-workflows', + }, + docker: { + 'amd64/registry': 'https://github.com/distribution/distribution', + 'amd64/traefik': 'https://github.com/containous/traefik', + 'coredns/coredns': 'https://github.com/coredns/coredns', + 'docker/compose': 'https://github.com/docker/compose', + 'drone/drone': 'https://github.com/drone/drone', + 'drone/drone-runner-docker': + 'https://github.com/drone-runners/drone-runner-docker', + 'drone/drone-runner-kube': + 'https://github.com/drone-runners/drone-runner-kube', + 'drone/drone-runner-ssh': + 'https://github.com/drone-runners/drone-runner-ssh', + 'gcr.io/kaniko-project/executor': + 'https://github.com/GoogleContainerTools/kaniko', + 'gitlab/gitlab-ce': 'https://gitlab.com/gitlab-org/gitlab-foss', + 'gitlab/gitlab-runner': 'https://gitlab.com/gitlab-org/gitlab-runner', + 'gitea/gitea': 'https://github.com/go-gitea/gitea', + 'hashicorp/terraform': 'https://github.com/hashicorp/terraform', + node: 'https://github.com/nodejs/node', + registry: 'https://github.com/distribution/distribution', + traefik: 'https://github.com/containous/traefik', + }, + kubernetes: { + node: 'https://github.com/nodejs/node', + }, + npm: { + node: 'https://github.com/nodejs/node', + }, + nvm: { + node: 'https://github.com/nodejs/node', + }, + pypi: { + mkdocs: 'https://github.com/mkdocs/mkdocs', + 'mkdocs-material': 'https://github.com/squidfunk/mkdocs-material', + mypy: 'https://github.com/python/mypy', + }, +}; diff --git a/lib/datasource/metadata.ts b/lib/datasource/metadata.ts index 927dfe99e19c6fd6fe964b622793db9f130cec6e..e2e970cf7dd95faf8aa1edadd6f70153de0fa463 100644 --- a/lib/datasource/metadata.ts +++ b/lib/datasource/metadata.ts @@ -5,112 +5,9 @@ import { DateTime } from 'luxon'; import * as hostRules from '../util/host-rules'; import { regEx } from '../util/regex'; import { validateUrl } from '../util/url'; +import { manualChangelogUrls, manualSourceUrls } from './metadata-manual'; import type { ReleaseResult } from './types'; -// Use this object to define changelog URLs for packages -// Only necessary when the changelog data cannot be found in the package's source repository -const manualChangelogUrls: Record<string, Record<string, string>> = { - npm: { - 'babel-preset-react-app': - 'https://github.com/facebook/create-react-app/releases', - firebase: 'https://firebase.google.com/support/release-notes/js', - 'flow-bin': 'https://github.com/facebook/flow/blob/master/Changelog.md', - gatsby: - 'https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby/CHANGELOG.md', - 'react-native': - 'https://github.com/react-native-community/react-native-releases/blob/master/CHANGELOG.md', - sharp: 'https://github.com/lovell/sharp/blob/master/docs/changelog.md', - 'tailwindcss-classnames': - 'https://github.com/muhammadsammy/tailwindcss-classnames/blob/master/CHANGELOG.md', - 'zone.js': - 'https://github.com/angular/angular/blob/master/packages/zone.js/CHANGELOG.md', - }, - pypi: { - alembic: 'https://alembic.sqlalchemy.org/en/latest/changelog.html', - beautifulsoup4: - 'https://bazaar.launchpad.net/~leonardr/beautifulsoup/bs4/view/head:/CHANGELOG', - django: 'https://github.com/django/django/tree/master/docs/releases', - djangorestframework: - 'https://www.django-rest-framework.org/community/release-notes/', - flake8: 'http://flake8.pycqa.org/en/latest/release-notes/index.html', - 'django-storages': - 'https://github.com/jschneier/django-storages/blob/master/CHANGELOG.rst', - hypothesis: - 'https://github.com/HypothesisWorks/hypothesis/blob/master/hypothesis-python/docs/changes.rst', - lxml: 'https://git.launchpad.net/lxml/plain/CHANGES.txt', - mypy: 'https://mypy-lang.blogspot.com/', - phonenumbers: - 'https://github.com/daviddrysdale/python-phonenumbers/blob/dev/python/HISTORY.md', - psycopg2: 'http://initd.org/psycopg/articles/tag/release/', - 'psycopg2-binary': 'http://initd.org/psycopg/articles/tag/release/', - pycountry: - 'https://github.com/flyingcircusio/pycountry/blob/master/HISTORY.txt', - 'django-debug-toolbar': - 'https://django-debug-toolbar.readthedocs.io/en/latest/changes.html', - 'firebase-admin': - 'https://firebase.google.com/support/release-notes/admin/python', - requests: 'https://github.com/psf/requests/blob/master/HISTORY.md', - sqlalchemy: 'https://docs.sqlalchemy.org/en/latest/changelog/', - uwsgi: 'https://uwsgi-docs.readthedocs.io/en/latest/#release-notes', - wagtail: 'https://github.com/wagtail/wagtail/tree/master/docs/releases', - }, - docker: { - 'gitlab/gitlab-ce': - 'https://gitlab.com/gitlab-org/gitlab-foss/-/blob/master/CHANGELOG.md', - 'gitlab/gitlab-runner': - 'https://gitlab.com/gitlab-org/gitlab-runner/-/blob/master/CHANGELOG.md', - 'google/cloud-sdk': 'https://cloud.google.com/sdk/docs/release-notes', - neo4j: 'https://neo4j.com/release-notes/', - 'whitesource/renovate': 'https://github.com/whitesource/renovate-on-prem', - }, -}; - -// Use this object to define manual source URLs for packages -// Only necessary if the datasource is unable to locate the source URL itself -const manualSourceUrls: Record<string, Record<string, string>> = { - orb: { - 'cypress-io/cypress': 'https://github.com/cypress-io/circleci-orb', - 'hutson/library-release-workflows': - 'https://github.com/hyper-expanse/library-release-workflows', - }, - docker: { - 'amd64/registry': 'https://github.com/distribution/distribution', - 'amd64/traefik': 'https://github.com/containous/traefik', - 'coredns/coredns': 'https://github.com/coredns/coredns', - 'docker/compose': 'https://github.com/docker/compose', - 'drone/drone': 'https://github.com/drone/drone', - 'drone/drone-runner-docker': - 'https://github.com/drone-runners/drone-runner-docker', - 'drone/drone-runner-kube': - 'https://github.com/drone-runners/drone-runner-kube', - 'drone/drone-runner-ssh': - 'https://github.com/drone-runners/drone-runner-ssh', - 'gcr.io/kaniko-project/executor': - 'https://github.com/GoogleContainerTools/kaniko', - 'gitlab/gitlab-ce': 'https://gitlab.com/gitlab-org/gitlab-foss', - 'gitlab/gitlab-runner': 'https://gitlab.com/gitlab-org/gitlab-runner', - 'gitea/gitea': 'https://github.com/go-gitea/gitea', - 'hashicorp/terraform': 'https://github.com/hashicorp/terraform', - node: 'https://github.com/nodejs/node', - registry: 'https://github.com/distribution/distribution', - traefik: 'https://github.com/containous/traefik', - }, - kubernetes: { - node: 'https://github.com/nodejs/node', - }, - npm: { - node: 'https://github.com/nodejs/node', - }, - nvm: { - node: 'https://github.com/nodejs/node', - }, - pypi: { - mkdocs: 'https://github.com/mkdocs/mkdocs', - 'mkdocs-material': 'https://github.com/squidfunk/mkdocs-material', - mypy: 'https://github.com/python/mypy', - }, -}; - const githubPages = regEx('^https://([^.]+).github.com/([^/]+)$'); const gitPrefix = regEx('^git:/?/?');