feat: add [gitlabissues] service (#8108)

* feat: add gitlab issues service

* fixes based on review

* fixes based on review

Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
This commit is contained in:
guoxudong
2022-07-05 06:38:05 +08:00
committed by GitHub
parent 8f03cf6025
commit e9c08512ce
3 changed files with 436 additions and 0 deletions

View File

@@ -0,0 +1,286 @@
import Joi from 'joi'
import { optionalUrl, nonNegativeInteger } from '../validators.js'
import { metric } from '../text-formatters.js'
import GitLabBase from './gitlab-base.js'
const schema = Joi.object({
statistics: Joi.object({
counts: Joi.object({
all: nonNegativeInteger,
closed: nonNegativeInteger,
opened: nonNegativeInteger,
}).required(),
}).allow(null),
}).required()
const queryParamSchema = Joi.object({
labels: Joi.string(),
gitlab_url: optionalUrl,
}).required()
const documentation = `
<p>
You may use your GitLab Project Id (e.g. 278964) or your Project Path (e.g. gitlab-org/gitlab ).
Note that only internet-accessible GitLab instances are supported, for example https://jihulab.com, https://gitlab.gnome.org, or https://gitlab.com/.
</p>
`
const labelDocumentation = `
<p>
If you want to use multiple labels then please use commas (<code>,</code>) to separate them, e.g. <code>foo,bar</code>.
</p>
`
export default class GitlabIssues extends GitLabBase {
static category = 'issue-tracking'
static route = {
base: 'gitlab/issues',
pattern: ':variant(all|open|closed):raw(-raw)?/:project+',
queryParamSchema,
}
static examples = [
{
title: 'GitLab issues',
pattern: 'open/:project+',
namedParams: {
project: 'gitlab-org/gitlab',
},
queryParams: { gitlab_url: 'https://gitlab.com' },
staticPreview: {
label: 'issues',
message: '44k open',
color: 'yellow',
},
documentation,
},
{
title: 'GitLab issues',
pattern: 'open-raw/:project+',
namedParams: {
project: 'gitlab-org/gitlab',
},
queryParams: { gitlab_url: 'https://gitlab.com' },
staticPreview: {
label: 'open issues',
message: '44k',
color: 'yellow',
},
documentation,
},
{
title: 'GitLab issues by-label',
pattern: 'open/:project+',
namedParams: {
project: 'gitlab-org/gitlab',
},
queryParams: {
labels: 'test,failure::new',
gitlab_url: 'https://gitlab.com',
},
staticPreview: {
label: 'test,failure::new issues',
message: '16 open',
color: 'yellow',
},
documentation: documentation + labelDocumentation,
},
{
title: 'GitLab issues by-label',
pattern: 'open-raw/:project+',
namedParams: {
project: 'gitlab-org/gitlab',
},
queryParams: {
labels: 'test,failure::new',
gitlab_url: 'https://gitlab.com',
},
staticPreview: {
label: 'open test,failure::new issues',
message: '16',
color: 'yellow',
},
documentation: documentation + labelDocumentation,
},
{
title: 'GitLab closed issues',
pattern: 'closed/:project+',
namedParams: {
project: 'gitlab-org/gitlab',
},
queryParams: { gitlab_url: 'https://gitlab.com' },
staticPreview: {
label: 'issues',
message: '72k closed',
color: 'yellow',
},
documentation,
},
{
title: 'GitLab closed issues',
pattern: 'closed-raw/:project+',
namedParams: {
project: 'gitlab-org/gitlab',
},
queryParams: { gitlab_url: 'https://gitlab.com' },
staticPreview: {
label: 'closed issues',
message: '72k ',
color: 'yellow',
},
documentation,
},
{
title: 'GitLab closed issues by-label',
pattern: 'closed/:project+',
namedParams: {
project: 'gitlab-org/gitlab',
},
queryParams: {
labels: 'test,failure::new',
gitlab_url: 'https://gitlab.com',
},
staticPreview: {
label: 'test,failure::new issues',
message: '4 closed',
color: 'yellow',
},
documentation: documentation + labelDocumentation,
},
{
title: 'GitLab closed issues by-label',
pattern: 'closed-raw/:project+',
namedParams: {
project: 'gitlab-org/gitlab',
},
queryParams: {
labels: 'test,failure::new',
gitlab_url: 'https://gitlab.com',
},
staticPreview: {
label: 'closed test,failure::new issues',
message: '4',
color: 'yellow',
},
documentation: documentation + labelDocumentation,
},
{
title: 'GitLab all issues',
pattern: 'all/:project+',
namedParams: {
project: 'gitlab-org/gitlab',
},
queryParams: { gitlab_url: 'https://gitlab.com' },
staticPreview: {
label: 'issues',
message: '115k all',
color: 'yellow',
},
documentation,
},
{
title: 'GitLab all issues',
pattern: 'all-raw/:project+',
namedParams: {
project: 'gitlab-org/gitlab',
},
queryParams: { gitlab_url: 'https://gitlab.com' },
staticPreview: {
label: 'all issues',
message: '115k',
color: 'yellow',
},
documentation,
},
{
title: 'GitLab all issues by-label',
pattern: 'all-raw/:project+',
namedParams: {
project: 'gitlab-org/gitlab',
},
queryParams: {
labels: 'test,failure::new',
gitlab_url: 'https://gitlab.com',
},
staticPreview: {
label: 'all test,failure::new issues',
message: '20',
color: 'yellow',
},
documentation: documentation + labelDocumentation,
},
]
static defaultBadgeData = { label: 'issues', color: 'informational' }
static render({ variant, raw, labels, issueCount }) {
const state = variant
const isMultiLabel = labels && labels.includes(',')
const labelText = labels ? `${isMultiLabel ? `${labels}` : labels} ` : ''
let labelPrefix = ''
let messageSuffix = ''
if (raw !== undefined) {
labelPrefix = `${state} `
} else {
messageSuffix = state
}
return {
label: `${labelPrefix}${labelText}issues`,
message: `${metric(issueCount)}${
messageSuffix ? ' ' : ''
}${messageSuffix}`,
color: issueCount > 0 ? 'yellow' : 'brightgreen',
}
}
async fetch({ project, baseUrl, labels }) {
// https://docs.gitlab.com/ee/api/issues_statistics.html#get-project-issues-statistics
return super.fetch({
schema,
url: `${baseUrl}/api/v4/projects/${encodeURIComponent(
project
)}/issues_statistics`,
options: labels ? { searchParams: { labels } } : undefined,
errorMessages: {
404: 'project not found',
},
})
}
static transform({ variant, statistics }) {
const state = variant
let issueCount
switch (state) {
case 'open':
issueCount = statistics.counts.opened
break
case 'closed':
issueCount = statistics.counts.closed
break
case 'all':
issueCount = statistics.counts.all
break
}
return issueCount
}
async handle(
{ variant, raw, project },
{ gitlab_url: baseUrl = 'https://gitlab.com', labels }
) {
const { statistics } = await this.fetch({
project,
baseUrl,
labels,
})
return this.constructor.render({
variant,
raw,
labels,
issueCount: this.constructor.transform({ variant, statistics }),
})
}
}

View File

@@ -0,0 +1,147 @@
import Joi from 'joi'
import { createServiceTester } from '../tester.js'
import {
isMetric,
isMetricOpenIssues,
isMetricClosedIssues,
} from '../test-validators.js'
export const t = await createServiceTester()
t.create('Issues (project not found)')
.get('/open/guoxudong.io/shields-test/do-not-exist.json')
.expectBadge({
label: 'issues',
message: 'project not found',
})
/**
* Opened issue number case
*/
t.create('Opened issues')
.get('/open/guoxudong.io/shields-test/issue-test.json')
.expectBadge({
label: 'issues',
message: isMetricOpenIssues,
})
t.create('Open issues raw')
.get('/open-raw/guoxudong.io/shields-test/issue-test.json')
.expectBadge({
label: 'open issues',
message: isMetric,
})
t.create('Open issues by label is > zero')
.get('/open/guoxudong.io/shields-test/issue-test.json?labels=discussion')
.expectBadge({
label: 'discussion issues',
message: isMetricOpenIssues,
})
t.create('Open issues by multi-word label is > zero')
.get(
'/open/guoxudong.io/shields-test/issue-test.json?labels=discussion,enhancement'
)
.expectBadge({
label: 'discussion,enhancement issues',
message: isMetricOpenIssues,
})
t.create('Open issues by label (raw)')
.get('/open-raw/guoxudong.io/shields-test/issue-test.json?labels=discussion')
.expectBadge({
label: 'open discussion issues',
message: isMetric,
})
t.create('Opened issues by Scoped labels')
.get('/open/gitlab-org%2Fgitlab.json?labels=test,failure::new')
.expectBadge({
label: 'test,failure::new issues',
message: isMetricOpenIssues,
})
/**
* Closed issue number case
*/
t.create('Closed issues')
.get('/closed/guoxudong.io/shields-test/issue-test.json')
.expectBadge({
label: 'issues',
message: isMetricClosedIssues,
})
t.create('Closed issues raw')
.get('/closed-raw/guoxudong.io/shields-test/issue-test.json')
.expectBadge({
label: 'closed issues',
message: isMetric,
})
t.create('Closed issues by label is > zero')
.get('/closed/guoxudong.io/shields-test/issue-test.json?labels=bug')
.expectBadge({
label: 'bug issues',
message: isMetricClosedIssues,
})
t.create('Closed issues by multi-word label is > zero')
.get('/closed/guoxudong.io/shields-test/issue-test.json?labels=bug,critical')
.expectBadge({
label: 'bug,critical issues',
message: isMetricClosedIssues,
})
t.create('Closed issues by label (raw)')
.get('/closed-raw/guoxudong.io/shields-test/issue-test.json?labels=bug')
.expectBadge({
label: 'closed bug issues',
message: isMetric,
})
/**
* All issue number case
*/
t.create('All issues')
.get('/all/guoxudong.io/shields-test/issue-test.json')
.expectBadge({
label: 'issues',
message: Joi.string().regex(
/^([0-9]+[kMGTPEZY]?|[1-9]\.[1-9][kMGTPEZY]) all$/
),
})
t.create('All issues raw')
.get('/all-raw/guoxudong.io/shields-test/issue-test.json')
.expectBadge({
label: 'all issues',
message: isMetric,
})
t.create('All issues by label is > zero')
.get('/all/guoxudong.io/shields-test/issue-test.json?labels=discussion')
.expectBadge({
label: 'discussion issues',
message: Joi.string().regex(
/^([0-9]+[kMGTPEZY]?|[1-9]\.[1-9][kMGTPEZY]) all$/
),
})
t.create('All issues by multi-word label is > zero')
.get(
'/all/guoxudong.io/shields-test/issue-test.json?labels=discussion,enhancement'
)
.expectBadge({
label: 'discussion,enhancement issues',
message: Joi.string().regex(
/^([0-9]+[kMGTPEZY]?|[1-9]\.[1-9][kMGTPEZY]) all$/
),
})
t.create('All issues by label (raw)')
.get('/all-raw/guoxudong.io/shields-test/issue-test.json?labels=discussion')
.expectBadge({
label: 'all discussion issues',
message: isMetric,
})

View File

@@ -79,6 +79,8 @@ const isMetricWithPattern = nestedRegexp => {
const isMetricOpenIssues = isMetricWithPattern(/ open/)
const isMetricClosedIssues = isMetricWithPattern(/ closed/)
const isMetricOverMetric = isMetricWithPattern(
/\/([1-9][0-9]*[kMGTPEZY]?|[1-9]\.[1-9][kMGTPEZY])/
)
@@ -167,6 +169,7 @@ export {
isMetricAllowNegative,
isMetricWithPattern,
isMetricOpenIssues,
isMetricClosedIssues,
isMetricOverMetric,
isMetricOverTimePeriod,
isZeroOverTimePeriod,