Compare commits

..

1 Commits

Author SHA1 Message Date
release[bot]
44893fe421 Update Changelog 2022-07-01 01:35:59 +00:00
23 changed files with 500 additions and 1488 deletions

View File

@@ -4,16 +4,25 @@ Note: this changelog is for the shields.io server. The changelog for the badge-m
---
## server-2022-07-03
## server-2022-07-01
- Add [galaxytoolshed] services [#8114](https://github.com/badges/shields/issues/8114)
- fix [gitlab] auth [#8145](https://github.com/badges/shields/issues/8145) [#8162](https://github.com/badges/shields/issues/8162)
- change routes for [gitlab] license and contributors badges [#8140](https://github.com/badges/shields/issues/8140)
- ci: support new circle PR url variable [#8135](https://github.com/badges/shields/issues/8135)
- increase cache length on AUR version badge, run [AUR] [#8110](https://github.com/badges/shields/issues/8110)
- Use GraphQL to fix GitHub file count badges [github] [#8112](https://github.com/badges/shields/issues/8112)
- update targets for [bower] service tests [#8107](https://github.com/badges/shields/issues/8107)
- accept version with suffix in [ore] service tests [#8106](https://github.com/badges/shields/issues/8106)
- style: unified self-managed gitlab instance name [#8105](https://github.com/badges/shields/issues/8105)
- feat: add [gitlab] contributors service [#8084](https://github.com/badges/shields/issues/8084)
- fix: display greasy fork dd correctly [#8088](https://github.com/badges/shields/issues/8088)
- [greasyfork] Add Greasy Fork service badges [#8080](https://github.com/badges/shields/issues/8080)
- Fix typos in endpoint badge docs [#8085](https://github.com/badges/shields/issues/8085)
- fix: gitlab licence service docs and example [#8083](https://github.com/badges/shields/issues/8083)
- add docstrings for endpoint-common service [#8079](https://github.com/badges/shields/issues/8079)
- fix(gitlab service test): fix gitlab pipeline & tag(nested group) service test, run [gitlabtag gitlabpipeline] [#8076](https://github.com/badges/shields/issues/8076)
- Add [gitlablicense] services [#8024](https://github.com/badges/shields/issues/8024)
- [Spack] Package Manager: Update Domain [#8046](https://github.com/badges/shields/issues/8046)
- Docstrings for dynamic-common service [#8027](https://github.com/badges/shields/issues/8027)
- switch [jitpack] to use latestOk endpoint [#8041](https://github.com/badges/shields/issues/8041)
- Dependency updates

View File

@@ -3,9 +3,6 @@
- The format of new badges should be of the form `/SERVICE/NOUN/PARAMETERS?QUERYSTRING` e.g:
`/github/issues/:user/:repo`. The service is github, the
badge is for issues, and the parameters are `:user/:repo`.
- The `NOUN` part of the route is:
- singular if the badge message represents a single entity, such as the current status of a build (e.g: `/build`), or a more abstract or aggregate representation of the thing (e.g.: `/coverage`, `/quality`)
- plural if there are (or may) be many of the thing (e.g: `/dependencies`, `/stars`)
- Parameters should always be part of the route if they are required to display a badge e.g: `:packageName`.
- Common optional params like, `:branch` or `:tag` should also be passed as part of the route.
- Query string parameters should be used when:

View File

@@ -1,75 +0,0 @@
# Integration with upstream services
## Overview
In a nutshell, the Shields Badge Server handles the responsibilities of accepting requests for badges, and then serving those badges back to users.
A grossly oversimplified visualization would probably look something like this:
```mermaid
sequenceDiagram
actor User
participant B as Badge Server
participant P as Data Provider
User->>+B: I'd like a badge for my project please!
B->>+P: Get data for project
P-->>-B: Data
B->>B: Make badge with data
B-->>-User: Here's your badge!
```
Shields is not a system of record (we're not the package registry, pipeline tool, etc.) so when Shields receives a request for a badge, the badge server will first have to reach out to the system of record in order to get the data points it needs to create your badge.
For example, if you ask Shields for a build status badge for your CircleCI pipeline, then Shields has to reach out to CircleCI to figure out what the status of your pipeline is (CircleCI would be the "Data Provider" actor in the above diagram). Similarly if you want a badge that shows the count of downloads of your npm package, Shields has to reach out to the npm.js registry.
That covers the gist, but the actual story is a bit more involved and complicated than that of course. There's a number of other components along the way, ranging from the browser on your local machine to extra services and actors we have deployed as part of the Shields.io runtime ecosystem which help ensure we can provide a stable and reliable deployment of the badge server-as-a-service. Additionally, badges rendered in GitHub have some additional factors at play that impose some additional constraints, detailed in the next section.
### GitHub Badge Rendering
A common usage pattern for badges is to embed them in your project's README files so relevant information is conveyed to the project's users. This means badges are often utilized and rendered in source control management platforms, like GitHub.
[GitHub utilizes a proxy service, called camo][camo], for handling the images that you see when you browse project pages on github.com, and this is utilized for badges too (both svg and png formats). GitHub does this for a number of reasons, including to anonymize requests and protect your privacy. However, this also requires the upstream images (including badges) to be returned quickly in order for those images to show up on your screen, with a rough ceiling of 3-4 seconds. If the upstream image provider is too slow to respond, then camo will timeout and the image won't be displayed.
This imposes constraints on Shields, as we need to ensure that the badge server completes the entire request/response workflow and returns the final badge within a few seconds.
[camo]: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-anonymized-urls
## Interaction patterns
Because of the time limits around the full badge flow discussed above, the badge server is somewhat constrained in terms of patterns it can employ to fetch data from the upstream provider. When we incorporate new badges we typically need to ensure when we receive a badge request and need to get data to serve that badge request, that we can get that data by making a single call to the upstream provider to get the data points needed for the badge.
The nature of the call Shields makes to upstream data providers is obviously dependent upon the nature of the upstream endpoints. The overwhelming majority of the time this occurs via a GET request, though there are a few occasions where the upstream endpoint requires a POST in order for us to retrieve data, and a few others where we issue a HEAD request because the data points we need for the badge actually reside in the response headers.
There are a couple other exceptions, but as a general pattern we strive to integrate with services via a stateless, single call manner.
### Authentication
Shields typically integrates with upstream data providers anonymously, largely because the data targets we need are anonymously available (open source packages, repositories, pipelines, etc.)
The badge server can be configured to make authenticated requests for certain supported services. This exists so that users who are [self-hosting] their own instance of the badge server can get badges for their private content, and also as part of agreements we've made with certain upstream data providers for the main Shields.io deployment.
[self-hosting]: ./self-hosting.md
### Rate Limits
Many upstream data providers employ common techniques to protect the availability and integrity of their service, and one common technique is [rate limiting].
Typically, when clients/consumers of rate limited endpoints will employ client-side techniques to avoid running afoul of those limits and potentially getting blocked or having their requests throttled. Unfortunately, those techniques aren't really viable for the Shields.io environment due to the workflow and constraints discussed above.
As such, we instead try to ensure that Shields.io never makes more calls to an upstream provider than their rate limits allow.
In cases where Shields.io may run close to or exceed those limits, we typically consider:
- increasing the cache periods we set (to reduce the number of badge requests we receive)
- collaborating with the vendor or maintainers of the upstream data provider to explore options for an increased rate limit for Shields.io
- decline to provide badges for that upstream data provider
[rate limiting]: https://en.wikipedia.org/wiki/Rate_limiting
### Denial of Service
coming soon...
### Considerations for new upstream integrations
coming soon...

948
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,7 @@
"@fontsource/lato": "^4.5.8",
"@fontsource/lekton": "^4.5.9",
"@renovate/pep440": "^1.0.0",
"@sentry/node": "^7.5.1",
"@sentry/node": "^7.2.0",
"@shields_io/camp": "^18.1.1",
"badge-maker": "file:badge-maker",
"bytes": "^3.1.2",
@@ -43,7 +43,7 @@
"got": "^12.1.0",
"graphql": "^15.6.1",
"graphql-tag": "^2.12.6",
"ioredis": "5.1.0",
"ioredis": "5.0.6",
"joi": "17.6.0",
"joi-extension-semver": "5.0.0",
"js-yaml": "^4.1.0",
@@ -51,17 +51,17 @@
"lodash.countby": "^4.6.0",
"lodash.groupby": "^4.6.0",
"lodash.times": "^4.3.2",
"moment": "^2.29.4",
"moment": "^2.29.3",
"node-env-flag": "^0.1.0",
"parse-link-header": "^2.0.0",
"path-to-regexp": "^6.2.1",
"pretty-bytes": "^6.0.0",
"priorityqueuejs": "^2.0.0",
"prom-client": "^14.0.1",
"qs": "^6.11.0",
"qs": "^6.10.5",
"query-string": "^7.1.1",
"semver": "~7.3.7",
"simple-icons": "7.4.0",
"simple-icons": "7.2.0",
"webextension-store-meta": "^1.0.5",
"xmldom": "~0.6.0",
"xpath": "~0.0.32"
@@ -141,9 +141,9 @@
]
},
"devDependencies": {
"@babel/core": "^7.18.6",
"@babel/core": "^7.18.5",
"@babel/polyfill": "^7.12.1",
"@babel/register": "7.18.6",
"@babel/register": "7.17.7",
"@istanbuljs/schema": "^0.1.3",
"@mapbox/react-click-to-select": "^2.2.1",
"@types/chai": "^4.3.1",
@@ -155,7 +155,7 @@
"@types/react-modal": "^3.13.1",
"@types/react-select": "^4.0.17",
"@types/styled-components": "5.1.25",
"@typescript-eslint/eslint-plugin": "^5.30.5",
"@typescript-eslint/eslint-plugin": "^5.28.0",
"@typescript-eslint/parser": "^5.27.0",
"babel-plugin-inline-react-svg": "^2.0.1",
"babel-preset-gatsby": "^2.14.0",
@@ -168,8 +168,8 @@
"child-process-promise": "^2.2.1",
"clipboard-copy": "^4.0.1",
"concurrently": "^7.2.2",
"cypress": "^10.3.0",
"danger": "^11.1.1",
"cypress": "^10.2.0",
"danger": "^11.0.7",
"danger-plugin-no-test-shortcuts": "^2.0.0",
"deepmerge": "^4.2.2",
"eslint": "^7.32.0",
@@ -203,7 +203,7 @@
"is-svg": "^4.3.2",
"js-yaml-loader": "^1.2.2",
"jsdoc": "^3.6.10",
"lint-staged": "^13.0.3",
"lint-staged": "^13.0.2",
"lodash.debounce": "^4.0.8",
"lodash.difference": "^4.5.0",
"minimist": "^1.2.6",
@@ -211,9 +211,9 @@
"mocha-env-reporter": "^4.0.0",
"mocha-junit-reporter": "^2.0.2",
"mocha-yaml-loader": "^1.0.3",
"nock": "13.2.8",
"nock": "13.2.7",
"node-mocks-http": "^1.11.0",
"nodemon": "^2.0.19",
"nodemon": "^2.0.18",
"npm-run-all": "^4.1.5",
"open-cli": "^7.0.1",
"portfinder": "^1.0.28",
@@ -236,7 +236,7 @@
"start-server-and-test": "1.14.0",
"styled-components": "^5.3.5",
"ts-mocha": "^10.0.0",
"tsd": "^0.22.0",
"tsd": "^0.21.0",
"typescript": "^4.7.4",
"url": "^0.11.0"
},

View File

@@ -31,7 +31,7 @@ export default class CondaVersion extends BaseCondaService {
static render({ variant, channel, version }) {
return {
label: variant === 'vn' ? channel : `conda | ${channel}`,
label: variant === 'vn' ? channel : `conda|${channel}`,
message: versionText(version),
color: versionColor(version),
}

View File

@@ -3,7 +3,7 @@ import { createServiceTester } from '../tester.js'
export const t = await createServiceTester()
t.create('version').get('/v/conda-forge/zlib.json').expectBadge({
label: 'conda | conda-forge',
label: 'conda|conda-forge',
message: isVPlusTripleDottedVersion,
})

View File

@@ -1,54 +0,0 @@
import Joi from 'joi'
import { nonNegativeInteger } from '../validators.js'
import { NotFound, BaseJsonService } from '../index.js'
const orderedInstallableRevisionsSchema = Joi.array()
.items(Joi.string())
.required()
const repositoryRevisionInstallInfoSchema = Joi.array()
.ordered(
Joi.object({
times_downloaded: nonNegativeInteger,
}).required()
)
.items(Joi.any())
export default class BaseGalaxyToolshedService extends BaseJsonService {
static defaultBadgeData = { label: 'galaxytoolshed' }
static baseUrl = 'https://toolshed.g2.bx.psu.edu'
async fetchOrderedInstallableRevisionsSchema({ repository, owner }) {
return this._requestJson({
schema: orderedInstallableRevisionsSchema,
url: `${this.constructor.baseUrl}/api/repositories/get_ordered_installable_revisions?name=${repository}&owner=${owner}`,
})
}
async fetchRepositoryRevisionInstallInfoSchema({
repository,
owner,
changesetRevision,
}) {
return this._requestJson({
schema: repositoryRevisionInstallInfoSchema,
url: `${this.constructor.baseUrl}/api/repositories/get_repository_revision_install_info?name=${repository}&owner=${owner}&changeset_revision=${changesetRevision}`,
})
}
async fetchLastOrderedInstallableRevisionsSchema({ repository, owner }) {
const changesetRevisions =
await this.fetchOrderedInstallableRevisionsSchema({
repository,
owner,
})
if (!Array.isArray(changesetRevisions) || !changesetRevisions.length) {
throw new NotFound({ prettyMessage: 'changesetRevision not found' })
}
return this.fetchRepositoryRevisionInstallInfoSchema({
repository,
owner,
changesetRevision: changesetRevisions[0],
})
}
}

View File

@@ -1,34 +0,0 @@
import { renderDownloadsBadge } from '../downloads.js'
import BaseGalaxyToolshedService from './galaxytoolshed-base.js'
export default class GalaxyToolshedDownloads extends BaseGalaxyToolshedService {
static category = 'downloads'
static route = {
base: 'galaxytoolshed/downloads',
pattern: ':repository/:owner',
}
static examples = [
{
title: 'Galaxy Toolshed - Downloads',
namedParams: {
repository: 'sra_tools',
owner: 'iuc',
},
staticPreview: renderDownloadsBadge({ downloads: 10000 }),
},
]
static defaultBadgeData = {
label: 'downloads',
}
async handle({ repository, owner }) {
const response = await this.fetchLastOrderedInstallableRevisionsSchema({
repository,
owner,
})
const { times_downloaded: downloads } = response[0]
return renderDownloadsBadge({ downloads })
}
}

View File

@@ -1,28 +0,0 @@
import { isMetric } from '../test-validators.js'
import { createServiceTester } from '../tester.js'
export const t = await createServiceTester()
t.create('downloads - raw').get('/sra_tools/iuc.json').expectBadge({
label: 'downloads',
message: isMetric,
})
t.create('downloads - repository not found')
.get('/sra_tool/iuc.json')
.expectBadge({
label: 'downloads',
message: 'not found',
})
t.create('downloads - owner not found').get('/sra_tools/iu.json').expectBadge({
label: 'downloads',
message: 'not found',
})
t.create('downloads - changesetRevision not found')
.get('/bioqc/badilla.json')
.expectBadge({
label: 'downloads',
message: 'changesetRevision not found',
})

View File

@@ -8,7 +8,7 @@ export default class GitLabBase extends BaseJsonService {
async fetch({ url, options, schema, errorMessages }) {
return this._requestJson(
this.authHelper.withBearerAuthHeader({
this.authHelper.withBasicAuth({
schema,
url,
options,
@@ -18,12 +18,10 @@ export default class GitLabBase extends BaseJsonService {
}
async fetchPage({ page, requestParams, schema }) {
const { res, buffer } = await this._request(
this.authHelper.withBearerAuthHeader({
...requestParams,
...{ options: { searchParams: { page } } },
})
)
const { res, buffer } = await this._request({
...requestParams,
...{ options: { searchParams: { page } } },
})
const json = this._parseJson(buffer)
const data = this.constructor._validate(json, schema)
@@ -37,7 +35,7 @@ export default class GitLabBase extends BaseJsonService {
errorMessages,
firstPageOnly = false,
}) {
const requestParams = {
const requestParams = this.authHelper.withBasicAuth({
url,
options: {
headers: { Accept: 'application/json' },
@@ -45,7 +43,7 @@ export default class GitLabBase extends BaseJsonService {
...options,
},
errorMessages,
}
})
const {
res: { headers },

View File

@@ -58,17 +58,15 @@ export default class GitlabContributors extends GitLabBase {
async handle({ project }, { gitlab_url: baseUrl = 'https://gitlab.com' }) {
// https://docs.gitlab.com/ee/api/repositories.html#contributors
const { res } = await this._request(
this.authHelper.withBearerAuthHeader({
url: `${baseUrl}/api/v4/projects/${encodeURIComponent(
project
)}/repository/contributors`,
options: { searchParams: { page: '1', per_page: '1' } },
errorMessages: {
404: 'project not found',
},
})
)
const { res } = await this._request({
url: `${baseUrl}/api/v4/projects/${encodeURIComponent(
project
)}/repository/contributors`,
options: { searchParams: { page: '1', per_page: '1' } },
errorMessages: {
404: 'project not found',
},
})
const data = this.constructor._validate(res.headers, schema)
// The total number of contributors is in the `x-total` field in the headers.
// https://docs.gitlab.com/ee/api/index.html#other-pagination-headers

View File

@@ -1,9 +1,6 @@
import { createServiceTester } from '../tester.js'
import { isMetric } from '../test-validators.js'
import { noToken } from '../test-helpers.js'
import _noGitLabToken from './gitlab-contributors.service.js'
export const t = await createServiceTester()
const noGitLabToken = noToken(_noGitLabToken)
t.create('Contributors')
.get('/guoxudong.io/shields-test/licenced-test.json')
@@ -32,11 +29,3 @@ t.create('Mocking the missing x-total header')
label: 'contributors',
message: 'invalid response data',
})
t.create('Contributors (private repo)')
.skipWhen(noGitLabToken)
.get('/shields-ops-group/test.json')
.expectBadge({
label: 'contributors',
message: isMetric,
})

View File

@@ -1,286 +0,0 @@
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

@@ -1,147 +0,0 @@
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

@@ -1,9 +1,6 @@
import { licenseToColor } from '../licenses.js'
import { createServiceTester } from '../tester.js'
import { noToken } from '../test-helpers.js'
import _noGitLabToken from './gitlab-license.service.js'
export const t = await createServiceTester()
const noGitLabToken = noToken(_noGitLabToken)
const publicDomainLicenseColor = licenseToColor('MIT License')
const unknownLicenseColor = licenseToColor()
@@ -58,12 +55,3 @@ t.create('Mocking License')
message: 'Apache License 2.0',
color: unknownLicenseColor,
})
t.create('License (private repo)')
.skipWhen(noGitLabToken)
.get('/shields-ops-group/test.json')
.expectBadge({
label: 'license',
message: 'MIT License',
color: `${publicDomainLicenseColor}`,
})

View File

@@ -1,48 +0,0 @@
import { expect } from 'chai'
import nock from 'nock'
import { cleanUpNockAfterEach, defaultContext } from '../test-helpers.js'
import GitLabRelease from './gitlab-release.service.js'
describe('GitLabRelease', 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/releases?page=1')
// 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.
.matchHeader('Authorization', `Bearer ${fakeToken}`)
.reply(200, [{ name: '1.9', tag_name: '1.9' }])
expect(
await GitLabRelease.invoke(
defaultContext,
config,
{ project: 'foo/bar' },
{}
)
).to.deep.equal({
label: undefined,
message: 'v1.9',
color: 'blue',
})
scope.done()
})
})
})

View File

@@ -26,7 +26,7 @@ describe('GitLabTag', function () {
.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.
.matchHeader('Authorization', `Bearer ${fakeToken}`)
.basicAuth({ user: '', pass: fakeToken })
.reply(200, [{ name: '1.9' }])
expect(

View File

@@ -1,9 +1,3 @@
/**
* Common functions and utilities for tasks related to license badges.
*
* @module
*/
import toArray from '../core/base-service/to-array.js'
const licenseTypes = {
@@ -94,11 +88,6 @@ const licenseTypes = {
},
}
/**
* Mapping of licenses to their corresponding color and priority.
*
* @type {object}
*/
const licenseToColorMap = {}
Object.keys(licenseTypes).forEach(licenseType => {
const { spdxLicenseIds, aliases, color, priority } = licenseTypes[licenseType]
@@ -110,12 +99,6 @@ Object.keys(licenseTypes).forEach(licenseType => {
})
})
/**
* Maps the license to its corresponding color and priority and sorts the list of mapped licenses by priority.
*
* @param {string | string[]} licenses License or list of licenses
* @returns {string} Color corresponding to the license or the list of licenses
*/
function licenseToColor(licenses) {
if (!Array.isArray(licenses)) {
licenses = [licenses]
@@ -130,17 +113,6 @@ function licenseToColor(licenses) {
return color
}
/**
* Handles rendering concerns of license badges.
* Determines the message of the badge by joining the licenses in a comma-separated format.
* Sets the badge color to the provided value, if not provided then the color is used from licenseToColorMap.
*
* @param {object} attrs Refer to individual attributes
* @param {string} [attrs.license] License to render, required if badge contains only one license
* @param {string[]} [attrs.licenses] List of licenses to render, required if badge contains multiple licenses
* @param {string} [attrs.color] If provided then the badge will use this color value
* @returns {object} Badge with message and color properties
*/
function renderLicenseBadge({ license, licenses, color }) {
if (licenses === undefined) {
licenses = toArray(license)

View File

@@ -1,154 +0,0 @@
import gql from 'graphql-tag'
import Joi from 'joi'
import yaml from 'js-yaml'
import { renderVersionBadge } from '../version.js'
import { GithubAuthV4Service } from '../github/github-auth-service.js'
import { NotFound, InvalidResponse } from '../index.js'
const tagsSchema = Joi.object({
data: Joi.object({
repository: Joi.object({
refs: Joi.object({
edges: Joi.array()
.items({
node: Joi.object({
name: Joi.string().required(),
}).required(),
})
.required(),
}).required(),
}).required(),
}).required(),
}).required()
const contentSchema = Joi.object({
data: Joi.object({
repository: Joi.object({
object: Joi.object({
text: Joi.string().required(),
}).allow(null),
}).required(),
}).required(),
}).required()
const distroSchema = Joi.object({
repositories: Joi.object().required(),
})
const packageSchema = Joi.object({
release: Joi.object({
version: Joi.string().required(),
}).required(),
})
export default class RosVersion extends GithubAuthV4Service {
static category = 'version'
static route = { base: 'ros/v', pattern: ':distro/:packageName' }
static examples = [
{
title: 'ROS Package Index',
namedParams: { distro: 'humble', packageName: 'vision_msgs' },
staticPreview: {
...renderVersionBadge({ version: '4.0.0' }),
label: 'ros | humble',
},
},
]
static defaultBadgeData = { label: 'ros' }
async handle({ distro, packageName }) {
const tagsJson = await this._requestGraphql({
query: gql`
query ($refPrefix: String!) {
repository(owner: "ros", name: "rosdistro") {
refs(
refPrefix: $refPrefix
first: 30
orderBy: { field: TAG_COMMIT_DATE, direction: DESC }
) {
edges {
node {
name
}
}
}
}
}
`,
variables: { refPrefix: `refs/tags/${distro}/` },
schema: tagsSchema,
})
// Filter for tags that look like dates: humble/2022-06-10
const tags = tagsJson.data.repository.refs.edges
.map(edge => edge.node.name)
.filter(tag => /^\d+-\d+-\d+$/.test(tag))
.sort()
.reverse()
const ref = tags[0] ? `refs/tags/${distro}/${tags[0]}` : 'refs/heads/master'
const prettyRef = tags[0] ? `${distro}/${tags[0]}` : 'master'
const contentJson = await this._requestGraphql({
query: gql`
query ($expression: String!) {
repository(owner: "ros", name: "rosdistro") {
object(expression: $expression) {
... on Blob {
text
}
}
}
}
`,
variables: {
expression: `${ref}:${distro}/distribution.yaml`,
},
schema: contentSchema,
})
if (!contentJson.data.repository.object) {
throw new NotFound({
prettyMessage: `distribution.yaml not found: ${distro}@${prettyRef}`,
})
}
const version = this.constructor._parseReleaseVersionFromDistro(
contentJson.data.repository.object.text,
packageName
)
return { ...renderVersionBadge({ version }), label: `ros | ${distro}` }
}
static _parseReleaseVersionFromDistro(distroYaml, packageName) {
let distro
try {
distro = yaml.load(distroYaml)
} catch (err) {
throw new InvalidResponse({
prettyMessage: 'unparseable distribution.yml',
underlyingError: err,
})
}
const validatedDistro = this._validate(distro, distroSchema, {
prettyErrorMessage: 'invalid distribution.yml',
})
if (!validatedDistro.repositories[packageName]) {
throw new NotFound({ prettyMessage: `package not found: ${packageName}` })
}
const packageInfo = this._validate(
validatedDistro.repositories[packageName],
packageSchema,
{
prettyErrorMessage: `invalid section for ${packageName} in distribution.yml`,
}
)
// Strip off "release inc" suffix
return packageInfo.release.version.replace(/-\d+$/, '')
}
}

View File

@@ -1,44 +0,0 @@
import { expect } from 'chai'
import RosVersion from './ros-version.service.js'
describe('parseReleaseVersionFromDistro', function () {
it('returns correct version', function () {
expect(
RosVersion._parseReleaseVersionFromDistro(
`
%YAML 1.1
# ROS distribution file
# see REP 143: http://ros.org/reps/rep-0143.html
---
release_platforms:
debian:
- bullseye
rhel:
- '8'
ubuntu:
- jammy
repositories:
vision_msgs:
doc:
type: git
url: https://github.com/ros-perception/vision_msgs.git
version: ros2
release:
tags:
release: release/humble/{package}/{version}
url: https://github.com/ros2-gbp/vision_msgs-release.git
version: 4.0.0-2
source:
test_pull_requests: true
type: git
url: https://github.com/ros-perception/vision_msgs.git
version: ros2
status: developed
type: distribution
version: 2
`,
'vision_msgs'
)
).to.equal('4.0.0')
})
})

View File

@@ -1,28 +0,0 @@
import { isSemver } from '../test-validators.js'
import { createServiceTester } from '../tester.js'
export const t = await createServiceTester()
t.create('gets the package version of vision_msgs in active distro')
.get('/humble/vision_msgs.json')
.expectBadge({ label: 'ros | humble', message: isSemver })
t.create('gets the package version of vision_msgs in EOL distro')
.get('/lunar/vision_msgs.json')
.expectBadge({ label: 'ros | lunar', message: isSemver })
t.create('returns not found for invalid package')
.get('/humble/this package does not exist - ros test.json')
.expectBadge({
label: 'ros',
color: 'red',
message: 'package not found: this package does not exist - ros test',
})
t.create('returns error for invalid distro')
.get('/xxxxxx/vision_msgs.json')
.expectBadge({
label: 'ros',
color: 'red',
message: 'distribution.yaml not found: xxxxxx@master',
})

View File

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