From 84a52aff0d275bafae09e7b2c502bbd4a7f523e7 Mon Sep 17 00:00:00 2001 From: Caleb Cartwright Date: Tue, 14 Sep 2021 20:36:37 -0500 Subject: [PATCH 1/4] feat: add GitLabRelease badge --- services/gitlab/gitlab-base.js | 46 ++++++++ services/gitlab/gitlab-release.service.js | 136 ++++++++++++++++++++++ services/gitlab/gitlab-release.tester.js | 33 ++++++ 3 files changed, 215 insertions(+) create mode 100644 services/gitlab/gitlab-release.service.js create mode 100644 services/gitlab/gitlab-release.tester.js diff --git a/services/gitlab/gitlab-base.js b/services/gitlab/gitlab-base.js index 4ddf232a2ae75..faa996fd15b6c 100644 --- a/services/gitlab/gitlab-base.js +++ b/services/gitlab/gitlab-base.js @@ -16,4 +16,50 @@ export default class GitLabBase extends BaseJsonService { }) ) } + + async fetchPage({ page, requestParams, schema }) { + const { res, buffer } = await this._request({ + ...requestParams, + ...{ options: { qs: { page } } }, + }) + + const json = this._parseJson(buffer) + const data = this.constructor._validate(json, schema) + return { res, data } + } + + async fetchPaginatedArrayData({ + url, + options, + schema, + errorMessages, + firstPageOnly = false, + }) { + const requestParams = this.authHelper.withBasicAuth({ + url, + options: { + headers: { Accept: 'application/json' }, + qs: { per_page: 100 }, + ...options, + }, + errorMessages, + }) + + const { + res: { headers }, + data, + } = await this.fetchPage({ page: 1, requestParams, schema }) + const numberOfPages = headers['x-total-pages'] + + if (numberOfPages === 1 || firstPageOnly) { + return data + } + + const pageData = await Promise.all( + [...Array(numberOfPages - 1).keys()].map((_, i) => + this.fetchPage({ page: ++i + 1, requestParams, schema }) + ) + ) + return [...data].concat(...pageData) + } } diff --git a/services/gitlab/gitlab-release.service.js b/services/gitlab/gitlab-release.service.js new file mode 100644 index 0000000000000..ab7411f3f1cd0 --- /dev/null +++ b/services/gitlab/gitlab-release.service.js @@ -0,0 +1,136 @@ +import Joi from 'joi' +import { optionalUrl } from '../validators.js' +import { latest, renderVersionBadge } from '../version.js' +import { NotFound } from '../index.js' +import GitLabBase from './gitlab-base.js' + +const schema = Joi.array().items( + Joi.object({ + name: Joi.string().required(), + tag_name: Joi.string().required(), + }) +) + +const queryParamSchema = Joi.object({ + gitlab_url: optionalUrl, + include_prereleases: Joi.equal(''), + sort: Joi.string().valid('date', 'semver').default('date'), + display_name: Joi.string().valid('tag', 'release').default('tag'), +}).required() + +const namedParams = { + user: 'shields-ops-group', + repo: 'repo-test', +} + +export default class GitLabRelease extends GitLabBase { + static category = 'version' + + static route = { + base: 'gitlab/v/release', + pattern: ':user/:repo', + queryParamSchema, + } + + static examples = [ + { + title: 'GitLab Release (latest by date)', + namedParams, + queryParams: { sort: 'date' }, + staticPreview: renderVersionBadge({ version: 'v2.0.0' }), + }, + { + title: 'GitLab Release (latest by SemVer)', + namedParams, + queryParams: { sort: 'semver' }, + staticPreview: renderVersionBadge({ version: 'v4.0.0' }), + }, + { + title: 'GitLab Release (latest by SemVer pre-release)', + namedParams, + queryParams: { + sort: 'semver', + include_prereleases: null, + }, + staticPreview: renderVersionBadge({ version: 'v5.0.0-beta.1' }), + }, + { + title: 'GitLab Release (custom instance)', + namedParams: { + user: 'GNOME', + repo: 'librsvg', + }, + queryParams: { + sort: 'semver', + include_prereleases: null, + gitlab_url: 'https://gitlab.gnome.org', + }, + staticPreview: renderVersionBadge({ version: 'v2.51.4' }), + }, + { + title: 'GitLab Release (by release name)', + namedParams: { + user: 'gitlab-org', + repo: 'gitlab', + }, + queryParams: { + sort: 'semver', + include_prereleases: null, + gitlab_url: 'https://gitlab.com', + display_name: 'release', + }, + staticPreview: renderVersionBadge({ version: 'GitLab 14.2' }), + }, + ] + + static defaultBadgeData = { label: 'release' } + + async fetch({ user, repo, baseUrl, isSemver }) { + // https://docs.gitlab.com/ee/api/releases/ + return this.fetchPaginatedArrayData({ + schema, + url: `${baseUrl}/api/v4/projects/${user}%2F${repo}/releases`, + errorMessages: { + 404: 'project not found', + }, + firstPageOnly: !isSemver, + }) + } + + static transform({ releases, isSemver, includePrereleases, displayName }) { + if (releases.length === 0) { + throw new NotFound({ prettyMessage: 'no releases found' }) + } + + const displayKey = displayName === 'tag' ? 'tag_name' : 'name' + + if (!isSemver) { + return releases[0][displayKey] + } + + return latest( + releases.map(t => t[displayKey]), + { pre: includePrereleases } + ) + } + + async handle( + { user, repo }, + { + gitlab_url: baseUrl = 'https://gitlab.com', + include_prereleases: pre, + sort, + display_name: displayName, + } + ) { + const isSemver = sort === 'semver' + const releases = await this.fetch({ user, repo, baseUrl, isSemver }) + const version = this.constructor.transform({ + releases, + isSemver, + includePrereleases: pre !== undefined, + displayName, + }) + return renderVersionBadge({ version }) + } +} diff --git a/services/gitlab/gitlab-release.tester.js b/services/gitlab/gitlab-release.tester.js new file mode 100644 index 0000000000000..6fe9d5c05c7c8 --- /dev/null +++ b/services/gitlab/gitlab-release.tester.js @@ -0,0 +1,33 @@ +import { isSemver, withRegex } from '../test-validators.js' +import { createServiceTester } from '../tester.js' +export const t = await createServiceTester() + +const isGitLabDisplayVersion = withRegex(/^GitLab [1-9][0-9]*.[0-9]*/) + +t.create('Release (latest by date)') + .get('/shields-ops-group/tag-test.json') + .expectBadge({ label: 'release', message: 'v2.0.0', color: 'blue' }) + +t.create('Release (latest by semver)') + .get('/shields-ops-group/tag-test.json?sort=semver') + .expectBadge({ label: 'release', message: 'v4.0.0', color: 'blue' }) + +t.create('Release (latest by semver pre-release)') + .get('/shields-ops-group/tag-test.json?sort=semver&include_prereleases') + .expectBadge({ label: 'release', message: 'v5.0.0-beta.1', color: 'orange' }) + +t.create('Release (release display name)') + .get('/gitlab-org/gitlab.json?display_name=release') + .expectBadge({ label: 'release', message: isGitLabDisplayVersion }) + +t.create('Release (custom instance') + .get('/GNOME/librsvg.json?gitlab_url=https://gitlab.gnome.org') + .expectBadge({ label: 'release', message: isSemver, color: 'blue' }) + +t.create('Release (project not found)') + .get('/fdroid/nonexistant.json') + .expectBadge({ label: 'release', message: 'project not found' }) + +t.create('Release (no tags)') + .get('/fdroid/fdroiddata.json') + .expectBadge({ label: 'release', message: 'no releases found' }) From bfe7c1c198eefe12ffa51018f7d07dba6ae8ed94 Mon Sep 17 00:00:00 2001 From: Caleb Cartwright Date: Sat, 16 Oct 2021 10:13:09 -0500 Subject: [PATCH 2/4] use single project route param --- services/gitlab/gitlab-release.service.js | 37 ++++++++++++++--------- services/gitlab/gitlab-release.tester.js | 4 +++ 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/services/gitlab/gitlab-release.service.js b/services/gitlab/gitlab-release.service.js index ab7411f3f1cd0..7d15b8f0f06bd 100644 --- a/services/gitlab/gitlab-release.service.js +++ b/services/gitlab/gitlab-release.service.js @@ -18,9 +18,16 @@ const queryParamSchema = Joi.object({ display_name: Joi.string().valid('tag', 'release').default('tag'), }).required() -const namedParams = { - user: 'shields-ops-group', - repo: 'repo-test', +const documentation = ` +

+ You may use your GitLab Project Id (e.g. 25813592) or your Project Path (e.g. megabyte-labs/dockerfile/ci-pipeline/ansible-lint) +

+` +const commonProps = { + namedParams: { + project: 'shields-ops-group/tag-test', + }, + documentation, } export default class GitLabRelease extends GitLabBase { @@ -28,26 +35,26 @@ export default class GitLabRelease extends GitLabBase { static route = { base: 'gitlab/v/release', - pattern: ':user/:repo', + pattern: ':project+', queryParamSchema, } static examples = [ { title: 'GitLab Release (latest by date)', - namedParams, + ...commonProps, queryParams: { sort: 'date' }, staticPreview: renderVersionBadge({ version: 'v2.0.0' }), }, { title: 'GitLab Release (latest by SemVer)', - namedParams, + ...commonProps, queryParams: { sort: 'semver' }, staticPreview: renderVersionBadge({ version: 'v4.0.0' }), }, { title: 'GitLab Release (latest by SemVer pre-release)', - namedParams, + ...commonProps, queryParams: { sort: 'semver', include_prereleases: null, @@ -57,9 +64,9 @@ export default class GitLabRelease extends GitLabBase { { title: 'GitLab Release (custom instance)', namedParams: { - user: 'GNOME', - repo: 'librsvg', + project: 'GNOME/librsvg', }, + documentation, queryParams: { sort: 'semver', include_prereleases: null, @@ -70,9 +77,9 @@ export default class GitLabRelease extends GitLabBase { { title: 'GitLab Release (by release name)', namedParams: { - user: 'gitlab-org', - repo: 'gitlab', + project: 'gitlab-org/gitlab', }, + documentation, queryParams: { sort: 'semver', include_prereleases: null, @@ -85,11 +92,11 @@ export default class GitLabRelease extends GitLabBase { static defaultBadgeData = { label: 'release' } - async fetch({ user, repo, baseUrl, isSemver }) { + async fetch({ project, baseUrl, isSemver }) { // https://docs.gitlab.com/ee/api/releases/ return this.fetchPaginatedArrayData({ schema, - url: `${baseUrl}/api/v4/projects/${user}%2F${repo}/releases`, + url: `${baseUrl}/api/v4/projects/${encodeURIComponent(project)}/releases`, errorMessages: { 404: 'project not found', }, @@ -115,7 +122,7 @@ export default class GitLabRelease extends GitLabBase { } async handle( - { user, repo }, + { project }, { gitlab_url: baseUrl = 'https://gitlab.com', include_prereleases: pre, @@ -124,7 +131,7 @@ export default class GitLabRelease extends GitLabBase { } ) { const isSemver = sort === 'semver' - const releases = await this.fetch({ user, repo, baseUrl, isSemver }) + const releases = await this.fetch({ project, baseUrl, isSemver }) const version = this.constructor.transform({ releases, isSemver, diff --git a/services/gitlab/gitlab-release.tester.js b/services/gitlab/gitlab-release.tester.js index 6fe9d5c05c7c8..653f3eb5a5a05 100644 --- a/services/gitlab/gitlab-release.tester.js +++ b/services/gitlab/gitlab-release.tester.js @@ -8,6 +8,10 @@ t.create('Release (latest by date)') .get('/shields-ops-group/tag-test.json') .expectBadge({ label: 'release', message: 'v2.0.0', color: 'blue' }) +t.create('Release (project id latest by date)') + .get('/29538796.json') + .expectBadge({ label: 'release', message: 'v2.0.0', color: 'blue' }) + t.create('Release (latest by semver)') .get('/shields-ops-group/tag-test.json?sort=semver') .expectBadge({ label: 'release', message: 'v4.0.0', color: 'blue' }) From cc62b850d4bacb783ac573e97e241fd16c99af2a Mon Sep 17 00:00:00 2001 From: Caleb Cartwright Date: Sat, 16 Oct 2021 10:21:35 -0500 Subject: [PATCH 3/4] add query param for date ordering --- services/gitlab/gitlab-release.service.js | 15 ++++++++++++--- services/gitlab/gitlab-release.tester.js | 8 ++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/services/gitlab/gitlab-release.service.js b/services/gitlab/gitlab-release.service.js index 7d15b8f0f06bd..5c63d211c1405 100644 --- a/services/gitlab/gitlab-release.service.js +++ b/services/gitlab/gitlab-release.service.js @@ -16,6 +16,9 @@ const queryParamSchema = Joi.object({ include_prereleases: Joi.equal(''), sort: Joi.string().valid('date', 'semver').default('date'), display_name: Joi.string().valid('tag', 'release').default('tag'), + date_order_by: Joi.string() + .valid('created_at', 'released_at') + .default('created_at'), }).required() const documentation = ` @@ -43,7 +46,7 @@ export default class GitLabRelease extends GitLabBase { { title: 'GitLab Release (latest by date)', ...commonProps, - queryParams: { sort: 'date' }, + queryParams: { sort: 'date', date_order_by: 'created_at' }, staticPreview: renderVersionBadge({ version: 'v2.0.0' }), }, { @@ -71,6 +74,7 @@ export default class GitLabRelease extends GitLabBase { sort: 'semver', include_prereleases: null, gitlab_url: 'https://gitlab.gnome.org', + date_order_by: 'created_at', }, staticPreview: renderVersionBadge({ version: 'v2.51.4' }), }, @@ -85,6 +89,7 @@ export default class GitLabRelease extends GitLabBase { include_prereleases: null, gitlab_url: 'https://gitlab.com', display_name: 'release', + date_order_by: 'created_at', }, staticPreview: renderVersionBadge({ version: 'GitLab 14.2' }), }, @@ -92,7 +97,7 @@ export default class GitLabRelease extends GitLabBase { static defaultBadgeData = { label: 'release' } - async fetch({ project, baseUrl, isSemver }) { + async fetch({ project, baseUrl, isSemver, orderBy }) { // https://docs.gitlab.com/ee/api/releases/ return this.fetchPaginatedArrayData({ schema, @@ -100,6 +105,9 @@ export default class GitLabRelease extends GitLabBase { errorMessages: { 404: 'project not found', }, + options: { + qs: { order_by: orderBy }, + }, firstPageOnly: !isSemver, }) } @@ -128,10 +136,11 @@ export default class GitLabRelease extends GitLabBase { include_prereleases: pre, sort, display_name: displayName, + date_order_by: orderBy, } ) { const isSemver = sort === 'semver' - const releases = await this.fetch({ project, baseUrl, isSemver }) + const releases = await this.fetch({ project, baseUrl, isSemver, orderBy }) const version = this.constructor.transform({ releases, isSemver, diff --git a/services/gitlab/gitlab-release.tester.js b/services/gitlab/gitlab-release.tester.js index 653f3eb5a5a05..243ed9cf43ff7 100644 --- a/services/gitlab/gitlab-release.tester.js +++ b/services/gitlab/gitlab-release.tester.js @@ -8,6 +8,14 @@ t.create('Release (latest by date)') .get('/shields-ops-group/tag-test.json') .expectBadge({ label: 'release', message: 'v2.0.0', color: 'blue' }) +t.create('Release (latest by date, order by created_at)') + .get('/shields-ops-group/tag-test.json?date_order_by=created_at') + .expectBadge({ label: 'release', message: 'v2.0.0', color: 'blue' }) + +t.create('Release (latest by date, order by released_at)') + .get('/shields-ops-group/tag-test.json?date_order_by=released_at') + .expectBadge({ label: 'release', message: 'v2.0.0', color: 'blue' }) + t.create('Release (project id latest by date)') .get('/29538796.json') .expectBadge({ label: 'release', message: 'v2.0.0', color: 'blue' }) From c3be5655ab68a5d7a2362e0ed887035e917b7e34 Mon Sep 17 00:00:00 2001 From: Caleb Cartwright Date: Sat, 16 Oct 2021 14:09:59 -0500 Subject: [PATCH 4/4] add test for nested subgroup --- services/gitlab/gitlab-release.tester.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/gitlab/gitlab-release.tester.js b/services/gitlab/gitlab-release.tester.js index 243ed9cf43ff7..b4b859d8f0752 100644 --- a/services/gitlab/gitlab-release.tester.js +++ b/services/gitlab/gitlab-release.tester.js @@ -8,6 +8,10 @@ t.create('Release (latest by date)') .get('/shields-ops-group/tag-test.json') .expectBadge({ label: 'release', message: 'v2.0.0', color: 'blue' }) +t.create('Release (nested groups latest by date)') + .get('/gitlab-org/frontend/eslint-plugin.json') + .expectBadge({ label: 'release', message: isSemver, color: 'blue' }) + t.create('Release (latest by date, order by created_at)') .get('/shields-ops-group/tag-test.json?date_order_by=created_at') .expectBadge({ label: 'release', message: 'v2.0.0', color: 'blue' })