[GITEA] add new gitea service (release/languages) (#9781)
* add gitea service based on gitlab * update gitea to use mocks * add gitea release test * move tests to use public repo on codeberg and fixes * add pagination, update tests to live, set gitea_url as required * add auth test (wip) * fix base auth test * fix required optionalUrl, remove default, assume semver from firstpage * update example to use stable repository
This commit is contained in:
@@ -128,6 +128,7 @@ const publicConfigSchema = Joi.object({
|
||||
},
|
||||
restApiVersion: Joi.date().raw().required(),
|
||||
},
|
||||
gitea: defaultService,
|
||||
gitlab: defaultService,
|
||||
jira: defaultService,
|
||||
jenkins: Joi.object({
|
||||
@@ -168,6 +169,7 @@ const privateConfigSchema = Joi.object({
|
||||
gh_client_id: Joi.string(),
|
||||
gh_client_secret: Joi.string(),
|
||||
gh_token: Joi.string(),
|
||||
gitea_token: Joi.string(),
|
||||
gitlab_token: Joi.string(),
|
||||
jenkins_user: Joi.string(),
|
||||
jenkins_pass: Joi.string(),
|
||||
|
||||
@@ -167,6 +167,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).
|
||||
|
||||
### Gitea
|
||||
|
||||
- `GITEA_ORIGINS` (yml: `public.services.gitea.authorizedOrigins`)
|
||||
- `GITEA_TOKEN` (yml: `private.gitea_token`)
|
||||
|
||||
A Gitea [Personal Access Token][gitea-pat] is required for accessing private content. If you need a Gitea token for your self-hosted Shields server then we recommend limiting the scopes to the minimal set necessary for the badges you are using.
|
||||
|
||||
[gitea-pat]: https://docs.gitea.com/development/api-usage#generating-and-listing-api-tokens
|
||||
|
||||
### GitLab
|
||||
|
||||
- `GITLAB_ORIGINS` (yml: `public.services.gitlab.authorizedOrigins`)
|
||||
|
||||
19
services/gitea/gitea-base.js
Normal file
19
services/gitea/gitea-base.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { BaseJsonService } from '../index.js'
|
||||
|
||||
export default class GiteaBase extends BaseJsonService {
|
||||
static auth = {
|
||||
passKey: 'gitea_token',
|
||||
serviceKey: 'gitea',
|
||||
}
|
||||
|
||||
async fetch({ url, options, schema, httpErrors }) {
|
||||
return this._requestJson(
|
||||
this.authHelper.withBearerAuthHeader({
|
||||
schema,
|
||||
url,
|
||||
options,
|
||||
httpErrors,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
48
services/gitea/gitea-base.spec.js
Normal file
48
services/gitea/gitea-base.spec.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import Joi from 'joi'
|
||||
import { expect } from 'chai'
|
||||
import nock from 'nock'
|
||||
import { cleanUpNockAfterEach, defaultContext } from '../test-helpers.js'
|
||||
import GiteaBase from './gitea-base.js'
|
||||
|
||||
class DummyGiteaService extends GiteaBase {
|
||||
static route = { base: 'fake-base' }
|
||||
|
||||
async handle() {
|
||||
const data = await this.fetch({
|
||||
schema: Joi.any(),
|
||||
url: 'https://codeberg.org/api/v1/repos/CanisHelix/shields-badge-test/releases',
|
||||
})
|
||||
return { message: data.message }
|
||||
}
|
||||
}
|
||||
|
||||
describe('GiteaBase', function () {
|
||||
describe('auth', function () {
|
||||
cleanUpNockAfterEach()
|
||||
|
||||
const config = {
|
||||
public: {
|
||||
services: {
|
||||
gitea: {
|
||||
authorizedOrigins: ['https://codeberg.org'],
|
||||
},
|
||||
},
|
||||
},
|
||||
private: {
|
||||
gitea_token: 'fake-key',
|
||||
},
|
||||
}
|
||||
|
||||
it('sends the auth information as configured', async function () {
|
||||
const scope = nock('https://codeberg.org')
|
||||
.get('/api/v1/repos/CanisHelix/shields-badge-test/releases')
|
||||
.matchHeader('Authorization', 'Bearer fake-key')
|
||||
.reply(200, { message: 'fake message' })
|
||||
expect(
|
||||
await DummyGiteaService.invoke(defaultContext, config, {}),
|
||||
).to.not.have.property('isError')
|
||||
|
||||
scope.done()
|
||||
})
|
||||
})
|
||||
})
|
||||
12
services/gitea/gitea-helper.js
Normal file
12
services/gitea/gitea-helper.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const documentation = `
|
||||
Note that the gitea_url parameter is required because there is canonical hosted gitea service provided by Gitea.
|
||||
`
|
||||
|
||||
function httpErrorsFor() {
|
||||
return {
|
||||
403: 'private repo',
|
||||
404: 'user or repo not found',
|
||||
}
|
||||
}
|
||||
|
||||
export { documentation, httpErrorsFor }
|
||||
77
services/gitea/gitea-languages-count.service.js
Normal file
77
services/gitea/gitea-languages-count.service.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import Joi from 'joi'
|
||||
import { nonNegativeInteger, optionalUrl } from '../validators.js'
|
||||
import { metric } from '../text-formatters.js'
|
||||
import { pathParam, queryParam } from '../index.js'
|
||||
import { documentation, httpErrorsFor } from './gitea-helper.js'
|
||||
import GiteaBase from './gitea-base.js'
|
||||
|
||||
/*
|
||||
We're expecting a response like { "Python": 39624, "Shell": 104 }
|
||||
The keys could be anything and {} is a valid response (e.g: for an empty repo)
|
||||
*/
|
||||
const schema = Joi.object().pattern(/./, nonNegativeInteger)
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
gitea_url: optionalUrl.required(),
|
||||
}).required()
|
||||
|
||||
export default class GiteaLanguageCount extends GiteaBase {
|
||||
static category = 'analysis'
|
||||
|
||||
static route = {
|
||||
base: 'gitea/languages/count',
|
||||
pattern: ':user/:repo',
|
||||
queryParamSchema,
|
||||
}
|
||||
|
||||
static openApi = {
|
||||
'/gitea/languages/count/{user}/{repo}': {
|
||||
get: {
|
||||
summary: 'Gitea language count',
|
||||
description: documentation,
|
||||
parameters: [
|
||||
pathParam({
|
||||
name: 'user',
|
||||
example: 'forgejo',
|
||||
}),
|
||||
pathParam({
|
||||
name: 'repo',
|
||||
example: 'forgejo',
|
||||
}),
|
||||
queryParam({
|
||||
name: 'gitea_url',
|
||||
example: 'https://codeberg.org',
|
||||
required: true,
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
static defaultBadgeData = { label: 'languages' }
|
||||
|
||||
static render({ languagesCount }) {
|
||||
return {
|
||||
message: metric(languagesCount),
|
||||
color: 'blue',
|
||||
}
|
||||
}
|
||||
|
||||
async fetch({ user, repo, baseUrl }) {
|
||||
// https://try.gitea.io/api/swagger#/repository/repoGetLanguages
|
||||
return super.fetch({
|
||||
schema,
|
||||
url: `${baseUrl}/api/v1/repos/${user}/${repo}/languages`,
|
||||
httpErrors: httpErrorsFor('user or repo not found'),
|
||||
})
|
||||
}
|
||||
|
||||
async handle({ user, repo }, { gitea_url: baseUrl }) {
|
||||
const data = await this.fetch({
|
||||
user,
|
||||
repo,
|
||||
baseUrl,
|
||||
})
|
||||
return this.constructor.render({ languagesCount: Object.keys(data).length })
|
||||
}
|
||||
}
|
||||
27
services/gitea/gitea-languages-count.tester.js
Normal file
27
services/gitea/gitea-languages-count.tester.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import Joi from 'joi'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('language count (empty repo)')
|
||||
.get(
|
||||
'/CanisHelix/shields-badge-test-empty.json?gitea_url=https://codeberg.org',
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'languages',
|
||||
message: '0',
|
||||
})
|
||||
|
||||
t.create('language count')
|
||||
.get('/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org')
|
||||
.expectBadge({
|
||||
label: 'languages',
|
||||
message: Joi.number().integer().positive(),
|
||||
})
|
||||
|
||||
t.create('language count (user or repo not found)')
|
||||
.get('/CanisHelix/does-not-exist.json?gitea_url=https://codeberg.org')
|
||||
.expectBadge({
|
||||
label: 'languages',
|
||||
message: 'user or repo not found',
|
||||
})
|
||||
147
services/gitea/gitea-release.service.js
Normal file
147
services/gitea/gitea-release.service.js
Normal file
@@ -0,0 +1,147 @@
|
||||
import Joi from 'joi'
|
||||
import { optionalUrl } from '../validators.js'
|
||||
import { latest, renderVersionBadge } from '../version.js'
|
||||
import { NotFound, pathParam, queryParam } from '../index.js'
|
||||
import { documentation, httpErrorsFor } from './gitea-helper.js'
|
||||
import GiteaBase from './gitea-base.js'
|
||||
|
||||
const schema = Joi.array().items(
|
||||
Joi.object({
|
||||
name: Joi.string().required(),
|
||||
tag_name: Joi.string().required(),
|
||||
prerelease: Joi.boolean().required(),
|
||||
}),
|
||||
)
|
||||
|
||||
const sortEnum = ['date', 'semver']
|
||||
const displayNameEnum = ['tag', 'release']
|
||||
const dateOrderByEnum = ['created_at', 'published_at']
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
gitea_url: optionalUrl.required(),
|
||||
include_prereleases: Joi.equal(''),
|
||||
sort: Joi.string()
|
||||
.valid(...sortEnum)
|
||||
.default('date'),
|
||||
display_name: Joi.string()
|
||||
.valid(...displayNameEnum)
|
||||
.default('tag'),
|
||||
date_order_by: Joi.string()
|
||||
.valid(...dateOrderByEnum)
|
||||
.default('created_at'),
|
||||
}).required()
|
||||
|
||||
export default class GiteaRelease extends GiteaBase {
|
||||
static category = 'version'
|
||||
|
||||
static route = {
|
||||
base: 'gitea/v/release',
|
||||
pattern: ':user/:repo',
|
||||
queryParamSchema,
|
||||
}
|
||||
|
||||
static openApi = {
|
||||
'/gitea/v/release/{user}/{repo}': {
|
||||
get: {
|
||||
summary: 'Gitea Release',
|
||||
description: documentation,
|
||||
parameters: [
|
||||
pathParam({
|
||||
name: 'user',
|
||||
example: 'forgejo',
|
||||
}),
|
||||
pathParam({
|
||||
name: 'repo',
|
||||
example: 'forgejo',
|
||||
}),
|
||||
queryParam({
|
||||
name: 'gitea_url',
|
||||
example: 'https://codeberg.org',
|
||||
required: true,
|
||||
}),
|
||||
queryParam({
|
||||
name: 'include_prereleases',
|
||||
schema: { type: 'boolean' },
|
||||
example: null,
|
||||
}),
|
||||
queryParam({
|
||||
name: 'sort',
|
||||
schema: { type: 'string', enum: sortEnum },
|
||||
example: 'semver',
|
||||
}),
|
||||
queryParam({
|
||||
name: 'display_name',
|
||||
schema: { type: 'string', enum: displayNameEnum },
|
||||
example: 'release',
|
||||
}),
|
||||
queryParam({
|
||||
name: 'date_order_by',
|
||||
schema: { type: 'string', enum: dateOrderByEnum },
|
||||
example: 'created_at',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
static defaultBadgeData = { label: 'release' }
|
||||
|
||||
async fetch({ user, repo, baseUrl }) {
|
||||
// https://try.gitea.io/api/swagger#/repository/repoGetRelease
|
||||
return super.fetch({
|
||||
schema,
|
||||
url: `${baseUrl}/api/v1/repos/${user}/${repo}/releases`,
|
||||
httpErrors: httpErrorsFor(),
|
||||
})
|
||||
}
|
||||
|
||||
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 latest(
|
||||
releases.map(t => t[displayKey]),
|
||||
{ pre: includePrereleases },
|
||||
)
|
||||
}
|
||||
|
||||
if (!includePrereleases) {
|
||||
const stableReleases = releases.filter(release => !release.prerelease)
|
||||
if (stableReleases.length > 0) {
|
||||
return stableReleases[0][displayKey]
|
||||
}
|
||||
}
|
||||
|
||||
return releases[0][displayKey]
|
||||
}
|
||||
|
||||
async handle(
|
||||
{ user, repo },
|
||||
{
|
||||
gitea_url: baseUrl,
|
||||
include_prereleases: pre,
|
||||
sort,
|
||||
display_name: displayName,
|
||||
date_order_by: orderBy,
|
||||
},
|
||||
) {
|
||||
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 })
|
||||
}
|
||||
}
|
||||
40
services/gitea/gitea-release.tester.js
Normal file
40
services/gitea/gitea-release.tester.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('Release (latest by date)')
|
||||
.get('/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org')
|
||||
.expectBadge({ label: 'release', message: 'v3.0.0', color: 'blue' })
|
||||
|
||||
t.create('Release (latest by date, order by created_at)')
|
||||
.get(
|
||||
'/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org&date_order_by=created_at',
|
||||
)
|
||||
.expectBadge({ label: 'release', message: 'v3.0.0', color: 'blue' })
|
||||
|
||||
t.create('Release (latest by date, order by published_at)')
|
||||
.get(
|
||||
'/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org&date_order_by=published_at',
|
||||
)
|
||||
.expectBadge({ label: 'release', message: 'v3.0.0', color: 'blue' })
|
||||
|
||||
t.create('Release (latest by semver)')
|
||||
.get(
|
||||
'/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org&sort=semver',
|
||||
)
|
||||
.expectBadge({ label: 'release', message: 'v4.0.0', color: 'blue' })
|
||||
|
||||
t.create('Release (latest by semver pre-release)')
|
||||
.get(
|
||||
'/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org&sort=semver&include_prereleases',
|
||||
)
|
||||
.expectBadge({ label: 'release', message: 'v5.0.0-rc1', color: 'orange' })
|
||||
|
||||
t.create('Release (project not found)')
|
||||
.get('/CanisHelix/does-not-exist.json?gitea_url=https://codeberg.org')
|
||||
.expectBadge({ label: 'release', message: 'user or repo not found' })
|
||||
|
||||
t.create('Release (no tags)')
|
||||
.get(
|
||||
'/CanisHelix/shields-badge-test-empty.json?gitea_url=https://codeberg.org',
|
||||
)
|
||||
.expectBadge({ label: 'release', message: 'no releases found' })
|
||||
Reference in New Issue
Block a user