diff --git a/lib/datasource/pod/__snapshots__/index.spec.ts.snap b/lib/datasource/pod/__snapshots__/index.spec.ts.snap deleted file mode 100644 index 7ce8a3c189b25bfffedcaef42bb00106c83f7ebe..0000000000000000000000000000000000000000 --- a/lib/datasource/pod/__snapshots__/index.spec.ts.snap +++ /dev/null @@ -1,107 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`datasource/pod/index getReleases processes real data from CDN 1`] = ` -Array [ - Object { - "headers": Object { - "accept-encoding": "gzip, deflate, br", - "host": "cdn.cocoapods.org", - "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", - }, - "method": "GET", - "url": "https://cdn.cocoapods.org/all_pods_versions_a_c_b.txt", - }, -] -`; - -exports[`datasource/pod/index getReleases processes real data from Github 1`] = ` -Array [ - Object { - "headers": Object { - "accept": "application/vnd.github.v3+json", - "accept-encoding": "gzip, deflate, br", - "host": "api.github.com", - "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", - }, - "method": "GET", - "url": "https://api.github.com/repos/Artsy/Specs/contents/Specs/foo", - }, - Object { - "headers": Object { - "accept": "application/vnd.github.v3+json", - "accept-encoding": "gzip, deflate, br", - "host": "api.github.com", - "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", - }, - "method": "GET", - "url": "https://api.github.com/repos/Artsy/Specs/contents/Specs/a/c/b/foo", - }, -] -`; - -exports[`datasource/pod/index getReleases returns null for 401 1`] = ` -Array [ - Object { - "headers": Object { - "accept-encoding": "gzip, deflate, br", - "host": "cdn.cocoapods.org", - "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", - }, - "method": "GET", - "url": "https://cdn.cocoapods.org/all_pods_versions_a_c_b.txt", - }, -] -`; - -exports[`datasource/pod/index getReleases returns null for 404 1`] = ` -Array [ - Object { - "headers": Object { - "accept": "application/vnd.github.v3+json", - "accept-encoding": "gzip, deflate, br", - "host": "api.github.com", - "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", - }, - "method": "GET", - "url": "https://api.github.com/repos/foo/bar/contents/Specs/foo", - }, - Object { - "headers": Object { - "accept": "application/vnd.github.v3+json", - "accept-encoding": "gzip, deflate, br", - "host": "api.github.com", - "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", - }, - "method": "GET", - "url": "https://api.github.com/repos/foo/bar/contents/Specs/a/c/b/foo", - }, -] -`; - -exports[`datasource/pod/index getReleases returns null for unknown error 1`] = ` -Array [ - Object { - "headers": Object { - "accept-encoding": "gzip, deflate, br", - "host": "cdn.cocoapods.org", - "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", - }, - "method": "GET", - "url": "https://cdn.cocoapods.org/all_pods_versions_a_c_b.txt", - }, -] -`; - -exports[`datasource/pod/index getReleases throws for 429 1`] = ` -Array [ - Object { - "headers": Object { - "accept-encoding": "gzip, deflate, br", - "host": "cdn.cocoapods.org", - "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", - }, - "method": "GET", - "url": "https://cdn.cocoapods.org/all_pods_versions_a_c_b.txt", - }, -] -`; diff --git a/lib/datasource/pod/index.spec.ts b/lib/datasource/pod/index.spec.ts index 28d681b3fd4af5391bf96eb558732c878a6f16c4..e2aef58e025d89197161a3c7af47aa7a341b940f 100644 --- a/lib/datasource/pod/index.spec.ts +++ b/lib/datasource/pod/index.spec.ts @@ -12,6 +12,8 @@ const config = { }; const githubApiHost = 'https://api.github.com'; +const githubEntApiHost = 'https://github.foo.com'; +const githubEntApiHost2 = 'https://ghe.foo.com'; const cocoapodsHost = 'https://cdn.cocoapods.org'; describe('datasource/pod/index', () => { @@ -45,16 +47,56 @@ describe('datasource/pod/index', () => { it('returns null for 404', async () => { httpMock .scope(githubApiHost) + .get('/repos/foo/bar/contents/Specs/a/c/b/foo') + .reply(404) + .get('/repos/foo/bar/contents/a/c/b/foo') + .reply(404) .get('/repos/foo/bar/contents/Specs/foo') .reply(404) - .get('/repos/foo/bar/contents/Specs/a/c/b/foo') + .get('/repos/foo/bar/contents/foo') .reply(404); const res = await getPkgReleases({ ...config, registryUrls: [...config.registryUrls, 'https://github.com/foo/bar'], }); expect(res).toBeNull(); - expect(httpMock.getTrace()).toMatchSnapshot(); + }); + it('returns null for 404 Github enterprise', async () => { + httpMock + .scope(githubEntApiHost) + .get('/api/v3/repos/foo/bar/contents/Specs/a/c/b/foo') + .reply(404) + .get('/api/v3/repos/foo/bar/contents/a/c/b/foo') + .reply(404) + .get('/api/v3/repos/foo/bar/contents/Specs/foo') + .reply(404) + .get('/api/v3/repos/foo/bar/contents/foo') + .reply(404); + const res = await getPkgReleases({ + ...config, + registryUrls: [ + ...config.registryUrls, + 'https://github.foo.com/foo/bar', + ], + }); + expect(res).toBeNull(); + }); + it('returns null for 404 Github enterprise with different url style', async () => { + httpMock + .scope(githubEntApiHost2) + .get('/api/v3/repos/foo/bar/contents/Specs/a/c/b/foo') + .reply(404) + .get('/api/v3/repos/foo/bar/contents/a/c/b/foo') + .reply(404) + .get('/api/v3/repos/foo/bar/contents/Specs/foo') + .reply(404) + .get('/api/v3/repos/foo/bar/contents/foo') + .reply(404); + const res = await getPkgReleases({ + ...config, + registryUrls: [...config.registryUrls, 'https://ghe.foo.com/foo/bar'], + }); + expect(res).toBeNull(); }); it('returns null for 401', async () => { httpMock @@ -62,7 +104,6 @@ describe('datasource/pod/index', () => { .get('/all_pods_versions_a_c_b.txt') .reply(401); expect(await getPkgReleases(config)).toBeNull(); - expect(httpMock.getTrace()).toMatchSnapshot(); }); it('throws for 429', async () => { httpMock @@ -70,7 +111,6 @@ describe('datasource/pod/index', () => { .get('/all_pods_versions_a_c_b.txt') .reply(429); await expect(getPkgReleases(config)).rejects.toThrow(EXTERNAL_HOST_ERROR); - expect(httpMock.getTrace()).toMatchSnapshot(); }); it('returns null for unknown error', async () => { httpMock @@ -78,7 +118,6 @@ describe('datasource/pod/index', () => { .get('/all_pods_versions_a_c_b.txt') .replyWithError('foobar'); expect(await getPkgReleases(config)).toBeNull(); - expect(httpMock.getTrace()).toMatchSnapshot(); }); it('processes real data from CDN', async () => { httpMock @@ -98,14 +137,77 @@ describe('datasource/pod/index', () => { }, ], }); - expect(httpMock.getTrace()).toMatchSnapshot(); }); - it('processes real data from Github', async () => { + it('processes real data from Github with shard with specs', async () => { httpMock .scope(githubApiHost) - .get('/repos/Artsy/Specs/contents/Specs/foo') + .get('/repos/Artsy/Specs/contents/Specs/a/c/b/foo') + .reply(200, [{ name: '1.2.3' }]); + const res = await getPkgReleases({ + ...config, + registryUrls: ['https://github.com/Artsy/Specs'], + }); + expect(res).toEqual({ + registryUrl: 'https://github.com/Artsy/Specs', + releases: [ + { + version: '1.2.3', + }, + ], + }); + }); + it('processes real data from Github with shard without specs', async () => { + httpMock + .scope(githubApiHost) + .get('/repos/Artsy/Specs/contents/Specs/a/c/b/foo') .reply(404) + .get('/repos/Artsy/Specs/contents/a/c/b/foo') + .reply(200, [{ name: '1.2.3' }]); + const res = await getPkgReleases({ + ...config, + registryUrls: ['https://github.com/Artsy/Specs'], + }); + expect(res).toEqual({ + registryUrl: 'https://github.com/Artsy/Specs', + releases: [ + { + version: '1.2.3', + }, + ], + }); + }); + it('processes real data from Github with specs without shard', async () => { + httpMock + .scope(githubApiHost) + .get('/repos/Artsy/Specs/contents/Specs/a/c/b/foo') + .reply(404) + .get('/repos/Artsy/Specs/contents/a/c/b/foo') + .reply(404) + .get('/repos/Artsy/Specs/contents/Specs/foo') + .reply(200, [{ name: '1.2.3' }]); + const res = await getPkgReleases({ + ...config, + registryUrls: ['https://github.com/Artsy/Specs'], + }); + expect(res).toEqual({ + registryUrl: 'https://github.com/Artsy/Specs', + releases: [ + { + version: '1.2.3', + }, + ], + }); + }); + it('processes real data from Github without specs without shard', async () => { + httpMock + .scope(githubApiHost) .get('/repos/Artsy/Specs/contents/Specs/a/c/b/foo') + .reply(404) + .get('/repos/Artsy/Specs/contents/a/c/b/foo') + .reply(404) + .get('/repos/Artsy/Specs/contents/Specs/foo') + .reply(404) + .get('/repos/Artsy/Specs/contents/foo') .reply(200, [{ name: '1.2.3' }]); const res = await getPkgReleases({ ...config, @@ -119,7 +221,90 @@ describe('datasource/pod/index', () => { }, ], }); - expect(httpMock.getTrace()).toMatchSnapshot(); + }); + it('processes real data from Github Enterprise with shard with specs', async () => { + httpMock + .scope(githubEntApiHost) + .get('/api/v3/repos/foo/bar/contents/Specs/a/c/b/foo') + .reply(200, [{ name: '1.2.3' }]); + const res = await getPkgReleases({ + ...config, + registryUrls: ['https://github.foo.com/foo/bar'], + }); + expect(res).toEqual({ + registryUrl: 'https://github.foo.com/foo/bar', + releases: [ + { + version: '1.2.3', + }, + ], + }); + }); + it('processes real data from Github Enterprise with shard without specs', async () => { + httpMock + .scope(githubEntApiHost) + .get('/api/v3/repos/foo/bar/contents/Specs/a/c/b/foo') + .reply(404) + .get('/api/v3/repos/foo/bar/contents/a/c/b/foo') + .reply(200, [{ name: '1.2.3' }]); + const res = await getPkgReleases({ + ...config, + registryUrls: ['https://github.foo.com/foo/bar'], + }); + expect(res).toEqual({ + registryUrl: 'https://github.foo.com/foo/bar', + releases: [ + { + version: '1.2.3', + }, + ], + }); + }); + it('processes real data from Github Enterprise with specs without shard', async () => { + httpMock + .scope(githubEntApiHost) + .get('/api/v3/repos/foo/bar/contents/Specs/a/c/b/foo') + .reply(404) + .get('/api/v3/repos/foo/bar/contents/a/c/b/foo') + .reply(404) + .get('/api/v3/repos/foo/bar/contents/Specs/foo') + .reply(200, [{ name: '1.2.3' }]); + const res = await getPkgReleases({ + ...config, + registryUrls: ['https://github.foo.com/foo/bar'], + }); + expect(res).toEqual({ + registryUrl: 'https://github.foo.com/foo/bar', + releases: [ + { + version: '1.2.3', + }, + ], + }); + }); + it('processes real data from Github Enterprise without specs without shard', async () => { + httpMock + .scope(githubEntApiHost) + .get('/api/v3/repos/foo/bar/contents/Specs/a/c/b/foo') + .reply(404) + .get('/api/v3/repos/foo/bar/contents/a/c/b/foo') + .reply(404) + .get('/api/v3/repos/foo/bar/contents/Specs/foo') + .reply(404) + .get('/api/v3/repos/foo/bar/contents/foo') + .reply(200, [{ name: '1.2.3' }]); + const res = await getPkgReleases({ + ...config, + registryUrls: ['https://github.foo.com/foo/bar'], + }); + expect(res).toEqual({ + registryUrl: 'https://github.foo.com/foo/bar', + releases: [ + { + version: '1.2.3', + }, + ], + }); }); }); }); diff --git a/lib/datasource/pod/index.ts b/lib/datasource/pod/index.ts index a9756383cd7124c86a3043ee798aab9b11863581..32a3d5ef46fd37a9be0c5c330bce53222811115e 100644 --- a/lib/datasource/pod/index.ts +++ b/lib/datasource/pod/index.ts @@ -7,6 +7,7 @@ import { Http } from '../../util/http'; import { GithubHttp } from '../../util/http/github'; import type { HttpError } from '../../util/http/types'; import { regEx } from '../../util/regex'; +import { massageGithubUrl } from '../metadata'; import type { GetReleasesConfig, ReleaseResult } from '../types'; export const id = 'pod'; @@ -21,6 +22,13 @@ const cacheMinutes = 30; const githubHttp = new GithubHttp(id); const http = new Http(id); +const enum URLFormatOptions { + WithShardWithSpec, + WithShardWithoutSpec, + WithSpecsWithoutShard, + WithoutSpecsWithoutShard, +} + function shardParts(lookupName: string): string[] { return crypto .createHash('md5') @@ -32,13 +40,27 @@ function shardParts(lookupName: string): string[] { function releasesGithubUrl( lookupName: string, - opts: { account: string; repo: string; useShard: boolean } + opts: { + hostURL: string; + account: string; + repo: string; + useShard: boolean; + useSpecs: boolean; + } ): string { - const { useShard, account, repo } = opts; - const prefix = 'https://api.github.com/repos'; + const { hostURL, account, repo, useShard, useSpecs } = opts; + const prefix = + hostURL && hostURL !== 'https://github.com' + ? `${hostURL}/api/v3/repos` + : 'https://api.github.com/repos'; const shard = shardParts(lookupName).join('/'); - const suffix = useShard ? `${shard}/${lookupName}` : lookupName; - return `${prefix}/${account}/${repo}/contents/Specs/${suffix}`; + // `Specs` in the pods repo URL is a new requirement for legacy support also allow pod repo URL without `Specs` + const lookupNamePath = useSpecs ? `Specs/${lookupName}` : lookupName; + const shardPath = useSpecs + ? `Specs/${shard}/${lookupName}` + : `${shard}/${lookupName}`; + const suffix = useShard ? shardPath : lookupNamePath; + return `${prefix}/${account}/${repo}/contents/${suffix}`; } function handleError(lookupName: string, err: HttpError): void { @@ -95,29 +117,53 @@ async function requestGithub<T = unknown>( } const githubRegex = regEx( - /^https:\/\/github\.com\/(?<account>[^/]+)\/(?<repo>[^/]+?)(\.git|\/.*)?$/ + /(?<hostURL>(^https:\/\/[a-zA-z0-9-.]+))\/(?<account>[^/]+)\/(?<repo>[^/]+?)(\.git|\/.*)?$/ ); async function getReleasesFromGithub( lookupName: string, - registryUrl: string, - useShard = false + opts: { hostURL: string; account: string; repo: string }, + useShard = true, + useSpecs = true, + urlFormatOptions = URLFormatOptions.WithShardWithSpec ): Promise<ReleaseResult | null> { - const match = githubRegex.exec(registryUrl); - const { account, repo } = match?.groups || {}; - const opts = { account, repo, useShard }; - const url = releasesGithubUrl(lookupName, opts); + const url = releasesGithubUrl(lookupName, { ...opts, useShard, useSpecs }); const resp = await requestGithub<{ name: string }[]>(url, lookupName); if (resp) { const releases = resp.map(({ name }) => ({ version: name })); return { releases }; } - if (!useShard) { - return getReleasesFromGithub(lookupName, registryUrl, true); + // iterating through enum to support different url formats + switch (urlFormatOptions) { + case URLFormatOptions.WithShardWithSpec: + return getReleasesFromGithub( + lookupName, + opts, + true, + false, + URLFormatOptions.WithShardWithoutSpec + ); + case URLFormatOptions.WithShardWithoutSpec: + return getReleasesFromGithub( + lookupName, + opts, + false, + true, + URLFormatOptions.WithSpecsWithoutShard + ); + case URLFormatOptions.WithSpecsWithoutShard: + return getReleasesFromGithub( + lookupName, + opts, + false, + false, + URLFormatOptions.WithoutSpecsWithoutShard + ); + case URLFormatOptions.WithoutSpecsWithoutShard: + default: + return null; } - - return null; } function releasesCDNUrl(lookupName: string, registryUrl: string): string { @@ -175,15 +221,18 @@ export async function getReleases({ } let baseUrl = registryUrl.replace(regEx(/\/+$/), ''); - + baseUrl = massageGithubUrl(baseUrl); // In order to not abuse github API limits, query CDN instead if (isDefaultRepo(baseUrl)) { [baseUrl] = defaultRegistryUrls; } let result: ReleaseResult | null = null; - if (githubRegex.exec(baseUrl)) { - result = await getReleasesFromGithub(podName, baseUrl); + const match = githubRegex.exec(baseUrl); + if (match) { + const { hostURL, account, repo } = match?.groups || {}; + const opts = { hostURL, account, repo }; + result = await getReleasesFromGithub(podName, opts); } else { result = await getReleasesFromCDN(podName, baseUrl); }