Skip to content

Commit

Permalink
Add API-based support for [GitLab] badges, add new GitLab Tag badge (#…
Browse files Browse the repository at this point in the history
…6988)

* Added GitLab Tag service

* Added prettyMessage for when repo has no tags

* Added pretty message for repo not found

* core: esm-ify gitlab tag service

* feat: support gitlab auth

* feat: support custom gitlab url on tag badges

* tests: add auth test for gitlab

* docs: fix gitlab config key references

* feat: support gitlab tag sorting options

* docs: add custom gitlab instance example for tags badge

* use v in gitlab route

* fix: gitlab tag examples

Co-authored-by: Ideotec <[email protected]>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Sep 14, 2021
1 parent 6bb62e4 commit 961e13b
Show file tree
Hide file tree
Showing 10 changed files with 241 additions and 0 deletions.
2 changes: 2 additions & 0 deletions config/custom-environment-variables.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ public:
debug:
enabled: 'GITHUB_DEBUG_ENABLED'
intervalSeconds: 'GITHUB_DEBUG_INTERVAL_SECONDS'
gitlab:
authorizedOrigins: 'GITLAB_ORIGINS'
jenkins:
authorizedOrigins: 'JENKINS_ORIGINS'
jira:
Expand Down
1 change: 1 addition & 0 deletions config/local-shields-io-production.template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ private:
discord_bot_token: ...
gh_client_id: ...
gh_client_secret: ...
gitlab_token: ...
redis_url: ...
sentry_dsn: ...
shields_secret: ...
Expand Down
1 change: 1 addition & 0 deletions config/local.template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ private:
# you can also set these values through environment variables, which may be
# preferable for self hosting.
gh_token: '...'
gitlab_token: '...'
twitch_client_id: '...'
twitch_client_secret: '...'
weblate_api_key: '...'
Expand Down
2 changes: 2 additions & 0 deletions core/server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ const publicConfigSchema = Joi.object({
intervalSeconds: Joi.number().integer().min(1).required(),
},
},
gitlab: defaultService,
jira: defaultService,
jenkins: Joi.object({
authorizedOrigins: origins,
Expand Down Expand Up @@ -161,6 +162,7 @@ const privateConfigSchema = Joi.object({
gh_client_id: Joi.string(),
gh_client_secret: Joi.string(),
gh_token: Joi.string(),
gitlab_token: Joi.string(),
jenkins_user: Joi.string(),
jenkins_pass: Joi.string(),
jira_user: Joi.string(),
Expand Down
2 changes: 2 additions & 0 deletions doc/production-hosting.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ Production hosting is managed by the Shields ops team:
| Twitch | OAuth app | @PyvesB |
| Discord | OAuth app | @PyvesB |
| YouTube | Account owner | @PyvesB |
| GitLab | Account owner | @calebcartwright |
| GitLab | Account access | @calebcartwright, @chris48s, @paulmelnikow, @PyvesB |
| OpenStreetMap (for Wheelmap) | Account owner | @paulmelnikow |
| DNS | Account owner | @olivierlacan |
| DNS | Read-only account access | @espadrine, @paulmelnikow, @chris48s |
Expand Down
9 changes: 9 additions & 0 deletions doc/server-secrets.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,15 @@ These settings are used by shields.io for GitHub OAuth app authorization
but will not be necessary for most self-hosted installations. See
[production-hosting.md](./production-hosting.md).

### GitLab

- `GITLAB_ORIGINS` (yml: `public.services.gitlab.authorizedOrigins`)
- `GITLAB_TOKEN` (yml: `private.gitlab_token`)

A GitLab [Personal Access Token][gitlab-pat] is required for accessing private content. If you need a GitLab token for your self-hosted Shields server then we recommend limiting the scopes to the minimal set necessary for the badges you are using.

[gitlab-pat]: https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html

### Jenkins CI

- `JENKINS_ORIGINS` (yml: `public.services.jenkins.authorizedOrigins`)
Expand Down
19 changes: 19 additions & 0 deletions services/gitlab/gitlab-base.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { BaseJsonService } from '../index.js'

export default class GitLabBase extends BaseJsonService {
static auth = {
passKey: 'gitlab_token',
serviceKey: 'gitlab',
}

async fetch({ url, options, schema, errorMessages }) {
return this._requestJson(
this.authHelper.withBasicAuth({
schema,
url,
options,
errorMessages,
})
)
}
}
131 changes: 131 additions & 0 deletions services/gitlab/gitlab-tag.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import Joi from 'joi'
import { version as versionColor } from '../color-formatters.js'
import { optionalUrl } from '../validators.js'
import { latest } from '../version.js'
import { addv } from '../text-formatters.js'
import { NotFound } from '../index.js'
import GitLabBase from './gitlab-base.js'

const schema = Joi.array().items(
Joi.object({
name: Joi.string().required(),
})
)

const queryParamSchema = Joi.object({
gitlab_url: optionalUrl,
include_prereleases: Joi.equal(''),
sort: Joi.string().valid('date', 'semver').default('date'),
}).required()

export default class GitlabTag extends GitLabBase {
static category = 'version'

static route = {
base: 'gitlab/v/tag',
pattern: ':user/:repo',
queryParamSchema,
}

static examples = [
{
title: 'GitLab tag (latest by date)',
namedParams: {
user: 'shields-ops-group',
repo: 'tag-test',
},
queryParams: { sort: 'date' },
staticPreview: this.render({ version: 'v2.0.0' }),
},
{
title: 'GitLab tag (latest by SemVer)',
namedParams: {
user: 'shields-ops-group',
repo: 'tag-test',
},
queryParams: { sort: 'semver' },
staticPreview: this.render({ version: 'v4.0.0' }),
},
{
title: 'GitLab tag (latest by SemVer pre-release)',
namedParams: {
user: 'shields-ops-group',
repo: 'tag-test',
},
queryParams: {
sort: 'semver',
include_prereleases: null,
},
staticPreview: this.render({ version: 'v5.0.0-beta.1', sort: 'semver' }),
},
{
title: 'GitLab tag (custom instance)',
namedParams: {
user: 'GNOME',
repo: 'librsvg',
},
queryParams: {
sort: 'semver',
include_prereleases: null,
gitlab_url: 'https://gitlab.gnome.org',
},
staticPreview: this.render({ version: 'v2.51.4' }),
},
]

static defaultBadgeData = { label: 'tag' }

static render({ version, sort }) {
return {
message: addv(version),
color: sort === 'semver' ? versionColor(version) : 'blue',
}
}

async fetch({ user, repo, baseUrl }) {
// https://docs.gitlab.com/ee/api/tags.html
// N.B. the documentation has contradictory information about default sort order.
// As of 2020-10-11 the default is by date, but we add the `order_by` query param
// explicitly in case that changes upstream.
return super.fetch({
schema,
url: `${baseUrl}/api/v4/projects/${user}%2F${repo}/repository/tags`,
options: { qs: { order_by: 'updated' } },
errorMessages: {
404: 'repo not found',
},
})
}

static transform({ tags, sort, includePrereleases }) {
if (tags.length === 0) {
throw new NotFound({ prettyMessage: 'no tags found' })
}

if (sort === 'date') {
return tags[0].name
}

return latest(
tags.map(t => t.name),
{ pre: includePrereleases }
)
}

async handle(
{ user, repo },
{
gitlab_url: baseUrl = 'https://gitlab.com',
include_prereleases: pre,
sort,
}
) {
const tags = await this.fetch({ user, repo, baseUrl })
const version = this.constructor.transform({
tags,
sort,
includePrereleases: pre !== undefined,
})
return this.constructor.render({ version, sort })
}
}
47 changes: 47 additions & 0 deletions services/gitlab/gitlab-tag.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { expect } from 'chai'
import nock from 'nock'
import { cleanUpNockAfterEach, defaultContext } from '../test-helpers.js'
import GitLabTag from './gitlab-tag.service.js'

describe('GitLabTag', function () {
describe('auth', function () {
cleanUpNockAfterEach()

const fakeToken = 'abc123'
const config = {
public: {
services: {
gitlab: {
authorizedOrigins: ['https://gitlab.com'],
},
},
},
private: {
gitlab_token: fakeToken,
},
}

it('sends the auth information as configured', async function () {
const scope = nock('https://gitlab.com/')
.get('/api/v4/projects/foo%2Fbar/repository/tags?order_by=updated')
// This ensures that the expected credentials are actually being sent with the HTTP request.
// Without this the request wouldn't match and the test would fail.
.basicAuth({ user: '', pass: fakeToken })
.reply(200, [{ name: '1.9' }])

expect(
await GitLabTag.invoke(
defaultContext,
config,
{ user: 'foo', repo: 'bar' },
{}
)
).to.deep.equal({
message: 'v1.9',
color: 'blue',
})

scope.done()
})
})
})
27 changes: 27 additions & 0 deletions services/gitlab/gitlab-tag.tester.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { isSemver } from '../test-validators.js'
import { createServiceTester } from '../tester.js'
export const t = await createServiceTester()

t.create('Tag (latest by date)')
.get('/shields-ops-group/tag-test.json')
.expectBadge({ label: 'tag', message: 'v2.0.0', color: 'blue' })

t.create('Tag (latest by SemVer)')
.get('/shields-ops-group/tag-test.json?sort=semver')
.expectBadge({ label: 'tag', message: 'v4.0.0', color: 'blue' })

t.create('Tag (latest by SemVer pre-release)')
.get('/shields-ops-group/tag-test.json?sort=semver&include_prereleases')
.expectBadge({ label: 'tag', message: 'v5.0.0-beta.1', color: 'orange' })

t.create('Tag (custom instance')
.get('/GNOME/librsvg.json?gitlab_url=https://gitlab.gnome.org')
.expectBadge({ label: 'tag', message: isSemver, color: 'blue' })

t.create('Tag (repo not found)')
.get('/fdroid/nonexistant.json')
.expectBadge({ label: 'tag', message: 'repo not found' })

t.create('Tag (no tags)')
.get('/fdroid/fdroiddata.json')
.expectBadge({ label: 'tag', message: 'no tags found' })

0 comments on commit 961e13b

Please sign in to comment.