Compare commits
86 Commits
server-202
...
test202304
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae1f3c3710 | ||
|
|
148b51d554 | ||
|
|
ba4a5619ec | ||
|
|
95ef751096 | ||
|
|
7cb0b67af5 | ||
|
|
54a66ef160 | ||
|
|
f0c3a5c363 | ||
|
|
8d0ed2c8c0 | ||
|
|
9c4e5682f3 | ||
|
|
b22c554bc8 | ||
|
|
74c7b062dd | ||
|
|
f5876c3ec4 | ||
|
|
3bf2246788 | ||
|
|
0d8f35b4ef | ||
|
|
9d57b61811 | ||
|
|
3cb76e78be | ||
|
|
138fdbee81 | ||
|
|
2cb5363c84 | ||
|
|
d248e9c892 | ||
|
|
0c77a064bd | ||
|
|
b5d07f5a96 | ||
|
|
d5a74d5d21 | ||
|
|
6deeadcfe1 | ||
|
|
82dddab90f | ||
|
|
0e0ac8fe60 | ||
|
|
4b847b98c2 | ||
|
|
793b2480e5 | ||
|
|
029b141726 | ||
|
|
009bc74109 | ||
|
|
63aa80db96 | ||
|
|
0356aba535 | ||
|
|
18216e8baa | ||
|
|
af664e8d7f | ||
|
|
e35f2ee1ad | ||
|
|
d242d8045b | ||
|
|
732f783b22 | ||
|
|
ca923b2c12 | ||
|
|
b0f8416dd0 | ||
|
|
0556f02fae | ||
|
|
19ec4a3c92 | ||
|
|
aafb26b8f7 | ||
|
|
c1ff413c57 | ||
|
|
bc12b1abc8 | ||
|
|
d3256100d5 | ||
|
|
c826c7017b | ||
|
|
73d5b95fbd | ||
|
|
a284857e86 | ||
|
|
38cf3ecc42 | ||
|
|
540bf96a6b | ||
|
|
5b44341e1b | ||
|
|
0dcfa76c53 | ||
|
|
0be37ada2d | ||
|
|
e03cd54bd6 | ||
|
|
37756e3d06 | ||
|
|
527c9cf561 | ||
|
|
576b0b582f | ||
|
|
46cab5f57b | ||
|
|
de39ee7aae | ||
|
|
ad25649d07 | ||
|
|
c88a822ce4 | ||
|
|
7c79eb1417 | ||
|
|
fc4ef6326a | ||
|
|
e8e220b8cf | ||
|
|
2fe650fbec | ||
|
|
6cbd4cb253 | ||
|
|
459019803c | ||
|
|
bcde955e30 | ||
|
|
eb969fd442 | ||
|
|
2856c0e1a3 | ||
|
|
fab3fd7a93 | ||
|
|
a5480d5a8c | ||
|
|
4a5bf538ff | ||
|
|
f55a655203 | ||
|
|
02c7486fed | ||
|
|
ab59b6b4fc | ||
|
|
227abb4d83 | ||
|
|
ab51c88d94 | ||
|
|
b9ef8c6003 | ||
|
|
b746a1d2ef | ||
|
|
565139660d | ||
|
|
549731421a | ||
|
|
deb0debc17 | ||
|
|
ba34ebac67 | ||
|
|
60ab3db855 | ||
|
|
e00f0c33bc | ||
|
|
26bf69cfe7 |
2
.github/workflows/build-docker-image.yml
vendored
2
.github/workflows/build-docker-image.yml
vendored
@@ -22,6 +22,6 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
tags: shieldsio/shields:pr-validation
|
||||
tags: ghcr.io/badges/shields:pr-validation
|
||||
build-args: |
|
||||
version=${{ env.SHORT_SHA }}
|
||||
|
||||
17
.github/workflows/create-release.yml
vendored
17
.github/workflows/create-release.yml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
@@ -52,3 +53,19 @@ jobs:
|
||||
tags: shieldsio/shields:server-${{ steps.date.outputs.date }}
|
||||
build-args: |
|
||||
version=server-${{ steps.date.outputs.date }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push snapshot release to GHCR
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ghcr.io/badges/shields:server-${{ steps.date.outputs.date }}
|
||||
build-args: |
|
||||
version=server-${{ steps.date.outputs.date }}
|
||||
|
||||
21
.github/workflows/publish-docker-next.yml
vendored
21
.github/workflows/publish-docker-next.yml
vendored
@@ -4,6 +4,9 @@ on:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
publish-docker-next:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -25,7 +28,7 @@ jobs:
|
||||
- name: Set Git Short SHA
|
||||
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push
|
||||
- name: Build and push to DockerHub
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
@@ -33,3 +36,19 @@ jobs:
|
||||
tags: shieldsio/shields:next
|
||||
build-args: |
|
||||
version=${{ env.SHORT_SHA }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push to GHCR
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ghcr.io/badges/shields:next
|
||||
build-args: |
|
||||
version=${{ env.SHORT_SHA }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -96,6 +96,7 @@ typings/
|
||||
badge-examples.json
|
||||
supported-features.json
|
||||
service-definitions.yml
|
||||
frontend/categories/*.yaml
|
||||
|
||||
# Local runtime configuration.
|
||||
/config/local*.yml
|
||||
|
||||
12
CHANGELOG.md
12
CHANGELOG.md
@@ -4,6 +4,18 @@ Note: this changelog is for the shields.io server. The changelog for the badge-m
|
||||
|
||||
---
|
||||
|
||||
## server-2023-04-02
|
||||
|
||||
- [JenkinsCoverage] Update Jenkins Code Coverage API for new plugin version [#9010](https://github.com/badges/shields/issues/9010)
|
||||
- [CTAN] fallback to date if version is empty [#9036](https://github.com/badges/shields/issues/9036)
|
||||
- Update to [CTAN] API version 2.0 [#9016](https://github.com/badges/shields/issues/9016)
|
||||
- handle missing statistics array in [VisualStudioMarketplace] badges [#8985](https://github.com/badges/shields/issues/8985)
|
||||
- [Netlify] upgrade colors for SVG parsing [#8971](https://github.com/badges/shields/issues/8971)
|
||||
- Fix [Vcpkg] version service for different version fields [#8945](https://github.com/badges/shields/issues/8945)
|
||||
- only try to close pool if one exists [#8947](https://github.com/badges/shields/issues/8947)
|
||||
- misc minor fixes to [githubsize node pypi] [#8946](https://github.com/badges/shields/issues/8946)
|
||||
- Dependency updates
|
||||
|
||||
## server-2023-03-01
|
||||
|
||||
**Deprecation:** For users who need to maintain a Github Token pool, storage has been provided via the `RedisTokenPersistence` and `REDIS_URL` settings. As of this release, the `RedisTokenPersistence` backend is now deprecated and will be removed in a future release. If you are using this feature, you will need to migrate to using the `SQLTokenPersistence` backend for storage and provide a postgres connection string via the `POSTGRES_URL` setting. [#8922](https://github.com/badges/shields/issues/8922)
|
||||
|
||||
@@ -134,9 +134,20 @@ Prettier before a commit by default.
|
||||
When adding or changing a service [please write tests][service-tests], and ensure the [title of your Pull Requests follows the required conventions](#running-service-tests-in-pull-requests) to ensure your tests are executed.
|
||||
When changing other code, please add unit tests.
|
||||
|
||||
To run the integration tests, you must have Redis installed and in your PATH.
|
||||
Use `brew install redis`, `yum install redis`, etc. The test runner will
|
||||
start the server automatically.
|
||||
The integration tests are not run by default. For most contributions it is OK to skip these unless you're working directly on the code for storing the GitHub token pool in postgres/redis.
|
||||
|
||||
To run the integration tests:
|
||||
|
||||
- You must have Redis installed and in your PATH. Use `brew install redis`, `apt-get install redis`, etc. The test runner will start the server automatically.
|
||||
- You must also have PostgreSQL installed. Use `brew install postgresql`, `apt-get install postgresql`, etc.
|
||||
- Set a connection string either with an env var `POSTGRES_URL=postgresql://user:pass@127.0.0.1:5432/db_name` or by using
|
||||
```yaml
|
||||
private:
|
||||
postgres_url: 'postgresql://user:pass@127.0.0.1:5432/db_name'
|
||||
```
|
||||
in a yaml config file.
|
||||
- Run `npm run migrate up` to apply DB migrations
|
||||
- Run `npm run test:integration` to run the tests
|
||||
|
||||
[service-tests]: https://github.com/badges/shields/blob/master/doc/service-tests.md
|
||||
|
||||
|
||||
@@ -140,6 +140,15 @@ class BaseService {
|
||||
*/
|
||||
static examples = []
|
||||
|
||||
/**
|
||||
* Optional: an OpenAPI Paths Object describing this service's
|
||||
* route or routes in OpenAPI format.
|
||||
*
|
||||
* @see https://swagger.io/specification/#paths-object
|
||||
* @abstract
|
||||
*/
|
||||
static openApi = undefined
|
||||
|
||||
static get _cacheLength() {
|
||||
const cacheLengths = {
|
||||
build: 30,
|
||||
@@ -183,7 +192,7 @@ class BaseService {
|
||||
}
|
||||
|
||||
static getDefinition() {
|
||||
const { category, name, isDeprecated } = this
|
||||
const { category, name, isDeprecated, openApi } = this
|
||||
const { base, format, pattern } = this.route
|
||||
const queryParams = getQueryParamNames(this.route)
|
||||
|
||||
@@ -200,7 +209,7 @@ class BaseService {
|
||||
route = undefined
|
||||
}
|
||||
|
||||
const result = { category, name, isDeprecated, route, examples }
|
||||
const result = { category, name, isDeprecated, route, examples, openApi }
|
||||
|
||||
assertValidServiceDefinition(result, `getDefinition() for ${this.name}`)
|
||||
|
||||
|
||||
@@ -129,6 +129,7 @@ function transformExample(inExample, index, ServiceClass) {
|
||||
ServiceClass
|
||||
)
|
||||
|
||||
const category = categories.find(c => c.id === ServiceClass.category)
|
||||
return {
|
||||
title,
|
||||
example: {
|
||||
@@ -146,9 +147,7 @@ function transformExample(inExample, index, ServiceClass) {
|
||||
style: style === 'flat' ? undefined : style,
|
||||
namedLogo,
|
||||
},
|
||||
keywords: keywords.concat(
|
||||
categories.find(c => c.id === ServiceClass.category).keywords
|
||||
),
|
||||
keywords: category ? keywords.concat(category.keywords) : keywords,
|
||||
documentation: documentation ? { __html: documentation } : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import glob from 'glob'
|
||||
import { globSync } from 'glob'
|
||||
import countBy from 'lodash.countby'
|
||||
import categories from '../../services/categories.js'
|
||||
import BaseService from './base.js'
|
||||
@@ -28,7 +28,7 @@ class InvalidService extends Error {
|
||||
}
|
||||
|
||||
function getServicePaths(pattern) {
|
||||
return glob.sync(toUnixPath(path.join(serviceDir, '**', pattern)))
|
||||
return globSync(toUnixPath(path.join(serviceDir, '**', pattern))).sort()
|
||||
}
|
||||
|
||||
async function loadServiceClasses(servicePaths) {
|
||||
@@ -53,8 +53,8 @@ async function loadServiceClasses(servicePaths) {
|
||||
if (serviceClass && serviceClass.prototype instanceof BaseService) {
|
||||
// Decorate each service class with the directory that contains it.
|
||||
serviceClass.serviceFamily = servicePath
|
||||
.replace(toUnixPath(serviceDir), '')
|
||||
.split('/')[1]
|
||||
.replace(serviceDir, '')
|
||||
.split(path.sep)[1]
|
||||
serviceClass.validateDefinition()
|
||||
return serviceClasses.push(serviceClass)
|
||||
}
|
||||
|
||||
335
core/base-service/openapi.js
Normal file
335
core/base-service/openapi.js
Normal file
@@ -0,0 +1,335 @@
|
||||
const baseUrl = process.env.BASE_URL || 'https://img.shields.io'
|
||||
const globalParamRefs = [
|
||||
{ $ref: '#/components/parameters/style' },
|
||||
{ $ref: '#/components/parameters/logo' },
|
||||
{ $ref: '#/components/parameters/logoColor' },
|
||||
{ $ref: '#/components/parameters/label' },
|
||||
{ $ref: '#/components/parameters/labelColor' },
|
||||
{ $ref: '#/components/parameters/color' },
|
||||
{ $ref: '#/components/parameters/cacheSeconds' },
|
||||
{ $ref: '#/components/parameters/link' },
|
||||
]
|
||||
|
||||
function getCodeSamples(altText) {
|
||||
return [
|
||||
{
|
||||
lang: 'URL',
|
||||
label: 'URL',
|
||||
source: '$url',
|
||||
},
|
||||
{
|
||||
lang: 'Markdown',
|
||||
label: 'Markdown',
|
||||
source: ``,
|
||||
},
|
||||
{
|
||||
lang: 'reStructuredText',
|
||||
label: 'rSt',
|
||||
source: `.. image:: $url\n: alt: ${altText}`,
|
||||
},
|
||||
{
|
||||
lang: 'AsciiDoc',
|
||||
label: 'AsciiDoc',
|
||||
source: `image:$url[${altText}]`,
|
||||
},
|
||||
{
|
||||
lang: 'HTML',
|
||||
label: 'HTML',
|
||||
source: `<img alt="${altText}" src="$url">`,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function pattern2openapi(pattern) {
|
||||
return pattern
|
||||
.replace(/:([A-Za-z0-9_\-.]+)(?=[/]?)/g, (matches, grp1) => `{${grp1}}`)
|
||||
.replace(/\([^)]*\)/g, '')
|
||||
.replace(/\+$/, '')
|
||||
}
|
||||
|
||||
function getEnum(pattern, paramName) {
|
||||
const re = new RegExp(`${paramName}\\(([A-Za-z0-9_\\-|]+)\\)`)
|
||||
const match = pattern.match(re)
|
||||
if (match === null) {
|
||||
return undefined
|
||||
}
|
||||
if (!match[1].includes('|')) {
|
||||
return undefined
|
||||
}
|
||||
return match[1].split('|')
|
||||
}
|
||||
|
||||
function param2openapi(pattern, paramName, exampleValue, paramType) {
|
||||
const outParam = {}
|
||||
outParam.name = paramName
|
||||
// We don't have description if we are building the OpenAPI spec from examples[]
|
||||
outParam.in = paramType
|
||||
if (paramType === 'path') {
|
||||
outParam.required = true
|
||||
} else {
|
||||
/* Occasionally we do have required query params, but we can't
|
||||
detect this if we are building the OpenAPI spec from examples[]
|
||||
so just assume all query params are optional */
|
||||
outParam.required = false
|
||||
}
|
||||
|
||||
if (exampleValue === null && paramType === 'query') {
|
||||
outParam.schema = { type: 'boolean' }
|
||||
outParam.allowEmptyValue = true
|
||||
} else {
|
||||
outParam.schema = { type: 'string' }
|
||||
}
|
||||
|
||||
if (paramType === 'path') {
|
||||
outParam.schema.enum = getEnum(pattern, paramName)
|
||||
}
|
||||
|
||||
outParam.example = exampleValue
|
||||
return outParam
|
||||
}
|
||||
|
||||
function getVariants(pattern) {
|
||||
/*
|
||||
given a URL pattern (which may include '/one/or/:more?/:optional/:parameters*')
|
||||
return an array of all possible permutations:
|
||||
[
|
||||
'/one/or/:more/:optional/:parameters',
|
||||
'/one/or/:optional/:parameters',
|
||||
'/one/or/:more/:optional',
|
||||
'/one/or/:optional',
|
||||
]
|
||||
*/
|
||||
const patterns = [pattern.split('/')]
|
||||
while (patterns.flat().find(p => p.endsWith('?') || p.endsWith('*'))) {
|
||||
for (let i = 0; i < patterns.length; i++) {
|
||||
const pattern = patterns[i]
|
||||
for (let j = 0; j < pattern.length; j++) {
|
||||
const path = pattern[j]
|
||||
if (path.endsWith('?') || path.endsWith('*')) {
|
||||
pattern[j] = path.slice(0, -1)
|
||||
patterns.push(patterns[i].filter(p => p !== pattern[j]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < patterns.length; i++) {
|
||||
patterns[i] = patterns[i].join('/')
|
||||
}
|
||||
return patterns
|
||||
}
|
||||
|
||||
function examples2openapi(examples) {
|
||||
const paths = {}
|
||||
for (const example of examples) {
|
||||
const patterns = getVariants(example.example.pattern)
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const openApiPattern = pattern2openapi(pattern)
|
||||
if (
|
||||
openApiPattern.includes('*') ||
|
||||
openApiPattern.includes('?') ||
|
||||
openApiPattern.includes('+') ||
|
||||
openApiPattern.includes('(')
|
||||
) {
|
||||
throw new Error(`unexpected characters in pattern '${openApiPattern}'`)
|
||||
}
|
||||
|
||||
/*
|
||||
There's several things going on in this block:
|
||||
1. Filter out any examples for params that don't appear
|
||||
in this variant of the route
|
||||
2. Make sure we add params to the array
|
||||
in the same order they appear in the route
|
||||
3. If there are any params we don't have an example value for,
|
||||
make sure they still appear in the pathParams array with
|
||||
exampleValue == undefined anyway
|
||||
*/
|
||||
const pathParams = []
|
||||
for (const param of openApiPattern
|
||||
.split('/')
|
||||
.filter(p => p.startsWith('{') && p.endsWith('}'))) {
|
||||
const paramName = param.slice(1, -1)
|
||||
const exampleValue = example.example.namedParams[paramName]
|
||||
pathParams.push(param2openapi(pattern, paramName, exampleValue, 'path'))
|
||||
}
|
||||
|
||||
const queryParams = example.example.queryParams || {}
|
||||
|
||||
const parameters = [
|
||||
...pathParams,
|
||||
...Object.entries(queryParams).map(([paramName, exampleValue]) =>
|
||||
param2openapi(pattern, paramName, exampleValue, 'query')
|
||||
),
|
||||
...globalParamRefs,
|
||||
]
|
||||
paths[openApiPattern] = {
|
||||
get: {
|
||||
summary: example.title,
|
||||
description: example?.documentation?.__html
|
||||
.replace(/<br>/g, '<br />') // react does not like <br>
|
||||
.replace(/{/g, '{')
|
||||
.replace(/}/g, '}')
|
||||
.replace(/<style>(.|\n)*?<\/style>/, ''), // workaround for w3c-validation TODO: remove later
|
||||
parameters,
|
||||
'x-code-samples': getCodeSamples(example.title),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
function addGlobalProperties(endpoints) {
|
||||
const paths = {}
|
||||
for (const key of Object.keys(endpoints)) {
|
||||
paths[key] = endpoints[key]
|
||||
paths[key].get.parameters = [
|
||||
...paths[key].get.parameters,
|
||||
...globalParamRefs,
|
||||
]
|
||||
paths[key].get['x-code-samples'] = getCodeSamples(paths[key].get.summary)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
function services2openapi(services) {
|
||||
const paths = {}
|
||||
for (const service of services) {
|
||||
if (service.openApi) {
|
||||
// if the service declares its own OpenAPI definition, use that...
|
||||
for (const [key, value] of Object.entries(
|
||||
addGlobalProperties(service.openApi)
|
||||
)) {
|
||||
if (key in paths) {
|
||||
throw new Error(`Conflicting route: ${key}`)
|
||||
}
|
||||
paths[key] = value
|
||||
}
|
||||
} else {
|
||||
// ...otherwise do our best to build one from examples[]
|
||||
for (const [key, value] of Object.entries(
|
||||
examples2openapi(service.examples)
|
||||
)) {
|
||||
// allow conflicting routes for legacy examples
|
||||
paths[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
function category2openapi(category, services) {
|
||||
const spec = {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
version: '1.0.0',
|
||||
title: category.name,
|
||||
license: {
|
||||
name: 'CC0',
|
||||
},
|
||||
},
|
||||
servers: [{ url: baseUrl }],
|
||||
components: {
|
||||
parameters: {
|
||||
style: {
|
||||
name: 'style',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'One of: flat (default), flat-square, plastic, for-the-badge, social',
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
example: 'flat',
|
||||
},
|
||||
logo: {
|
||||
name: 'logo',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'One of the named logos (bitcoin, dependabot, gitlab, npm, paypal, serverfault, stackexchange, superuser, telegram, travis) or simple-icons. All simple-icons are referenced using icon slugs. You can click the icon title on <a href="https://simpleicons.org/" rel="noopener noreferrer" target="_blank">simple-icons</a> to copy the slug or they can be found in the <a href="https://github.com/simple-icons/simple-icons/blob/master/slugs.md">slugs.md file</a> in the simple-icons repository.',
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
example: 'appveyor',
|
||||
},
|
||||
logoColor: {
|
||||
name: 'logoColor',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'The color of the logo (hex, rgb, rgba, hsl, hsla and css named colors supported). Supported for named logos and Shields logos but not for custom logos. For multicolor Shields logos, the corresponding named logo will be used and colored.',
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
example: 'violet',
|
||||
},
|
||||
label: {
|
||||
name: 'label',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'Override the default left-hand-side text (<a href="https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding">URL-Encoding</a> needed for spaces or special characters!)',
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
example: 'healthiness',
|
||||
},
|
||||
labelColor: {
|
||||
name: 'labelColor',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'Background color of the left part (hex, rgb, rgba, hsl, hsla and css named colors supported).',
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
example: 'abcdef',
|
||||
},
|
||||
color: {
|
||||
name: 'color',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'Background color of the right part (hex, rgb, rgba, hsl, hsla and css named colors supported).',
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
example: 'fedcba',
|
||||
},
|
||||
cacheSeconds: {
|
||||
name: 'cacheSeconds',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'HTTP cache lifetime (rules are applied to infer a default value on a per-badge basis, any values specified below the default will be ignored).',
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
example: '3600',
|
||||
},
|
||||
link: {
|
||||
name: 'link',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'Specify what clicking on the left/right of a badge should do. Note that this only works when integrating your badge in an `<object>` HTML tag, but not an `<img>` tag or a markup language.',
|
||||
style: 'form',
|
||||
explode: true,
|
||||
schema: {
|
||||
type: 'array',
|
||||
maxItems: 2,
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
paths: services2openapi(services),
|
||||
}
|
||||
|
||||
return spec
|
||||
}
|
||||
|
||||
export { category2openapi }
|
||||
379
core/base-service/openapi.spec.js
Normal file
379
core/base-service/openapi.spec.js
Normal file
@@ -0,0 +1,379 @@
|
||||
import chai from 'chai'
|
||||
import { category2openapi } from './openapi.js'
|
||||
import BaseJsonService from './base-json.js'
|
||||
const { expect } = chai
|
||||
|
||||
class OpenApiService extends BaseJsonService {
|
||||
static category = 'build'
|
||||
static route = { base: 'openapi/service', pattern: ':packageName/:distTag*' }
|
||||
|
||||
// this service defines its own API Paths Object
|
||||
static openApi = {
|
||||
'/openapi/service/{packageName}': {
|
||||
get: {
|
||||
summary: 'OpenApiService Summary',
|
||||
description: 'OpenApiService Description',
|
||||
parameters: [
|
||||
{
|
||||
name: 'packageName',
|
||||
description: 'packageName description',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'badge-maker',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'/openapi/service/{packageName}/{distTag}': {
|
||||
get: {
|
||||
summary: 'OpenApiService Summary (with Tag)',
|
||||
description: 'OpenApiService Description (with Tag)',
|
||||
parameters: [
|
||||
{
|
||||
name: 'packageName',
|
||||
description: 'packageName description',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'badge-maker',
|
||||
},
|
||||
{
|
||||
name: 'distTag',
|
||||
description: 'distTag description',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'latest',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
class LegacyService extends BaseJsonService {
|
||||
static category = 'build'
|
||||
static route = { base: 'legacy/service', pattern: ':packageName/:distTag*' }
|
||||
|
||||
// this service defines an Examples Array
|
||||
static examples = [
|
||||
{
|
||||
title: 'LegacyService Title',
|
||||
namedParams: { packageName: 'badge-maker' },
|
||||
staticPreview: { label: 'build', message: 'passing' },
|
||||
documentation: 'LegacyService Description',
|
||||
},
|
||||
{
|
||||
title: 'LegacyService Title (with Tag)',
|
||||
namedParams: { packageName: 'badge-maker', distTag: 'latest' },
|
||||
staticPreview: { label: 'build', message: 'passing' },
|
||||
documentation: 'LegacyService Description (with Tag)',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const expected = {
|
||||
openapi: '3.0.0',
|
||||
info: { version: '1.0.0', title: 'build', license: { name: 'CC0' } },
|
||||
servers: [{ url: 'https://img.shields.io' }],
|
||||
components: {
|
||||
parameters: {
|
||||
style: {
|
||||
name: 'style',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'One of: flat (default), flat-square, plastic, for-the-badge, social',
|
||||
schema: { type: 'string' },
|
||||
example: 'flat',
|
||||
},
|
||||
logo: {
|
||||
name: 'logo',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'One of the named logos (bitcoin, dependabot, gitlab, npm, paypal, serverfault, stackexchange, superuser, telegram, travis) or simple-icons. All simple-icons are referenced using icon slugs. You can click the icon title on <a href="https://simpleicons.org/" rel="noopener noreferrer" target="_blank">simple-icons</a> to copy the slug or they can be found in the <a href="https://github.com/simple-icons/simple-icons/blob/master/slugs.md">slugs.md file</a> in the simple-icons repository.',
|
||||
schema: { type: 'string' },
|
||||
example: 'appveyor',
|
||||
},
|
||||
logoColor: {
|
||||
name: 'logoColor',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'The color of the logo (hex, rgb, rgba, hsl, hsla and css named colors supported). Supported for named logos and Shields logos but not for custom logos. For multicolor Shields logos, the corresponding named logo will be used and colored.',
|
||||
schema: { type: 'string' },
|
||||
example: 'violet',
|
||||
},
|
||||
label: {
|
||||
name: 'label',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'Override the default left-hand-side text (<a href="https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding">URL-Encoding</a> needed for spaces or special characters!)',
|
||||
schema: { type: 'string' },
|
||||
example: 'healthiness',
|
||||
},
|
||||
labelColor: {
|
||||
name: 'labelColor',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'Background color of the left part (hex, rgb, rgba, hsl, hsla and css named colors supported).',
|
||||
schema: { type: 'string' },
|
||||
example: 'abcdef',
|
||||
},
|
||||
color: {
|
||||
name: 'color',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'Background color of the right part (hex, rgb, rgba, hsl, hsla and css named colors supported).',
|
||||
schema: { type: 'string' },
|
||||
example: 'fedcba',
|
||||
},
|
||||
cacheSeconds: {
|
||||
name: 'cacheSeconds',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'HTTP cache lifetime (rules are applied to infer a default value on a per-badge basis, any values specified below the default will be ignored).',
|
||||
schema: { type: 'string' },
|
||||
example: '3600',
|
||||
},
|
||||
link: {
|
||||
name: 'link',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'Specify what clicking on the left/right of a badge should do. Note that this only works when integrating your badge in an `<object>` HTML tag, but not an `<img>` tag or a markup language.',
|
||||
style: 'form',
|
||||
explode: true,
|
||||
schema: { type: 'array', maxItems: 2, items: { type: 'string' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
paths: {
|
||||
'/openapi/service/{packageName}': {
|
||||
get: {
|
||||
summary: 'OpenApiService Summary',
|
||||
description: 'OpenApiService Description',
|
||||
parameters: [
|
||||
{
|
||||
name: 'packageName',
|
||||
description: 'packageName description',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'badge-maker',
|
||||
},
|
||||
{ $ref: '#/components/parameters/style' },
|
||||
{ $ref: '#/components/parameters/logo' },
|
||||
{ $ref: '#/components/parameters/logoColor' },
|
||||
{ $ref: '#/components/parameters/label' },
|
||||
{ $ref: '#/components/parameters/labelColor' },
|
||||
{ $ref: '#/components/parameters/color' },
|
||||
{ $ref: '#/components/parameters/cacheSeconds' },
|
||||
{ $ref: '#/components/parameters/link' },
|
||||
],
|
||||
'x-code-samples': [
|
||||
{ lang: 'URL', label: 'URL', source: '$url' },
|
||||
{
|
||||
lang: 'Markdown',
|
||||
label: 'Markdown',
|
||||
source: '',
|
||||
},
|
||||
{
|
||||
lang: 'reStructuredText',
|
||||
label: 'rSt',
|
||||
source: '.. image:: $url\n: alt: OpenApiService Summary',
|
||||
},
|
||||
{
|
||||
lang: 'AsciiDoc',
|
||||
label: 'AsciiDoc',
|
||||
source: 'image:$url[OpenApiService Summary]',
|
||||
},
|
||||
{
|
||||
lang: 'HTML',
|
||||
label: 'HTML',
|
||||
source: '<img alt="OpenApiService Summary" src="$url">',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'/openapi/service/{packageName}/{distTag}': {
|
||||
get: {
|
||||
summary: 'OpenApiService Summary (with Tag)',
|
||||
description: 'OpenApiService Description (with Tag)',
|
||||
parameters: [
|
||||
{
|
||||
name: 'packageName',
|
||||
description: 'packageName description',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'badge-maker',
|
||||
},
|
||||
{
|
||||
name: 'distTag',
|
||||
description: 'distTag description',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'latest',
|
||||
},
|
||||
{ $ref: '#/components/parameters/style' },
|
||||
{ $ref: '#/components/parameters/logo' },
|
||||
{ $ref: '#/components/parameters/logoColor' },
|
||||
{ $ref: '#/components/parameters/label' },
|
||||
{ $ref: '#/components/parameters/labelColor' },
|
||||
{ $ref: '#/components/parameters/color' },
|
||||
{ $ref: '#/components/parameters/cacheSeconds' },
|
||||
{ $ref: '#/components/parameters/link' },
|
||||
],
|
||||
'x-code-samples': [
|
||||
{ lang: 'URL', label: 'URL', source: '$url' },
|
||||
{
|
||||
lang: 'Markdown',
|
||||
label: 'Markdown',
|
||||
source: '',
|
||||
},
|
||||
{
|
||||
lang: 'reStructuredText',
|
||||
label: 'rSt',
|
||||
source:
|
||||
'.. image:: $url\n: alt: OpenApiService Summary (with Tag)',
|
||||
},
|
||||
{
|
||||
lang: 'AsciiDoc',
|
||||
label: 'AsciiDoc',
|
||||
source: 'image:$url[OpenApiService Summary (with Tag)]',
|
||||
},
|
||||
{
|
||||
lang: 'HTML',
|
||||
label: 'HTML',
|
||||
source: '<img alt="OpenApiService Summary (with Tag)" src="$url">',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'/legacy/service/{packageName}/{distTag}': {
|
||||
get: {
|
||||
summary: 'LegacyService Title (with Tag)',
|
||||
description: 'LegacyService Description (with Tag)',
|
||||
parameters: [
|
||||
{
|
||||
name: 'packageName',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'badge-maker',
|
||||
},
|
||||
{
|
||||
name: 'distTag',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'latest',
|
||||
},
|
||||
{ $ref: '#/components/parameters/style' },
|
||||
{ $ref: '#/components/parameters/logo' },
|
||||
{ $ref: '#/components/parameters/logoColor' },
|
||||
{ $ref: '#/components/parameters/label' },
|
||||
{ $ref: '#/components/parameters/labelColor' },
|
||||
{ $ref: '#/components/parameters/color' },
|
||||
{ $ref: '#/components/parameters/cacheSeconds' },
|
||||
{ $ref: '#/components/parameters/link' },
|
||||
],
|
||||
'x-code-samples': [
|
||||
{ lang: 'URL', label: 'URL', source: '$url' },
|
||||
{
|
||||
lang: 'Markdown',
|
||||
label: 'Markdown',
|
||||
source: '',
|
||||
},
|
||||
{
|
||||
lang: 'reStructuredText',
|
||||
label: 'rSt',
|
||||
source: '.. image:: $url\n: alt: LegacyService Title (with Tag)',
|
||||
},
|
||||
{
|
||||
lang: 'AsciiDoc',
|
||||
label: 'AsciiDoc',
|
||||
source: 'image:$url[LegacyService Title (with Tag)]',
|
||||
},
|
||||
{
|
||||
lang: 'HTML',
|
||||
label: 'HTML',
|
||||
source: '<img alt="LegacyService Title (with Tag)" src="$url">',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'/legacy/service/{packageName}': {
|
||||
get: {
|
||||
summary: 'LegacyService Title (with Tag)',
|
||||
description: 'LegacyService Description (with Tag)',
|
||||
parameters: [
|
||||
{
|
||||
name: 'packageName',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'badge-maker',
|
||||
},
|
||||
{ $ref: '#/components/parameters/style' },
|
||||
{ $ref: '#/components/parameters/logo' },
|
||||
{ $ref: '#/components/parameters/logoColor' },
|
||||
{ $ref: '#/components/parameters/label' },
|
||||
{ $ref: '#/components/parameters/labelColor' },
|
||||
{ $ref: '#/components/parameters/color' },
|
||||
{ $ref: '#/components/parameters/cacheSeconds' },
|
||||
{ $ref: '#/components/parameters/link' },
|
||||
],
|
||||
'x-code-samples': [
|
||||
{ lang: 'URL', label: 'URL', source: '$url' },
|
||||
{
|
||||
lang: 'Markdown',
|
||||
label: 'Markdown',
|
||||
source: '',
|
||||
},
|
||||
{
|
||||
lang: 'reStructuredText',
|
||||
label: 'rSt',
|
||||
source: '.. image:: $url\n: alt: LegacyService Title (with Tag)',
|
||||
},
|
||||
{
|
||||
lang: 'AsciiDoc',
|
||||
label: 'AsciiDoc',
|
||||
source: 'image:$url[LegacyService Title (with Tag)]',
|
||||
},
|
||||
{
|
||||
lang: 'HTML',
|
||||
label: 'HTML',
|
||||
source: '<img alt="LegacyService Title (with Tag)" src="$url">',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
function clean(obj) {
|
||||
// remove any undefined values in the object
|
||||
return JSON.parse(JSON.stringify(obj))
|
||||
}
|
||||
|
||||
describe('category2openapi', function () {
|
||||
it('generates an Open API spec', function () {
|
||||
expect(
|
||||
clean(
|
||||
category2openapi({ name: 'build' }, [
|
||||
OpenApiService.getDefinition(),
|
||||
LegacyService.getDefinition(),
|
||||
])
|
||||
)
|
||||
).to.deep.equal(expected)
|
||||
})
|
||||
})
|
||||
@@ -46,6 +46,28 @@ const serviceDefinition = Joi.object({
|
||||
})
|
||||
)
|
||||
.default([]),
|
||||
openApi: Joi.object().pattern(
|
||||
/./,
|
||||
Joi.object({
|
||||
get: Joi.object({
|
||||
summary: Joi.string().required(),
|
||||
description: Joi.string().required(),
|
||||
parameters: Joi.array()
|
||||
.items(
|
||||
Joi.object({
|
||||
name: Joi.string().required(),
|
||||
description: Joi.string(),
|
||||
in: Joi.string().valid('query', 'path').required(),
|
||||
required: Joi.boolean().required(),
|
||||
schema: Joi.object({ type: Joi.string().required() }).required(),
|
||||
example: Joi.string(),
|
||||
})
|
||||
)
|
||||
.min(1)
|
||||
.required(),
|
||||
}).required(),
|
||||
}).required()
|
||||
),
|
||||
}).required()
|
||||
|
||||
function assertValidServiceDefinition(example, message = undefined) {
|
||||
|
||||
@@ -20,7 +20,9 @@ export default class SqlTokenPersistence {
|
||||
}
|
||||
|
||||
async stop() {
|
||||
await this.pool.end()
|
||||
if (this.pool) {
|
||||
await this.pool.end()
|
||||
}
|
||||
}
|
||||
|
||||
async onTokenAdded(token) {
|
||||
|
||||
@@ -45,14 +45,14 @@ The tests are also divided into several parts:
|
||||
7. [The service tests themselves][service tests] live integration tests of the
|
||||
services, and some mocked tests
|
||||
1. `*.tester.js` in subfolders of [`services`][services]
|
||||
8. Integration tests of Redis-backed persistence code
|
||||
1. [`core/token-pooling/redis-token-persistence.integration.js`][redis-token-persistence.integration]
|
||||
8. Integration tests of PostgreSQL-backed persistence code
|
||||
1. [`core/token-pooling/sql-token-persistence.integration.js`][sql-token-persistence.integration]
|
||||
9. Integration tests of the GitHub authorization code
|
||||
1. [`services/github/github-api-provider.integration.js`][github-api-provider.integration]
|
||||
|
||||
[service-test-runner]: https://github.com/badges/shields/tree/master/core/service-test-runner
|
||||
[service tests]: https://github.com/badges/shields/blob/master/doc/service-tests.md
|
||||
[redis-token-persistence.integration]: https://github.com/badges/shields/blob/master/core/token-pooling/redis-token-persistence.integration.js
|
||||
[sql-token-persistence.integration]: https://github.com/badges/shields/blob/master/core/token-pooling/sql-token-persistence.integration.js
|
||||
[github-api-provider.integration]: https://github.com/badges/shields/blob/master/services/github/github-api-provider.integration.js
|
||||
|
||||
Our goal is to reach 100% coverage of the code in the
|
||||
|
||||
@@ -14,49 +14,41 @@ Production hosting is managed by the Shields ops team:
|
||||
[operations issues]: https://github.com/badges/shields/issues?q=is%3Aissue+is%3Aopen+label%3Aoperations
|
||||
[ops discord]: https://discordapp.com/channels/308323056592486420/480747695879749633
|
||||
|
||||
| Component | Subcomponent | People with access |
|
||||
| ----------------------------- | ------------------------------- | --------------------------------------------------------------- |
|
||||
| shields-io-production | Full access | @calebcartwright, @chris48s, @paulmelnikow |
|
||||
| shields-io-production | Access management | @calebcartwright, @chris48s, @paulmelnikow |
|
||||
| Compose.io Redis | Account owner | @paulmelnikow |
|
||||
| Compose.io Redis | Account access | @paulmelnikow |
|
||||
| Compose.io Redis | Database connection credentials | @calebcartwright, @chris48s, @paulmelnikow, @pyvesb |
|
||||
| Raster server | Full access as team members | @paulmelnikow, @chris48s, @calebcartwright, @platan |
|
||||
| shields-server.com redirector | Full access as team members | @paulmelnikow, @chris48s, @calebcartwright, @platan |
|
||||
| Cloudflare (CDN) | Account owner | @espadrine |
|
||||
| Cloudflare (CDN) | Access management | @espadrine |
|
||||
| Cloudflare (CDN) | Admin access | @calebcartwright, @chris48s, @espadrine, @paulmelnikow, @PyvesB |
|
||||
| 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 |
|
||||
| Sentry | Error reports | @espadrine, @paulmelnikow |
|
||||
| Metrics server | Owner | @platan |
|
||||
| UptimeRobot | Account owner | @paulmelnikow |
|
||||
| More metrics | Owner | @RedSparr0w |
|
||||
| Component | Subcomponent | People with access |
|
||||
| ----------------------------- | --------------------------- | --------------------------------------------------------------- |
|
||||
| shields-io-production | Full access | @calebcartwright, @chris48s, @paulmelnikow |
|
||||
| shields-io-production | Access management | @calebcartwright, @chris48s, @paulmelnikow |
|
||||
| Raster server | Full access as team members | @paulmelnikow, @chris48s, @calebcartwright, @platan |
|
||||
| shields-server.com redirector | Full access as team members | @paulmelnikow, @chris48s, @calebcartwright, @platan |
|
||||
| Cloudflare (CDN) | Account owner | @espadrine |
|
||||
| Cloudflare (CDN) | Access management | @espadrine |
|
||||
| Cloudflare (CDN) | Admin access | @calebcartwright, @chris48s, @espadrine, @paulmelnikow, @PyvesB |
|
||||
| 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 |
|
||||
| Sentry | Error reports | @espadrine, @paulmelnikow |
|
||||
| Metrics server | Owner | @platan |
|
||||
| UptimeRobot | Account owner | @paulmelnikow |
|
||||
| More metrics | Owner | @RedSparr0w |
|
||||
|
||||
## Attached state
|
||||
|
||||
Shields has mercifully little persistent state:
|
||||
|
||||
1. The GitHub tokens we collect are saved on each server in a cloud Redis
|
||||
database. They can also be fetched from the [GitHub auth admin endpoint][]
|
||||
for debugging.
|
||||
1. The GitHub tokens we collect are stored in a fly.io postgres database
|
||||
2. The server keeps the [resource cache][] in memory. It is neither
|
||||
persisted nor inspectable.
|
||||
|
||||
[github auth admin endpoint]: https://github.com/badges/shields/blob/master/services/github/auth/admin.js
|
||||
[resource cache]: https://github.com/badges/shields/blob/master/core/base-service/resource-cache.js
|
||||
|
||||
## Configuration
|
||||
|
||||
To bootstrap the configuration process,
|
||||
[the script that starts the server][start-shields.sh] sets a single
|
||||
environment variable:
|
||||
To bootstrap the configuration of non-secret settings, we set a single environment variable:
|
||||
|
||||
```
|
||||
NODE_CONFIG_ENV=shields-io-production
|
||||
@@ -71,7 +63,8 @@ files:
|
||||
contains non-secrets which are checked in to the main repo.
|
||||
- [`default.yml`][default.yml]. This file contains defaults.
|
||||
|
||||
[start-shields.sh]: https://github.com/badges/ServerScript/blob/master/start-shields.sh#L7
|
||||
Secrets are supplied directly as environment vars.
|
||||
|
||||
[config]: https://github.com/lorenwest/node-config/wiki/Configuration-Files
|
||||
[local-shields-io-production.yml]: ../config/local-shields-io-production.template.yml
|
||||
[shields-io-production.yml]: ../config/shields-io-production.yml
|
||||
|
||||
@@ -45,11 +45,11 @@ We are happy to document and collate any self-hosting patterns/approaches that o
|
||||
We try to make it as easy as possible for users to self-host a Shields server so we publish a few releases of the server. Please be sure to refer to the [self hosting guide][self hosting] for a detailed walk through on how to spin up a server.
|
||||
|
||||
- The server uses [Calendar Versioning](https://calver.org/). Tags of the form `server-YYYY-MM-DD` are server releases (these are the tags that are relevant to self-hosting users, e.g. [server-2021-02-01](https://github.com/badges/shields/releases/tag/server-2021-02-01)).
|
||||
- As well as [tags on GitHub](https://github.com/badges/shields/tags), server releases are also pushed to [DockerHub](https://registry.hub.docker.com/r/shieldsio/shields/tags). See the self-hosting section on [Docker](https://github.com/badges/shields/blob/master/doc/self-hosting.md#Docker) for more details.
|
||||
- As well as [tags on GitHub](https://github.com/badges/shields/tags), server releases are also pushed to [DockerHub](https://registry.hub.docker.com/r/shieldsio/shields/tags) and [GitHub Container Registry](https://github.com/badges/shields/pkgs/container/shields/versions?filters%5Bversion_type%5D=tagged). See the self-hosting section on [Docker](https://github.com/badges/shields/blob/master/doc/self-hosting.md#Docker) for more details.
|
||||
- We publish release notes for server releases in the [CHANGELOG](https://github.com/badges/shields/blob/master/CHANGELOG.md). There may occasionally be non-backwards compatible changes to be aware of.
|
||||
- We will normally put out one release per month. If there is a security patch or major bugfix affecting self-hosting users, we may put out an out-of-sequence release.
|
||||
- Releases are just a snapshot in time. We advise always tracking the latest release to ensure you are up-to-date with the latest bug fixes and security updates. There are no 'patch' releases - we don't backport fixes to old releases. Tagged versions just provide a convenient way to apply upgrades in a controlled way or roll back to an older version if necessary and communicate about versions.
|
||||
- You can stay on the bleeding edge by tracking the `master` branch for source installs or the `next` tag on DockerHub.
|
||||
- You can stay on the bleeding edge by tracking the `master` branch for source installs or the `next` tag on DockerHub/GHCR.
|
||||
|
||||
[shields.io]: https://shields.io
|
||||
[npm package]: https://www.npmjs.com/package/badge-maker
|
||||
|
||||
@@ -71,18 +71,30 @@ vercel
|
||||
|
||||
## Docker
|
||||
|
||||
### DockerHub
|
||||
### Public Images
|
||||
|
||||
We publish images to DockerHub at https://registry.hub.docker.com/r/shieldsio/shields
|
||||
We publish images to:
|
||||
|
||||
The `next` tag is the latest build from `master`, or tagged releases are available
|
||||
https://registry.hub.docker.com/r/shieldsio/shields/tags
|
||||
- DockerHub at https://registry.hub.docker.com/r/shieldsio/shields and
|
||||
- GitHub Container Registry at https://github.com/badges/shields/pkgs/container/shields
|
||||
|
||||
```console
|
||||
The `next` tag is the latest build from `master`, or tagged snapshot releases are available:
|
||||
|
||||
- https://registry.hub.docker.com/r/shieldsio/shields/tags
|
||||
- https://github.com/badges/shields/pkgs/container/shields/versions?filters%5Bversion_type%5D=tagged
|
||||
|
||||
```sh
|
||||
# DockerHub
|
||||
$ docker pull shieldsio/shields:next
|
||||
$ docker run shieldsio/shields:next
|
||||
```
|
||||
|
||||
```sh
|
||||
# GHCR
|
||||
$ docker pull ghcr.io/badges/shields:next
|
||||
$ docker pull ghcr.io/badges/shields:next
|
||||
```
|
||||
|
||||
### Building Docker Image Locally
|
||||
|
||||
Alternatively, you can build and run the server locally using Docker. First build an image:
|
||||
|
||||
0
frontend/categories/.gitkeep
Normal file
0
frontend/categories/.gitkeep
Normal file
@@ -327,17 +327,18 @@ export default function Usage({ baseUrl }: { baseUrl: string }): JSX.Element {
|
||||
<QueryParam
|
||||
documentation={
|
||||
<span>
|
||||
Insert one of the named logos from (<NamedLogos />) or{' '}
|
||||
Insert one of the named logos from (<NamedLogos />) or
|
||||
simple-icons. All simple-icons are referenced using icon slugs.
|
||||
You can click the icon title on{' '}
|
||||
<a
|
||||
href="https://simpleicons.org/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
simple-icons
|
||||
</a>
|
||||
. Simple-icons are referenced using icon slugs which can be
|
||||
found on the simple-icons site or in the{' '}
|
||||
<a href="https://github.com/simple-icons/simple-icons/blob/develop/slugs.md">
|
||||
</a>{' '}
|
||||
to copy the slug or they can be found in the{' '}
|
||||
<a href="https://github.com/simple-icons/simple-icons/blob/master/slugs.md">
|
||||
slugs.md file
|
||||
</a>{' '}
|
||||
in the simple-icons repository.
|
||||
|
||||
3305
package-lock.json
generated
3305
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
64
package.json
64
package.json
@@ -24,8 +24,8 @@
|
||||
"@fontsource/lato": "^4.5.10",
|
||||
"@fontsource/lekton": "^4.5.11",
|
||||
"@renovate/pep440": "^1.0.0",
|
||||
"@renovatebot/ruby-semver": "^2.1.8",
|
||||
"@sentry/node": "^7.38.0",
|
||||
"@renovatebot/ruby-semver": "^2.1.10",
|
||||
"@sentry/node": "^7.47.0",
|
||||
"@shields_io/camp": "^18.1.2",
|
||||
"badge-maker": "file:badge-maker",
|
||||
"bytes": "^3.1.2",
|
||||
@@ -39,14 +39,14 @@
|
||||
"decamelize": "^3.2.0",
|
||||
"emojic": "^1.1.17",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"fast-xml-parser": "^4.1.2",
|
||||
"glob": "^8.1.0",
|
||||
"fast-xml-parser": "^4.1.3",
|
||||
"glob": "^9.3.4",
|
||||
"global-agent": "^3.0.0",
|
||||
"got": "^12.5.3",
|
||||
"got": "^12.6.0",
|
||||
"graphql": "^15.6.1",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"ioredis": "5.3.1",
|
||||
"joi": "17.8.3",
|
||||
"joi": "17.9.1",
|
||||
"joi-extension-semver": "5.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonpath": "~1.1.1",
|
||||
@@ -57,14 +57,14 @@
|
||||
"node-pg-migrate": "^6.2.2",
|
||||
"parse-link-header": "^2.0.0",
|
||||
"path-to-regexp": "^6.2.1",
|
||||
"pg": "^8.9.0",
|
||||
"pg": "^8.10.0",
|
||||
"pretty-bytes": "^6.1.0",
|
||||
"priorityqueuejs": "^2.0.0",
|
||||
"prom-client": "^14.1.1",
|
||||
"qs": "^6.11.0",
|
||||
"prom-client": "^14.2.0",
|
||||
"qs": "^6.11.1",
|
||||
"query-string": "^8.1.0",
|
||||
"semver": "~7.3.8",
|
||||
"simple-icons": "8.5.0",
|
||||
"simple-icons": "8.9.0",
|
||||
"webextension-store-meta": "^1.0.5",
|
||||
"xmldom": "~0.6.0",
|
||||
"xpath": "~0.0.32"
|
||||
@@ -145,7 +145,7 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.0",
|
||||
"@babel/core": "^7.21.4",
|
||||
"@babel/polyfill": "^7.12.1",
|
||||
"@babel/register": "7.21.0",
|
||||
"@istanbuljs/schema": "^0.1.3",
|
||||
@@ -159,8 +159,8 @@
|
||||
"@types/react-modal": "^3.13.1",
|
||||
"@types/react-select": "^4.0.17",
|
||||
"@types/styled-components": "5.1.26",
|
||||
"@typescript-eslint/eslint-plugin": "^5.53.0",
|
||||
"@typescript-eslint/parser": "^5.46.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.57.1",
|
||||
"@typescript-eslint/parser": "^5.55.0",
|
||||
"babel-plugin-inline-react-svg": "^2.0.2",
|
||||
"babel-preset-gatsby": "^2.22.0",
|
||||
"c8": "^7.13.0",
|
||||
@@ -171,28 +171,28 @@
|
||||
"chai-string": "^1.4.0",
|
||||
"child-process-promise": "^2.2.1",
|
||||
"clipboard-copy": "^4.0.1",
|
||||
"concurrently": "^7.6.0",
|
||||
"cypress": "^12.6.0",
|
||||
"concurrently": "^8.0.1",
|
||||
"cypress": "^12.9.0",
|
||||
"cypress-wait-for-stable-dom": "^0.1.0",
|
||||
"danger": "^11.2.3",
|
||||
"deepmerge": "^4.3.0",
|
||||
"danger": "^11.2.4",
|
||||
"deepmerge": "^4.3.1",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-config-standard": "^16.0.3",
|
||||
"eslint-config-standard-jsx": "^10.0.0",
|
||||
"eslint-config-standard-react": "^11.0.1",
|
||||
"eslint-plugin-chai-friendly": "^0.7.2",
|
||||
"eslint-plugin-cypress": "^2.12.1",
|
||||
"eslint-plugin-cypress": "^2.13.2",
|
||||
"eslint-plugin-icedfrisby": "^0.1.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jsdoc": "^40.0.0",
|
||||
"eslint-plugin-jsdoc": "^40.1.1",
|
||||
"eslint-plugin-mocha": "^10.1.0",
|
||||
"eslint-plugin-no-extension-in-require": "^0.2.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^5.2.0",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-sort-class-members": "^1.16.0",
|
||||
"eslint-plugin-sort-class-members": "^1.17.0",
|
||||
"fetch-ponyfill": "^7.1.0",
|
||||
"form-data": "^4.0.0",
|
||||
"gatsby": "4.23.1",
|
||||
@@ -208,7 +208,7 @@
|
||||
"is-svg": "^4.3.2",
|
||||
"js-yaml-loader": "^1.2.2",
|
||||
"jsdoc": "^4.0.2",
|
||||
"lint-staged": "^13.1.2",
|
||||
"lint-staged": "^13.2.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.difference": "^4.5.0",
|
||||
"minimist": "^1.2.8",
|
||||
@@ -217,12 +217,12 @@
|
||||
"mocha-junit-reporter": "^2.2.0",
|
||||
"mocha-yaml-loader": "^1.0.3",
|
||||
"nock": "13.3.0",
|
||||
"node-mocks-http": "^1.12.1",
|
||||
"nodemon": "^2.0.20",
|
||||
"node-mocks-http": "^1.12.2",
|
||||
"nodemon": "^2.0.22",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"open-cli": "^7.1.0",
|
||||
"open-cli": "^7.2.0",
|
||||
"portfinder": "^1.0.32",
|
||||
"prettier": "2.8.4",
|
||||
"prettier": "2.8.7",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-error-overlay": "^6.0.11",
|
||||
@@ -232,17 +232,17 @@
|
||||
"react-select": "^4.3.1",
|
||||
"read-all-stdin-sync": "^1.0.5",
|
||||
"redis-server": "^1.2.2",
|
||||
"rimraf": "^4.1.2",
|
||||
"rimraf": "^4.4.1",
|
||||
"sazerac": "^2.0.0",
|
||||
"simple-git-hooks": "^2.8.1",
|
||||
"sinon": "^15.0.1",
|
||||
"sinon": "^15.0.3",
|
||||
"sinon-chai": "^3.7.0",
|
||||
"snap-shot-it": "^7.9.10",
|
||||
"start-server-and-test": "1.15.4",
|
||||
"styled-components": "^5.3.6",
|
||||
"start-server-and-test": "2.0.0",
|
||||
"styled-components": "^5.3.9",
|
||||
"ts-mocha": "^10.0.0",
|
||||
"tsd": "^0.25.0",
|
||||
"typescript": "^4.9.5",
|
||||
"tsd": "^0.28.1",
|
||||
"typescript": "^5.0.3",
|
||||
"url": "^0.11.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
49
scripts/export-openapi-cli.js
Normal file
49
scripts/export-openapi-cli.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import yaml from 'js-yaml'
|
||||
import { collectDefinitions } from '../core/base-service/loader.js'
|
||||
import { category2openapi } from '../core/base-service/openapi.js'
|
||||
|
||||
const specsPath = path.join('frontend', 'categories')
|
||||
|
||||
function writeSpec(filename, spec) {
|
||||
// Omit undefined
|
||||
// https://github.com/nodeca/js-yaml/issues/356#issuecomment-312430599
|
||||
const cleaned = JSON.parse(JSON.stringify(spec))
|
||||
|
||||
fs.writeFileSync(
|
||||
filename,
|
||||
yaml.dump(cleaned, { flowLevel: 5, forceQuotes: true })
|
||||
)
|
||||
}
|
||||
|
||||
;(async () => {
|
||||
const definitions = await collectDefinitions()
|
||||
|
||||
for (const category of definitions.categories) {
|
||||
const services = definitions.services.filter(
|
||||
service => service.category === category.id && !service.isDeprecated
|
||||
)
|
||||
|
||||
writeSpec(
|
||||
path.join(specsPath, `${category.id}.yaml`),
|
||||
category2openapi(category, services)
|
||||
)
|
||||
}
|
||||
|
||||
let coreServices = []
|
||||
coreServices = coreServices.concat(
|
||||
definitions.services.filter(
|
||||
service => service.category === 'static' && !service.isDeprecated
|
||||
)
|
||||
)
|
||||
coreServices = coreServices.concat(
|
||||
definitions.services.filter(
|
||||
service => service.category === 'dynamic' && !service.isDeprecated
|
||||
)
|
||||
)
|
||||
writeSpec(
|
||||
path.join(specsPath, '1core.yaml'),
|
||||
category2openapi({ name: 'Core' }, coreServices)
|
||||
)
|
||||
})()
|
||||
@@ -3,6 +3,19 @@ import { collectDefinitions } from '../core/base-service/loader.js'
|
||||
;(async () => {
|
||||
const definitions = await collectDefinitions()
|
||||
|
||||
// filter out static, dynamic and debug badge examples
|
||||
const publicCategories = definitions.categories.map(c => c.id)
|
||||
definitions.services = definitions.services.filter(s =>
|
||||
publicCategories.includes(s.category)
|
||||
)
|
||||
|
||||
// drop the openApi property for the "legacy" frontend
|
||||
for (const service of definitions.services) {
|
||||
if (service.openApi) {
|
||||
service.openApi = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// Omit undefined
|
||||
// https://github.com/nodeca/js-yaml/issues/356#issuecomment-312430599
|
||||
const cleaned = JSON.parse(JSON.stringify(definitions))
|
||||
|
||||
@@ -20,17 +20,17 @@ if (data.stats.passes > 0) {
|
||||
process.stdout.write(`✔ ${data.stats.passes} passed\n`)
|
||||
}
|
||||
if (data.stats.failures > 0) {
|
||||
process.stdout.write(`✖ ${data.stats.failures} failed\n\n`)
|
||||
process.stdout.write(`✖ ${data.stats.failures} failed\n`)
|
||||
}
|
||||
if (data.stats.pending > 0) {
|
||||
process.stdout.write(`● ${data.stats.pending} pending\n\n`)
|
||||
process.exit(2)
|
||||
process.stdout.write(`● ${data.stats.pending} pending\n`)
|
||||
}
|
||||
process.stdout.write('\n')
|
||||
|
||||
if (data.stats.failures > 0) {
|
||||
for (const test of data.tests) {
|
||||
process.stdout.write('## Failures\n\n')
|
||||
for (const test of data.failures) {
|
||||
if (test.err && Object.keys(test.err).length > 0) {
|
||||
process.stdout.write(`### ${test.title}\n\n`)
|
||||
process.stdout.write(`${test.fullTitle}\n\n`)
|
||||
process.stdout.write('```\n')
|
||||
process.stdout.write(`${test.err.stack}\n`)
|
||||
@@ -38,3 +38,10 @@ if (data.stats.failures > 0) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.stats.pending > 0) {
|
||||
process.stdout.write('## Pending\n\n')
|
||||
for (const test of data.pending) {
|
||||
process.stdout.write(`${test.fullTitle}\n\n`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import Joi from 'joi'
|
||||
import { renderLicenseBadge } from '../licenses.js'
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
import { BaseJsonService } from '../index.js'
|
||||
import { BaseJsonService, InvalidResponse } from '../index.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
license: Joi.array().items(Joi.string()).single(),
|
||||
version: Joi.object({
|
||||
number: Joi.string().required(),
|
||||
number: Joi.string().allow('').required(),
|
||||
date: Joi.string().allow('').required(),
|
||||
}).required(),
|
||||
}).required()
|
||||
|
||||
@@ -14,7 +15,7 @@ class BaseCtanService extends BaseJsonService {
|
||||
static defaultBadgeData = { label: 'ctan' }
|
||||
|
||||
async fetch({ library }) {
|
||||
const url = `http://www.ctan.org/json/pkg/${library}`
|
||||
const url = `https://www.ctan.org/json/2.0/pkg/${library}`
|
||||
return this._requestJson({
|
||||
schema,
|
||||
url,
|
||||
@@ -67,7 +68,22 @@ class CtanVersion extends BaseCtanService {
|
||||
|
||||
async handle({ library }) {
|
||||
const json = await this.fetch({ library })
|
||||
return renderVersionBadge({ version: json.version.number })
|
||||
const version = json.version.number
|
||||
if (version !== '') {
|
||||
return renderVersionBadge({ version })
|
||||
} else {
|
||||
const date = json.version.date
|
||||
if (date !== '') {
|
||||
return renderVersionBadge({
|
||||
version: date,
|
||||
versionFormatter: color => 'blue',
|
||||
})
|
||||
} else {
|
||||
return new InvalidResponse({
|
||||
underlyingError: new Error('Both number and date are empty'),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import Joi from 'joi'
|
||||
import { ServiceTester } from '../tester.js'
|
||||
import { isVPlusDottedVersionAtLeastOne } from '../test-validators.js'
|
||||
import { withRegex } from '../test-validators.js'
|
||||
|
||||
// same as isVPlusDottedVersionAtLeastOne, but also accepts an optional
|
||||
// single lowercase alphabet letter suffix
|
||||
// e.g.: v1.81a
|
||||
const isVPlusDottedVersionAtLeastOneWithOptionalAlphabetLetter = withRegex(
|
||||
/^v\d+(\.\d+)?(\.\d+)?[a-z]?$/
|
||||
)
|
||||
|
||||
export const t = new ServiceTester({
|
||||
id: 'ctan',
|
||||
@@ -14,11 +22,12 @@ t.create('license').get('/l/novel.json').expectBadge({
|
||||
t.create('license missing')
|
||||
.get('/l/novel.json')
|
||||
.intercept(nock =>
|
||||
nock('http://www.ctan.org')
|
||||
.get('/json/pkg/novel')
|
||||
nock('https://www.ctan.org')
|
||||
.get('/json/2.0/pkg/novel')
|
||||
.reply(200, {
|
||||
version: {
|
||||
number: 'notRelevant',
|
||||
date: 'notRelevant',
|
||||
},
|
||||
})
|
||||
)
|
||||
@@ -30,12 +39,13 @@ t.create('license missing')
|
||||
t.create('single license')
|
||||
.get('/l/tex.json')
|
||||
.intercept(nock =>
|
||||
nock('http://www.ctan.org')
|
||||
.get('/json/pkg/tex')
|
||||
nock('https://www.ctan.org')
|
||||
.get('/json/2.0/pkg/tex')
|
||||
.reply(200, {
|
||||
license: 'knuth',
|
||||
version: {
|
||||
number: 'notRelevant',
|
||||
date: 'notRelevant',
|
||||
},
|
||||
})
|
||||
)
|
||||
@@ -46,17 +56,18 @@ t.create('single license')
|
||||
|
||||
t.create('version').get('/v/novel.json').expectBadge({
|
||||
label: 'ctan',
|
||||
message: isVPlusDottedVersionAtLeastOne,
|
||||
message: isVPlusDottedVersionAtLeastOneWithOptionalAlphabetLetter,
|
||||
})
|
||||
|
||||
t.create('version')
|
||||
.get('/v/novel.json')
|
||||
.intercept(nock =>
|
||||
nock('http://www.ctan.org')
|
||||
.get('/json/pkg/novel')
|
||||
nock('https://www.ctan.org')
|
||||
.get('/json/2.0/pkg/novel')
|
||||
.reply(200, {
|
||||
version: {
|
||||
number: 'v1.11',
|
||||
date: '',
|
||||
},
|
||||
})
|
||||
)
|
||||
@@ -65,3 +76,9 @@ t.create('version')
|
||||
message: 'v1.11',
|
||||
color: 'blue',
|
||||
})
|
||||
|
||||
t.create('date as version').get('/v/l3kernel.json').expectBadge({
|
||||
label: 'ctan',
|
||||
message: Joi.date().iso(),
|
||||
color: 'blue',
|
||||
})
|
||||
|
||||
@@ -6,6 +6,53 @@ import jsonPath from './json-path.js'
|
||||
export default class DynamicJson extends jsonPath(BaseJsonService) {
|
||||
static enabledMetrics = [MetricNames.SERVICE_RESPONSE_SIZE]
|
||||
static route = createRoute('json')
|
||||
static openApi = {
|
||||
'/badge/dynamic/json': {
|
||||
get: {
|
||||
summary: 'Dynamic JSON Badge',
|
||||
description: `<p>
|
||||
The Dynamic JSON Badge allows you to extract an arbitrary value from any
|
||||
JSON Document using a JSONPath selector and show it on a badge.
|
||||
</p>`,
|
||||
parameters: [
|
||||
{
|
||||
name: 'url',
|
||||
description: 'The URL to a JSON document',
|
||||
in: 'query',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example:
|
||||
'https://github.com/badges/shields/raw/master/package.json',
|
||||
},
|
||||
{
|
||||
name: 'query',
|
||||
description:
|
||||
'A <a href="https://jsonpath.com/">JSONPath</a> expression that will be used to query the document',
|
||||
in: 'query',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: '$.name',
|
||||
},
|
||||
{
|
||||
name: 'prefix',
|
||||
description: 'Optional prefix to append to the value',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
example: '[',
|
||||
},
|
||||
{
|
||||
name: 'suffix',
|
||||
description: 'Optional suffix to append to the value',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
example: ']',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
async fetch({ schema, url, errorMessages }) {
|
||||
return this._requestJson({
|
||||
|
||||
@@ -27,7 +27,7 @@ t.create('Malformed url')
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'Package Name',
|
||||
message: 'inaccessible',
|
||||
message: 'invalid',
|
||||
color: 'lightgrey',
|
||||
})
|
||||
|
||||
|
||||
@@ -15,6 +15,53 @@ export default class DynamicXml extends BaseService {
|
||||
static category = 'dynamic'
|
||||
static enabledMetrics = [MetricNames.SERVICE_RESPONSE_SIZE]
|
||||
static route = createRoute('xml')
|
||||
static openApi = {
|
||||
'/badge/dynamic/xml': {
|
||||
get: {
|
||||
summary: 'Dynamic XML Badge',
|
||||
description: `<p>
|
||||
The Dynamic XML Badge allows you to extract an arbitrary value from any
|
||||
XML Document using an XPath selector and show it on a badge.
|
||||
</p>`,
|
||||
parameters: [
|
||||
{
|
||||
name: 'url',
|
||||
description: 'The URL to a XML document',
|
||||
in: 'query',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'https://httpbin.org/xml',
|
||||
},
|
||||
{
|
||||
name: 'query',
|
||||
description:
|
||||
'A <a href="http://xpather.com/">XPath</a> expression that will be used to query the document',
|
||||
in: 'query',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: '//slideshow/slide[1]/title',
|
||||
},
|
||||
{
|
||||
name: 'prefix',
|
||||
description: 'Optional prefix to append to the value',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
example: '[',
|
||||
},
|
||||
{
|
||||
name: 'suffix',
|
||||
description: 'Optional suffix to append to the value',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
example: ']',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
static defaultBadgeData = { label: 'custom badge' }
|
||||
|
||||
transform({ pathExpression, buffer }) {
|
||||
|
||||
@@ -6,6 +6,53 @@ import jsonPath from './json-path.js'
|
||||
export default class DynamicYaml extends jsonPath(BaseYamlService) {
|
||||
static enabledMetrics = [MetricNames.SERVICE_RESPONSE_SIZE]
|
||||
static route = createRoute('yaml')
|
||||
static openApi = {
|
||||
'/badge/dynamic/yaml': {
|
||||
get: {
|
||||
summary: 'Dynamic YAML Badge',
|
||||
description: `<p>
|
||||
The Dynamic YAML Badge allows you to extract an arbitrary value from any
|
||||
YAML Document using a JSONPath selector and show it on a badge.
|
||||
</p>`,
|
||||
parameters: [
|
||||
{
|
||||
name: 'url',
|
||||
description: 'The URL to a YAML document',
|
||||
in: 'query',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example:
|
||||
'https://raw.githubusercontent.com/badges/shields/master/.github/dependabot.yml',
|
||||
},
|
||||
{
|
||||
name: 'query',
|
||||
description:
|
||||
'A <a href="https://jsonpath.com/">JSONPath</a> expression that will be used to query the document',
|
||||
in: 'query',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: '$.version',
|
||||
},
|
||||
{
|
||||
name: 'prefix',
|
||||
description: 'Optional prefix to append to the value',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
example: '[',
|
||||
},
|
||||
{
|
||||
name: 'suffix',
|
||||
description: 'Optional suffix to append to the value',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
example: ']',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
async fetch({ schema, url, errorMessages }) {
|
||||
return this._requestYaml({
|
||||
|
||||
@@ -11,14 +11,144 @@ const queryParamSchema = Joi.object({
|
||||
url: optionalUrl.required(),
|
||||
}).required()
|
||||
|
||||
const description = `<p>
|
||||
Using the endpoint badge, you can provide content for a badge through
|
||||
a JSON endpoint. The content can be prerendered, or generated on the
|
||||
fly. To strike a balance between responsiveness and bandwidth
|
||||
utilization on one hand, and freshness on the other, cache behavior is
|
||||
configurable, subject to the Shields minimum. The endpoint URL is
|
||||
provided to Shields through the query string. Shields fetches it and
|
||||
formats the badge.
|
||||
</p>
|
||||
<p>
|
||||
The endpoint badge takes a single required query param: <code>url</code>, which is the URL to your JSON endpoint
|
||||
</p>
|
||||
<div>
|
||||
<h2>Example JSON Endpoint Response</h2>
|
||||
<code>{ "schemaVersion": 1, "label": "hello", "message": "sweet world", "color": "orange" }</code>
|
||||
<h2>Example Shields Response</h2>
|
||||
<img src="https://img.shields.io/badge/hello-sweet_world-orange" />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Schema</h2>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Property</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>schemaVersion</code></td>
|
||||
<td>Required. Always the number <code>1</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>label</code></td>
|
||||
<td>
|
||||
Required. The left text, or the empty string to omit the left side of
|
||||
the badge. This can be overridden by the query string.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>message</code></td>
|
||||
<td>Required. Can't be empty. The right text.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>color</code></td>
|
||||
<td>
|
||||
Default: <code>lightgrey</code>. The right color. Supports the eight
|
||||
named colors above, as well as hex, rgb, rgba, hsl, hsla and css named
|
||||
colors. This can be overridden by the query string.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>labelColor</code></td>
|
||||
<td>
|
||||
Default: <code>grey</code>. The left color. This can be overridden by
|
||||
the query string.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>isError</code></td>
|
||||
<td>
|
||||
Default: <code>false</code>. <code>true</code> to treat this as an
|
||||
error badge. This prevents the user from overriding the color. In the
|
||||
future, it may affect cache behavior.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>namedLogo</code></td>
|
||||
<td>
|
||||
Default: none. One of the named logos supported by Shields or
|
||||
<a href="https://simpleicons.org/">simple-icons</a>. Can be overridden
|
||||
by the query string.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>logoSvg</code></td>
|
||||
<td>Default: none. An SVG string containing a custom logo.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>logoColor</code></td>
|
||||
<td>
|
||||
Default: none. Same meaning as the query string. Can be overridden by
|
||||
the query string. Only works for named logos and Shields logos. If you
|
||||
override the color of a multicolor Shield logo, the corresponding
|
||||
named logo will be used and colored.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>logoWidth</code></td>
|
||||
<td>
|
||||
Default: none. Same meaning as the query string. Can be overridden by
|
||||
the query string.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>logoPosition</code></td>
|
||||
<td>
|
||||
Default: none. Same meaning as the query string. Can be overridden by
|
||||
the query string.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>style</code></td>
|
||||
<td>
|
||||
Default: <code>flat</code>. The default template to use. Can be
|
||||
overridden by the query string.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>`
|
||||
|
||||
export default class Endpoint extends BaseJsonService {
|
||||
static category = 'dynamic'
|
||||
|
||||
static route = {
|
||||
base: 'endpoint',
|
||||
pattern: '',
|
||||
queryParamSchema,
|
||||
}
|
||||
|
||||
static openApi = {
|
||||
'/endpoint': {
|
||||
get: {
|
||||
summary: 'Endpoint Badge',
|
||||
description,
|
||||
parameters: [
|
||||
{
|
||||
name: 'url',
|
||||
description: 'The URL to your JSON endpoint',
|
||||
in: 'query',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'https://shields.redsparr0w.com/2473/monday',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
static _cacheLength = 300
|
||||
static defaultBadgeData = { label: 'custom badge' }
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import zlib from 'zlib'
|
||||
import { expect } from 'chai'
|
||||
import { getShieldsIcon } from '../../lib/logos.js'
|
||||
import { getShieldsIcon, getSimpleIcon } from '../../lib/logos.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
@@ -73,13 +73,13 @@ t.create('named logo with color')
|
||||
schemaVersion: 1,
|
||||
label: 'hey',
|
||||
message: 'yo',
|
||||
namedLogo: 'npm',
|
||||
namedLogo: 'github',
|
||||
logoColor: 'blue',
|
||||
})
|
||||
)
|
||||
.after((err, res, body) => {
|
||||
expect(err).not.to.be.ok
|
||||
expect(body).to.include(getShieldsIcon({ name: 'npm', color: 'blue' }))
|
||||
expect(body).to.include(getSimpleIcon({ name: 'github', color: 'blue' }))
|
||||
})
|
||||
|
||||
const logoSvg = Buffer.from(
|
||||
|
||||
@@ -21,7 +21,7 @@ export default class GithubSize extends GithubAuthV3Service {
|
||||
|
||||
static route = {
|
||||
base: 'github/size',
|
||||
pattern: ':user/:repo/:path*',
|
||||
pattern: ':user/:repo/:path+',
|
||||
queryParamSchema,
|
||||
}
|
||||
|
||||
|
||||
@@ -30,4 +30,13 @@ export default [
|
||||
dateAdded: new Date('2019-11-29'),
|
||||
...commonProps,
|
||||
}),
|
||||
redirector({
|
||||
route: {
|
||||
base: 'jenkins/coverage/api',
|
||||
pattern: '',
|
||||
},
|
||||
category: 'coverage',
|
||||
transformPath: () => '/jenkins/coverage/apiv1',
|
||||
dateAdded: new Date('2023-03-21'),
|
||||
}),
|
||||
]
|
||||
|
||||
@@ -53,3 +53,11 @@ t.create('api prefix + job url in path')
|
||||
'https://jenkins.library.illinois.edu/job/OpenSourceProjects/job/Speedwagon/job/master'
|
||||
)}`
|
||||
)
|
||||
|
||||
t.create('old v1 api prefix to new prefix')
|
||||
.get(
|
||||
'/coverage/api.svg?jobUrl=http://loneraver.duckdns.org:8082/job/github/job/VisVid/job/master'
|
||||
)
|
||||
.expectRedirect(
|
||||
'/jenkins/coverage/apiv1.svg?jobUrl=http://loneraver.duckdns.org:8082/job/github/job/VisVid/job/master'
|
||||
)
|
||||
|
||||
@@ -42,7 +42,7 @@ const formatMap = {
|
||||
},
|
||||
pluginSpecificPath: 'cobertura',
|
||||
},
|
||||
api: {
|
||||
apiv1: {
|
||||
schema: Joi.object({
|
||||
results: Joi.object({
|
||||
elements: Joi.array()
|
||||
@@ -66,6 +66,25 @@ const formatMap = {
|
||||
},
|
||||
pluginSpecificPath: 'coverage/result',
|
||||
},
|
||||
apiv4: {
|
||||
schema: Joi.object({
|
||||
projectStatistics: Joi.object({
|
||||
line: Joi.string()
|
||||
.pattern(/\d+\.\d+%/)
|
||||
.required(),
|
||||
}).required(),
|
||||
}).required(),
|
||||
treeQueryParam: 'projectStatistics[line]',
|
||||
transform: json => {
|
||||
const lineCoverageStr = json.projectStatistics.line
|
||||
const lineCoverage = lineCoverageStr.substring(
|
||||
0,
|
||||
lineCoverageStr.length - 1
|
||||
)
|
||||
return { coverage: Number.parseFloat(lineCoverage) }
|
||||
},
|
||||
pluginSpecificPath: 'coverage',
|
||||
},
|
||||
}
|
||||
|
||||
const documentation = `
|
||||
@@ -74,7 +93,7 @@ const documentation = `
|
||||
<ul>
|
||||
<li><a href="https://plugins.jenkins.io/jacoco">JaCoCo</a></li>
|
||||
<li><a href="https://plugins.jenkins.io/cobertura">Cobertura</a></li>
|
||||
<li>Any plugin which integrates with the <a href="https://plugins.jenkins.io/code-coverage-api">Code Coverage API</a> (e.g. llvm-cov, Cobertura 1.13+, etc.)</li>
|
||||
<li>Any plugin which integrates with version 1 or 4+ of the <a href="https://plugins.jenkins.io/code-coverage-api">Code Coverage API</a> (e.g. llvm-cov, Cobertura 1.13+, etc.)</li>
|
||||
</ul>
|
||||
</p>
|
||||
`
|
||||
@@ -84,7 +103,7 @@ export default class JenkinsCoverage extends JenkinsBase {
|
||||
|
||||
static route = {
|
||||
base: 'jenkins/coverage',
|
||||
pattern: ':format(jacoco|cobertura|api)',
|
||||
pattern: ':format(jacoco|cobertura|apiv1|apiv4)',
|
||||
queryParamSchema,
|
||||
}
|
||||
|
||||
|
||||
@@ -31,14 +31,49 @@ t.create('cobertura: job found')
|
||||
)
|
||||
.expectBadge({ label: 'coverage', message: isIntegerPercentage })
|
||||
|
||||
t.create('code coverage API: job not found')
|
||||
t.create('code coverage API v1: job not found')
|
||||
.get(
|
||||
'/api.json?jobUrl=https://jenkins.library.illinois.edu/job/does-not-exist'
|
||||
'/apiv1.json?jobUrl=https://jenkins.library.illinois.edu/job/does-not-exist'
|
||||
)
|
||||
.expectBadge({ label: 'coverage', message: 'job or coverage not found' })
|
||||
|
||||
t.create('code coverage API: job found')
|
||||
const coverageApiV1Response = {
|
||||
_class: 'io.jenkins.plugins.coverage.targets.CoverageResult',
|
||||
results: {
|
||||
elements: [
|
||||
{ name: 'Report', ratio: 100.0 },
|
||||
{ name: 'Group', ratio: 100.0 },
|
||||
{ name: 'Package', ratio: 66.666664 },
|
||||
{ name: 'File', ratio: 52.0 },
|
||||
{ name: 'Class', ratio: 52.0 },
|
||||
{ name: 'Line', ratio: 40.66363 },
|
||||
{ name: 'Conditional', ratio: 29.91968 },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
t.create('code coverage API v1: job found')
|
||||
.get(
|
||||
'/api.json?jobUrl=http://loneraver.duckdns.org:8082/job/github/job/VisVid/job/master/'
|
||||
'/apiv1.json?jobUrl=http://loneraver.duckdns.org:8082/job/github/job/VisVid/job/master'
|
||||
)
|
||||
.intercept(nock =>
|
||||
nock(
|
||||
'http://loneraver.duckdns.org:8082/job/github/job/VisVid/job/master/lastCompletedBuild'
|
||||
)
|
||||
.get('/coverage/result/api/json')
|
||||
.query(true)
|
||||
.reply(200, coverageApiV1Response)
|
||||
)
|
||||
.expectBadge({ label: 'coverage', message: isIntegerPercentage })
|
||||
|
||||
t.create('code coverage API v4+: job not found')
|
||||
.get(
|
||||
'/apiv4.json?jobUrl=https://jenkins.library.illinois.edu/job/does-not-exist'
|
||||
)
|
||||
.expectBadge({ label: 'coverage', message: 'job or coverage not found' })
|
||||
|
||||
t.create('code coverage API v4+: job found')
|
||||
.get(
|
||||
'/apiv4.json?jobUrl=https://jenkins.mm12.xyz/jenkins/job/nmfu/job/master'
|
||||
)
|
||||
.expectBadge({ label: 'coverage', message: isIntegerPercentage })
|
||||
|
||||
@@ -52,9 +52,9 @@ export default class Netlify extends BaseSvgScrapingService {
|
||||
const { buffer } = await this._request({
|
||||
url,
|
||||
})
|
||||
if (buffer.includes('#0D544F')) return { message: 'passing' }
|
||||
if (buffer.includes('#900B31')) return { message: 'failing' }
|
||||
if (buffer.includes('#AB6F10')) return { message: 'building' }
|
||||
if (buffer.includes('#0F4A21')) return { message: 'passing' }
|
||||
if (buffer.includes('#800A20')) return { message: 'failing' }
|
||||
if (buffer.includes('#603408')) return { message: 'building' }
|
||||
return { message: 'unknown' }
|
||||
}
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ export default class NodeVersionBase extends NPMBase {
|
||||
},
|
||||
{
|
||||
title: `${prefix} (scoped)`,
|
||||
pattern: '@:scope/:packageName',
|
||||
namedParams: { scope: 'stdlib', packageName: 'stdlib' },
|
||||
pattern: ':scope/:packageName',
|
||||
namedParams: { scope: '@stdlib', packageName: 'stdlib' },
|
||||
staticPreview: this.renderStaticPreview({
|
||||
nodeVersionRange: '>= 6.0.0',
|
||||
}),
|
||||
@@ -48,8 +48,8 @@ export default class NodeVersionBase extends NPMBase {
|
||||
},
|
||||
{
|
||||
title: `${prefix} (scoped with tag)`,
|
||||
pattern: '@:scope/:packageName/:tag',
|
||||
namedParams: { scope: 'stdlib', packageName: 'stdlib', tag: 'latest' },
|
||||
pattern: ':scope/:packageName/:tag',
|
||||
namedParams: { scope: '@stdlib', packageName: 'stdlib', tag: 'latest' },
|
||||
staticPreview: this.renderStaticPreview({
|
||||
nodeVersionRange: '>= 6.0.0',
|
||||
tag: 'latest',
|
||||
@@ -59,8 +59,8 @@ export default class NodeVersionBase extends NPMBase {
|
||||
},
|
||||
{
|
||||
title: `${prefix} (scoped with tag, custom registry)`,
|
||||
pattern: '@:scope/:packageName/:tag',
|
||||
namedParams: { scope: 'stdlib', packageName: 'stdlib', tag: 'latest' },
|
||||
pattern: ':scope/:packageName/:tag',
|
||||
namedParams: { scope: '@stdlib', packageName: 'stdlib', tag: 'latest' },
|
||||
queryParams: { registry_uri: 'https://registry.npmjs.com' },
|
||||
staticPreview: this.renderStaticPreview({
|
||||
nodeVersionRange: '>= 6.0.0',
|
||||
|
||||
@@ -23,32 +23,36 @@ export default class NpmsIOScore extends BaseJsonService {
|
||||
static route = {
|
||||
base: 'npms-io',
|
||||
pattern:
|
||||
':type(final|maintenance|popularity|quality)-score/:scope(@.+)?/:packageName',
|
||||
':type(final-score|maintenance-score|popularity-score|quality-score)/:scope(@.+)?/:packageName',
|
||||
}
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'npms.io (final)',
|
||||
namedParams: { type: 'final', packageName: 'egg' },
|
||||
namedParams: { type: 'final-score', packageName: 'egg' },
|
||||
staticPreview: this.render({ score: 0.9711 }),
|
||||
keywords,
|
||||
},
|
||||
{
|
||||
title: 'npms.io (popularity)',
|
||||
pattern: ':type/:scope/:packageName',
|
||||
namedParams: { type: 'popularity', scope: '@vue', packageName: 'cli' },
|
||||
namedParams: {
|
||||
type: 'popularity-score',
|
||||
scope: '@vue',
|
||||
packageName: 'cli',
|
||||
},
|
||||
staticPreview: this.render({ type: 'popularity', score: 0.89 }),
|
||||
keywords,
|
||||
},
|
||||
{
|
||||
title: 'npms.io (quality)',
|
||||
namedParams: { type: 'quality', packageName: 'egg' },
|
||||
namedParams: { type: 'quality-score', packageName: 'egg' },
|
||||
staticPreview: this.render({ type: 'quality', score: 0.98 }),
|
||||
keywords,
|
||||
},
|
||||
{
|
||||
title: 'npms.io (maintenance)',
|
||||
namedParams: { type: 'maintenance', packageName: 'command' },
|
||||
namedParams: { type: 'maintenance-score', packageName: 'command' },
|
||||
staticPreview: this.render({ type: 'maintenance', score: 0.222 }),
|
||||
keywords,
|
||||
},
|
||||
@@ -76,8 +80,10 @@ export default class NpmsIOScore extends BaseJsonService {
|
||||
errorMessages: { 404: 'package not found or too new' },
|
||||
})
|
||||
|
||||
const score = type === 'final' ? json.score.final : json.score.detail[type]
|
||||
const scoreType = type.slice(0, -6)
|
||||
const score =
|
||||
scoreType === 'final' ? json.score.final : json.score.detail[scoreType]
|
||||
|
||||
return this.constructor.render({ type, score })
|
||||
return this.constructor.render({ type: scoreType, score })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export default class PypiBase extends BaseJsonService {
|
||||
static buildRoute(base) {
|
||||
return {
|
||||
base,
|
||||
pattern: ':egg*',
|
||||
pattern: ':egg+',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ export default class PypiFrameworkVersion extends PypiBase {
|
||||
base: 'pypi/frameworkversions',
|
||||
pattern: `:frameworkName(${Object.keys(frameworkNameMap).join(
|
||||
'|'
|
||||
)})/:packageName*`,
|
||||
)})/:packageName+`,
|
||||
}
|
||||
|
||||
static examples = [
|
||||
|
||||
@@ -31,8 +31,7 @@ export default class SonarFortifyRating extends SonarBase {
|
||||
},
|
||||
staticPreview: this.render({ rating: 4 }),
|
||||
keywords,
|
||||
documentation: `
|
||||
<p>
|
||||
documentation: `<p>
|
||||
Note that the Fortify Security Rating badge will only work on Sonar instances that have the <a href='https://marketplace.microfocus.com/fortify/content/fortify-sonarqube-plugin'>Fortify SonarQube Plugin</a> installed.
|
||||
The badge is not available for projects analyzed on SonarCloud.io
|
||||
</p>
|
||||
|
||||
@@ -47,15 +47,14 @@ const queryParamWithFormatSchema = Joi.object({
|
||||
}).required()
|
||||
|
||||
const keywords = ['sonarcloud', 'sonarqube']
|
||||
const documentation = `
|
||||
<p>
|
||||
const documentation = `<p>
|
||||
The Sonar badges will work with both SonarCloud.io and self-hosted SonarQube instances.
|
||||
Just enter the correct protocol and path for your target Sonar deployment.
|
||||
</p>
|
||||
<p>
|
||||
If you are targeting a legacy SonarQube instance that is version 5.3 or earlier, then be sure
|
||||
to include the version query parameter with the value of your SonarQube version.
|
||||
</p
|
||||
</p>
|
||||
`
|
||||
|
||||
export {
|
||||
|
||||
@@ -43,8 +43,7 @@ class SonarTestsSummary extends SonarBase {
|
||||
isCompact: false,
|
||||
}),
|
||||
keywords,
|
||||
documentation: `
|
||||
${documentation}
|
||||
documentation: `${documentation}
|
||||
${testResultsDocumentation}
|
||||
`,
|
||||
},
|
||||
|
||||
@@ -1,15 +1,73 @@
|
||||
import { escapeFormat } from '../../core/badge-urls/path-helpers.js'
|
||||
import { BaseStaticService } from '../index.js'
|
||||
|
||||
const description = `<p>
|
||||
The static badge accepts a single required path parameter which encodes either:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Label, message and color seperated by a dash <code>-</code>. For example:<br />
|
||||
<img alt="any text: you like" src="https://img.shields.io/badge/any_text-you_like-blue" /> -
|
||||
<a href="https://img.shields.io/badge/any_text-you_like-blue">https://img.shields.io/badge/any_text-you_like-blue</a>
|
||||
</li>
|
||||
<li>
|
||||
Message and color only, seperated by a dash <code>-</code>. For example:<br />
|
||||
<img alt="just the message" src="https://img.shields.io/badge/just%20the%20message-8A2BE2" /> -
|
||||
<a href="https://img.shields.io/badge/just%20the%20message-8A2BE2">https://img.shields.io/badge/just%20the%20message-8A2BE2</a>
|
||||
</li>
|
||||
</ul>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>URL input</th>
|
||||
<th>Badge output</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Underscore <code>_</code> or <code>%20</code></td>
|
||||
<td>Space <code> </code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Double underscore <code>__</code></td>
|
||||
<td>Underscore <code>_</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Double dash <code>--</code></td>
|
||||
<td>Dash <code>-</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
Hex, rgb, rgba, hsl, hsla and css named colors may be used.
|
||||
</p>`
|
||||
|
||||
export default class StaticBadge extends BaseStaticService {
|
||||
static category = 'static'
|
||||
|
||||
static route = {
|
||||
base: '',
|
||||
format: '(?::|badge/)((?:[^-]|--)*?)-?((?:[^-]|--)*)-((?:[^-.]|--)+)',
|
||||
capture: ['label', 'message', 'color'],
|
||||
}
|
||||
|
||||
static openApi = {
|
||||
'/badge/{badgeContent}': {
|
||||
get: {
|
||||
summary: 'Static Badge',
|
||||
description,
|
||||
parameters: [
|
||||
{
|
||||
name: 'badgeContent',
|
||||
description:
|
||||
'Label, (optional) message, and color. Seperated by dashes',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'build-passing-brightgreen',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
handle({ label, message, color }) {
|
||||
return { label: escapeFormat(label), message: escapeFormat(message), color }
|
||||
}
|
||||
|
||||
17
services/vcpkg/vcpkg-version-helpers.js
Normal file
17
services/vcpkg/vcpkg-version-helpers.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { InvalidResponse } from '../index.js'
|
||||
|
||||
export function parseVersionFromVcpkgManifest(manifest) {
|
||||
if (manifest['version-date']) {
|
||||
return manifest['version-date']
|
||||
}
|
||||
if (manifest['version-semver']) {
|
||||
return manifest['version-semver']
|
||||
}
|
||||
if (manifest['version-string']) {
|
||||
return manifest['version-string']
|
||||
}
|
||||
if (manifest.version) {
|
||||
return manifest.version
|
||||
}
|
||||
throw new InvalidResponse({ prettyMessage: 'missing version' })
|
||||
}
|
||||
41
services/vcpkg/vcpkg-version-helpers.spec.js
Normal file
41
services/vcpkg/vcpkg-version-helpers.spec.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { expect } from 'chai'
|
||||
import { InvalidResponse } from '../index.js'
|
||||
import { parseVersionFromVcpkgManifest } from './vcpkg-version-helpers.js'
|
||||
|
||||
describe('parseVersionFromVcpkgManifest', function () {
|
||||
it('returns a version when `version` field is detected', function () {
|
||||
expect(
|
||||
parseVersionFromVcpkgManifest({
|
||||
version: '2.12.1',
|
||||
})
|
||||
).to.equal('2.12.1')
|
||||
})
|
||||
|
||||
it('returns a version when `version-date` field is detected', function () {
|
||||
expect(
|
||||
parseVersionFromVcpkgManifest({
|
||||
'version-date': '2022-12-04',
|
||||
})
|
||||
).to.equal('2022-12-04')
|
||||
})
|
||||
|
||||
it('returns a version when `version-semver` field is detected', function () {
|
||||
expect(
|
||||
parseVersionFromVcpkgManifest({
|
||||
'version-semver': '3.11.2',
|
||||
})
|
||||
).to.equal('3.11.2')
|
||||
})
|
||||
|
||||
it('returns a version when `version-date` field is detected', function () {
|
||||
expect(
|
||||
parseVersionFromVcpkgManifest({
|
||||
'version-string': '22.01',
|
||||
})
|
||||
).to.equal('22.01')
|
||||
})
|
||||
|
||||
it('rejects when no version field variant is detected', function () {
|
||||
expect(() => parseVersionFromVcpkgManifest('{}')).to.throw(InvalidResponse)
|
||||
})
|
||||
})
|
||||
@@ -3,10 +3,26 @@ import { ConditionalGithubAuthV3Service } from '../github/github-auth-service.js
|
||||
import { fetchJsonFromRepo } from '../github/github-common-fetch.js'
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
import { NotFound } from '../index.js'
|
||||
import { parseVersionFromVcpkgManifest } from './vcpkg-version-helpers.js'
|
||||
|
||||
const vcpkgManifestSchema = Joi.object({
|
||||
version: Joi.string().required(),
|
||||
}).required()
|
||||
// Handle the different version fields available in Vcpkg manifests
|
||||
// https://learn.microsoft.com/en-us/vcpkg/reference/vcpkg-json?source=recommendations#version
|
||||
const vcpkgManifestSchema = Joi.alternatives()
|
||||
.match('one')
|
||||
.try(
|
||||
Joi.object({
|
||||
version: Joi.string().required(),
|
||||
}).required(),
|
||||
Joi.object({
|
||||
'version-date': Joi.string().required(),
|
||||
}).required(),
|
||||
Joi.object({
|
||||
'version-semver': Joi.string().required(),
|
||||
}).required(),
|
||||
Joi.object({
|
||||
'version-string': Joi.string().required(),
|
||||
}).required()
|
||||
)
|
||||
|
||||
export default class VcpkgVersion extends ConditionalGithubAuthV3Service {
|
||||
static category = 'version'
|
||||
@@ -29,13 +45,14 @@ export default class VcpkgVersion extends ConditionalGithubAuthV3Service {
|
||||
|
||||
async handle({ portName }) {
|
||||
try {
|
||||
const { version } = await fetchJsonFromRepo(this, {
|
||||
const manifest = await fetchJsonFromRepo(this, {
|
||||
schema: vcpkgManifestSchema,
|
||||
user: 'microsoft',
|
||||
repo: 'vcpkg',
|
||||
branch: 'master',
|
||||
filename: `ports/${portName}/vcpkg.json`,
|
||||
})
|
||||
const version = parseVersionFromVcpkgManifest(manifest)
|
||||
return this.constructor.render({ version })
|
||||
} catch (error) {
|
||||
if (error instanceof NotFound) {
|
||||
|
||||
@@ -3,11 +3,11 @@ import { createServiceTester } from '../tester.js'
|
||||
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('gets the port version of entt')
|
||||
.get('/entt.json')
|
||||
.expectBadge({ label: 'vcpkg', message: isSemver })
|
||||
t.create('gets nlohmann-json port version')
|
||||
.get('/nlohmann-json.json')
|
||||
.expectBadge({ label: 'vcpkg', color: 'blue', message: isSemver })
|
||||
|
||||
t.create('returns not found for invalid port')
|
||||
t.create('gets not found error for invalid port')
|
||||
.get('/this-port-does-not-exist.json')
|
||||
.expectBadge({
|
||||
label: 'vcpkg',
|
||||
|
||||
@@ -16,7 +16,7 @@ const extensionQuerySchema = Joi.object({
|
||||
value: Joi.number().required(),
|
||||
})
|
||||
)
|
||||
.required(),
|
||||
.default([]),
|
||||
versions: Joi.array()
|
||||
.items(
|
||||
Joi.object({
|
||||
|
||||
@@ -101,6 +101,35 @@ t.create('zero installs')
|
||||
color: 'red',
|
||||
})
|
||||
|
||||
t.create('missing statistics array')
|
||||
.get('/visual-studio-marketplace/i/swellaby.rust-pack.json')
|
||||
.intercept(nock =>
|
||||
nock('https://marketplace.visualstudio.com/_apis/public/gallery/')
|
||||
.post('/extensionquery/')
|
||||
.reply(200, {
|
||||
results: [
|
||||
{
|
||||
extensions: [
|
||||
{
|
||||
versions: [
|
||||
{
|
||||
version: '1.0.0',
|
||||
},
|
||||
],
|
||||
releaseDate: '2019-04-13T07:50:27.000Z',
|
||||
lastUpdated: '2019-04-13T07:50:27.000Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'installs',
|
||||
message: '0',
|
||||
color: 'red',
|
||||
})
|
||||
|
||||
t.create('downloads')
|
||||
.get('/visual-studio-marketplace/d/swellaby.rust-pack.json')
|
||||
.intercept(nock =>
|
||||
|
||||
Reference in New Issue
Block a user