Skip to content

Commit

Permalink
[GITEA] add new gitea service (release/languages) (#9781)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
CanisHelix authored Dec 18, 2023
1 parent 2814de2 commit 8f1f787
Show file tree
Hide file tree
Showing 9 changed files with 381 additions and 0 deletions.
2 changes: 2 additions & 0 deletions core/server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ const publicConfigSchema = Joi.object({
},
restApiVersion: Joi.date().raw().required(),
},
gitea: defaultService,
gitlab: defaultService,
jira: defaultService,
jenkins: Joi.object({
Expand Down Expand Up @@ -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(),
Expand Down
9 changes: 9 additions & 0 deletions doc/server-secrets.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down
19 changes: 19 additions & 0 deletions services/gitea/gitea-base.js
Original file line number Diff line number Diff line change
@@ -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 changes: 48 additions & 0 deletions services/gitea/gitea-base.spec.js
Original file line number Diff line number Diff line change
@@ -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 changes: 12 additions & 0 deletions services/gitea/gitea-helper.js
Original file line number Diff line number Diff line change
@@ -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 changes: 77 additions & 0 deletions services/gitea/gitea-languages-count.service.js
Original file line number Diff line number Diff line change
@@ -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 changes: 27 additions & 0 deletions services/gitea/gitea-languages-count.tester.js
Original file line number Diff line number Diff line change
@@ -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 changes: 147 additions & 0 deletions services/gitea/gitea-release.service.js
Original file line number Diff line number Diff line change
@@ -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 })
}
}
Loading

0 comments on commit 8f1f787

Please sign in to comment.