From 443b22b0ae74b43111e3e79e900cf369756a1344 Mon Sep 17 00:00:00 2001 From: Sebastian Poxhofer <secustor@users.noreply.github.com> Date: Wed, 1 Sep 2021 11:36:38 +0200 Subject: [PATCH] feat(gitlab-release): implement datasource (#11226) Co-authored-by: Nejc Habjan <hab.nejc@gmail.com> Co-authored-by: Rhys Arkins <rhys@arkins.net> Co-authored-by: Michael Kriese <michael.kriese@visualon.de> --- lib/datasource/api.ts | 2 + .../__snapshots__/index.spec.ts.snap | 43 ++++++++++++++ lib/datasource/gitlab-releases/index.spec.ts | 58 +++++++++++++++++++ lib/datasource/gitlab-releases/index.ts | 54 +++++++++++++++++ lib/datasource/gitlab-releases/types.ts | 5 ++ lib/datasource/gitlab-tags/index.ts | 4 +- lib/types/platform/gitlab/index.spec.ts | 25 ++++++++ lib/types/platform/gitlab/index.ts | 7 +++ .../http/__snapshots__/gitlab.spec.ts.snap | 15 +++++ lib/util/http/auth.ts | 8 +-- lib/util/http/gitlab.spec.ts | 33 +++++++++-- lib/util/http/gitlab.ts | 7 ++- 12 files changed, 247 insertions(+), 14 deletions(-) create mode 100644 lib/datasource/gitlab-releases/__snapshots__/index.spec.ts.snap create mode 100644 lib/datasource/gitlab-releases/index.spec.ts create mode 100644 lib/datasource/gitlab-releases/index.ts create mode 100644 lib/datasource/gitlab-releases/types.ts create mode 100644 lib/types/platform/gitlab/index.spec.ts diff --git a/lib/datasource/api.ts b/lib/datasource/api.ts index b6c3d51137..356af4bc8a 100644 --- a/lib/datasource/api.ts +++ b/lib/datasource/api.ts @@ -11,6 +11,7 @@ import * as gitRefs from './git-refs'; import * as gitTags from './git-tags'; import * as githubReleases from './github-releases'; import * as githubTags from './github-tags'; +import { GitlabReleasesDatasource } from './gitlab-releases'; import * as gitlabTags from './gitlab-tags'; import * as go from './go'; import { GradleVersionDatasource } from './gradle-version'; @@ -50,6 +51,7 @@ api.set('git-tags', gitTags); api.set('github-releases', githubReleases); api.set('github-tags', githubTags); api.set('gitlab-tags', gitlabTags); +api.set(GitlabReleasesDatasource.id, new GitlabReleasesDatasource()); api.set('go', go); api.set('gradle-version', new GradleVersionDatasource()); api.set('helm', new HelmDatasource()); diff --git a/lib/datasource/gitlab-releases/__snapshots__/index.spec.ts.snap b/lib/datasource/gitlab-releases/__snapshots__/index.spec.ts.snap new file mode 100644 index 0000000000..f39b80f4c8 --- /dev/null +++ b/lib/datasource/gitlab-releases/__snapshots__/index.spec.ts.snap @@ -0,0 +1,43 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`datasource/gitlab-releases/index getReleases returns releases from custom registry 1`] = ` +Object { + "registryUrl": "https://gitlab.company.com", + "releases": Array [ + Object { + "gitRef": "v1.0.0", + "registryUrl": "https://gitlab.company.com", + "releaseTimestamp": "2021-01-01T00:00:00.000Z", + "version": "v1.0.0", + }, + Object { + "gitRef": "v1.1.0", + "registryUrl": "https://gitlab.company.com", + "releaseTimestamp": "2021-03-01T00:00:00.000Z", + "version": "v1.1.0", + }, + ], + "sourceUrl": "https://gitlab.company.com/some/dep2", +} +`; + +exports[`datasource/gitlab-releases/index getReleases returns releases from default registry 1`] = ` +Object { + "registryUrl": "https://gitlab.com", + "releases": Array [ + Object { + "gitRef": "v1.0.0", + "registryUrl": "https://gitlab.com", + "releaseTimestamp": "2021-01-01T00:00:00.000Z", + "version": "v1.0.0", + }, + Object { + "gitRef": "v1.1.0", + "registryUrl": "https://gitlab.com", + "releaseTimestamp": "2021-03-01T00:00:00.000Z", + "version": "v1.1.0", + }, + ], + "sourceUrl": "https://gitlab.com/some/dep2", +} +`; diff --git a/lib/datasource/gitlab-releases/index.spec.ts b/lib/datasource/gitlab-releases/index.spec.ts new file mode 100644 index 0000000000..e4a438d007 --- /dev/null +++ b/lib/datasource/gitlab-releases/index.spec.ts @@ -0,0 +1,58 @@ +import { getPkgReleases } from '..'; +import * as httpMock from '../../../test/http-mock'; +import { GitlabReleasesDatasource } from '.'; + +describe('datasource/gitlab-releases/index', () => { + describe('getReleases', () => { + const body = [ + { + tag_name: 'v1.0.0', + released_at: '2021-01-01T00:00:00.000Z', + }, + { + tag_name: 'v1.1.0', + released_at: '2021-03-01T00:00:00.000Z', + }, + ]; + + it('returns releases from custom registry', async () => { + httpMock + .scope('https://gitlab.company.com') + .get('/api/v4/projects/some%2Fdep2/releases') + .reply(200, body); + const res = await getPkgReleases({ + datasource: GitlabReleasesDatasource.id, + registryUrls: ['https://gitlab.company.com'], + depName: 'some/dep2', + }); + expect(res).toMatchSnapshot(); + expect(res.releases).toHaveLength(2); + }); + + it('returns releases from default registry', async () => { + httpMock + .scope('https://gitlab.com') + .get('/api/v4/projects/some%2Fdep2/releases') + .reply(200, body); + const res = await getPkgReleases({ + datasource: GitlabReleasesDatasource.id, + depName: 'some/dep2', + }); + expect(res).toMatchSnapshot(); + expect(res.releases).toHaveLength(2); + }); + + it('return null if not found', async () => { + httpMock + .scope('https://gitlab.com') + .get('/api/v4/projects/some%2Fdep2/releases') + .reply(404); + expect( + await getPkgReleases({ + datasource: GitlabReleasesDatasource.id, + depName: 'some/dep2', + }) + ).toBeNull(); + }); + }); +}); diff --git a/lib/datasource/gitlab-releases/index.ts b/lib/datasource/gitlab-releases/index.ts new file mode 100644 index 0000000000..6a4cec10bf --- /dev/null +++ b/lib/datasource/gitlab-releases/index.ts @@ -0,0 +1,54 @@ +import { cache } from '../../util/cache/package/decorator'; +import { GitlabHttp } from '../../util/http/gitlab'; +import { Datasource } from '../datasource'; +import type { GetReleasesConfig, Release, ReleaseResult } from '../types'; +import type { GitlabRelease } from './types'; + +export class GitlabReleasesDatasource extends Datasource { + static readonly id = 'gitlab-releases'; + + override readonly defaultRegistryUrls = ['https://gitlab.com']; + + static readonly registryStrategy = 'first'; + + constructor() { + super(GitlabReleasesDatasource.id); + this.http = new GitlabHttp(GitlabReleasesDatasource.id); + } + + @cache({ + namespace: `datasource-${GitlabReleasesDatasource.id}`, + key: ({ registryUrl, lookupName }: GetReleasesConfig) => + `${registryUrl}/${lookupName}`, + }) + async getReleases({ + registryUrl, + lookupName, + }: GetReleasesConfig): Promise<ReleaseResult | null> { + const urlEncodedRepo = encodeURIComponent(lookupName); + const apiUrl = `${registryUrl}/api/v4/projects/${urlEncodedRepo}/releases`; + + try { + const gitlabReleasesResponse = ( + await this.http.getJson<GitlabRelease[]>(apiUrl) + ).body; + + return { + sourceUrl: `${registryUrl}/${lookupName}`, + releases: gitlabReleasesResponse.map(({ tag_name, released_at }) => { + const release: Release = { + registryUrl, + gitRef: tag_name, + version: tag_name, + releaseTimestamp: released_at, + }; + return release; + }), + }; + } catch (e) { + this.handleGenericErrors(e); + } + /* istanbul ignore next */ + return null; + } +} diff --git a/lib/datasource/gitlab-releases/types.ts b/lib/datasource/gitlab-releases/types.ts new file mode 100644 index 0000000000..d6b4cab4fb --- /dev/null +++ b/lib/datasource/gitlab-releases/types.ts @@ -0,0 +1,5 @@ +export interface GitlabRelease { + name: string; + tag_name: string; + released_at: string; +} diff --git a/lib/datasource/gitlab-tags/index.ts b/lib/datasource/gitlab-tags/index.ts index bf3355c07b..fde3a5f869 100644 --- a/lib/datasource/gitlab-tags/index.ts +++ b/lib/datasource/gitlab-tags/index.ts @@ -4,9 +4,9 @@ import { joinUrlParts } from '../../util/url'; import type { GetReleasesConfig, ReleaseResult } from '../types'; import type { GitlabTag } from './types'; -const gitlabApi = new GitlabHttp(); - export const id = 'gitlab-tags'; +const gitlabApi = new GitlabHttp(id); + export const customRegistrySupport = true; export const defaultRegistryUrls = ['https://gitlab.com']; export const registryStrategy = 'first'; diff --git a/lib/types/platform/gitlab/index.spec.ts b/lib/types/platform/gitlab/index.spec.ts new file mode 100644 index 0000000000..945f2e4476 --- /dev/null +++ b/lib/types/platform/gitlab/index.spec.ts @@ -0,0 +1,25 @@ +import { + PLATFORM_TYPE_GITHUB, + PLATFORM_TYPE_GITLAB, +} from '../../../constants/platforms'; +import { GitlabReleasesDatasource } from '../../../datasource/gitlab-releases'; +import { id as GL_TAGS_DS } from '../../../datasource/gitlab-tags'; +import { GITLAB_API_USING_HOST_TYPES } from './index'; + +describe('types/platform/gitlab/index', () => { + it('should be part of the GITLAB_API_USING_HOST_TYPES', () => { + expect(GITLAB_API_USING_HOST_TYPES.includes(GL_TAGS_DS)).toBeTrue(); + expect( + GITLAB_API_USING_HOST_TYPES.includes(GitlabReleasesDatasource.id) + ).toBeTrue(); + expect( + GITLAB_API_USING_HOST_TYPES.includes(PLATFORM_TYPE_GITLAB) + ).toBeTrue(); + }); + + it('should be not part of the GITLAB_API_USING_HOST_TYPES ', () => { + expect( + GITLAB_API_USING_HOST_TYPES.includes(PLATFORM_TYPE_GITHUB) + ).toBeFalse(); + }); +}); diff --git a/lib/types/platform/gitlab/index.ts b/lib/types/platform/gitlab/index.ts index a67df13e1a..999f9149b1 100644 --- a/lib/types/platform/gitlab/index.ts +++ b/lib/types/platform/gitlab/index.ts @@ -1,3 +1,4 @@ +import { PLATFORM_TYPE_GITLAB } from '../../../constants/platforms'; import { GitTreeNode } from '../../git'; export type GitLabBranch = { @@ -12,3 +13,9 @@ export type GitlabTreeNode = { id: string; name: string; } & GitTreeNode; + +export const GITLAB_API_USING_HOST_TYPES = [ + PLATFORM_TYPE_GITLAB, + 'gitlab-releases', + 'gitlab-tags', +]; diff --git a/lib/util/http/__snapshots__/gitlab.spec.ts.snap b/lib/util/http/__snapshots__/gitlab.spec.ts.snap index 8b1f605d78..56e7c378c3 100644 --- a/lib/util/http/__snapshots__/gitlab.spec.ts.snap +++ b/lib/util/http/__snapshots__/gitlab.spec.ts.snap @@ -107,3 +107,18 @@ Array [ }, ] `; + +exports[`util/http/gitlab supports different datasources 1`] = ` +Array [ + Object { + "headers": Object { + "accept-encoding": "gzip, deflate, br", + "authorization": "Bearer def", + "host": "gitlab.com", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://gitlab.com/api/v4/some-url", + }, +] +`; diff --git a/lib/util/http/auth.ts b/lib/util/http/auth.ts index 0c14ca7092..b3a5383dfc 100644 --- a/lib/util/http/auth.ts +++ b/lib/util/http/auth.ts @@ -1,10 +1,8 @@ import is from '@sindresorhus/is'; import { NormalizedOptions } from 'got'; -import { - PLATFORM_TYPE_GITEA, - PLATFORM_TYPE_GITLAB, -} from '../../constants/platforms'; +import { PLATFORM_TYPE_GITEA } from '../../constants/platforms'; import { GITHUB_API_USING_HOST_TYPES } from '../../types'; +import { GITLAB_API_USING_HOST_TYPES } from '../../types/platform/gitlab'; import { GotOptions } from './types'; export function applyAuthorization(inOptions: GotOptions): GotOptions { @@ -29,7 +27,7 @@ export function applyAuthorization(inOptions: GotOptions): GotOptions { ); } } - } else if (options.hostType === PLATFORM_TYPE_GITLAB) { + } else if (GITLAB_API_USING_HOST_TYPES.includes(options.hostType)) { // GitLab versions earlier than 12.2 only support authentication with // a personal access token, which is 20 characters long. if (options.token.length === 20) { diff --git a/lib/util/http/gitlab.spec.ts b/lib/util/http/gitlab.spec.ts index acd7c6d989..13f6a3962f 100644 --- a/lib/util/http/gitlab.spec.ts +++ b/lib/util/http/gitlab.spec.ts @@ -1,14 +1,10 @@ import * as httpMock from '../../../test/http-mock'; import { EXTERNAL_HOST_ERROR } from '../../constants/error-messages'; import { PLATFORM_TYPE_GITLAB } from '../../constants/platforms'; +import { GitlabReleasesDatasource } from '../../datasource/gitlab-releases'; import * as hostRules from '../host-rules'; import { GitlabHttp, setBaseUrl } from './gitlab'; -hostRules.add({ - hostType: PLATFORM_TYPE_GITLAB, - token: 'abc123', -}); - const gitlabApiHost = 'https://gitlab.com'; const selfHostedUrl = 'http://mycompany.com/gitlab'; @@ -19,10 +15,17 @@ describe('util/http/gitlab', () => { gitlabApi = new GitlabHttp(); setBaseUrl(`${gitlabApiHost}/api/v4/`); delete process.env.GITLAB_IGNORE_REPO_URL; + + hostRules.add({ + hostType: PLATFORM_TYPE_GITLAB, + token: 'abc123', + }); }); afterEach(() => { jest.resetAllMocks(); + + hostRules.clear(); }); it('paginates', async () => { @@ -68,6 +71,26 @@ describe('util/http/gitlab', () => { expect(trace).toHaveLength(3); expect(trace).toMatchSnapshot(); }); + + it('supports different datasources', async () => { + const gitlabApiDatasource = new GitlabHttp(GitlabReleasesDatasource.id); + hostRules.add({ hostType: PLATFORM_TYPE_GITLAB, token: 'abc' }); + hostRules.add({ + hostType: GitlabReleasesDatasource.id, + token: 'def', + }); + httpMock + .scope(gitlabApiHost, { reqheaders: { authorization: 'Bearer def' } }) + .get('/api/v4/some-url') + .reply(200); + const response = await gitlabApiDatasource.get('/some-url'); + expect(response).not.toBeNull(); + + const trace = httpMock.getTrace(); + expect(trace).toHaveLength(1); + expect(trace).toMatchSnapshot(); + }); + it('attempts to paginate', async () => { httpMock.scope(gitlabApiHost).get('/api/v4/some-url').reply(200, ['a'], { link: '<https://gitlab.com/api/v4/some-url&page=3>; rel="last"', diff --git a/lib/util/http/gitlab.ts b/lib/util/http/gitlab.ts index a9ca70f00c..18eacdd224 100644 --- a/lib/util/http/gitlab.ts +++ b/lib/util/http/gitlab.ts @@ -20,8 +20,11 @@ export interface GitlabHttpOptions extends InternalHttpOptions { } export class GitlabHttp extends Http<GitlabHttpOptions, GitlabHttpOptions> { - constructor(options?: GitlabHttpOptions) { - super(PLATFORM_TYPE_GITLAB, options); + constructor( + type: string = PLATFORM_TYPE_GITLAB, + options?: GitlabHttpOptions + ) { + super(type, options); } protected override async request<T>( -- GitLab