Compare commits
118 Commits
server-202
...
server-202
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1954963cd3 | ||
|
|
fbb2ff3619 | ||
|
|
9e29d3d3f2 | ||
|
|
1fad0888b2 | ||
|
|
4bf0675de2 | ||
|
|
4fb2896597 | ||
|
|
647a466399 | ||
|
|
bf15ad1f3a | ||
|
|
202d72d365 | ||
|
|
efa705d925 | ||
|
|
5234ecde93 | ||
|
|
345da16365 | ||
|
|
37a36f5fea | ||
|
|
3f63990049 | ||
|
|
0605a074e9 | ||
|
|
74ada5af29 | ||
|
|
04d5a9d4d1 | ||
|
|
f185167b7d | ||
|
|
567cae8e9c | ||
|
|
b7da0f8a56 | ||
|
|
19363f2245 | ||
|
|
ba6fdfb5a6 | ||
|
|
9bfca5a575 | ||
|
|
324e6be067 | ||
|
|
15a7b074f4 | ||
|
|
35fbbad90e | ||
|
|
fe8031af27 | ||
|
|
5cc21ce040 | ||
|
|
7621603bef | ||
|
|
786149ccbe | ||
|
|
60125707ad | ||
|
|
c64bb7e1f3 | ||
|
|
e1c1a021f9 | ||
|
|
daf5ad5b2a | ||
|
|
a9e67c8b19 | ||
|
|
62cabe0b32 | ||
|
|
ae7960244d | ||
|
|
e8686395b1 | ||
|
|
6a77a5991a | ||
|
|
7cfd3f5d25 | ||
|
|
a668340706 | ||
|
|
9f11656dae | ||
|
|
b6f198e718 | ||
|
|
acf77ac62f | ||
|
|
7ea9e01194 | ||
|
|
cd6527d82b | ||
|
|
e9c08512ce | ||
|
|
8f03cf6025 | ||
|
|
241790631b | ||
|
|
9af4fa926b | ||
|
|
ae1be3060e | ||
|
|
0c6886dc50 | ||
|
|
076ee25bfb | ||
|
|
3998055a3e | ||
|
|
27fd324f15 | ||
|
|
7d088eb725 | ||
|
|
2fb331fcc0 | ||
|
|
613cfff7bc | ||
|
|
166dcb2c84 | ||
|
|
3719eaf0c2 | ||
|
|
e987117dbc | ||
|
|
4e62a5583c | ||
|
|
8ab0225997 | ||
|
|
3c6ca5d59c | ||
|
|
0ceb558bb4 | ||
|
|
65d12efe32 | ||
|
|
16fdf3251b | ||
|
|
7aab131bc2 | ||
|
|
f55d9057a7 | ||
|
|
ff66e1db7d | ||
|
|
f2b7da902e | ||
|
|
01b5ab7f4e | ||
|
|
ce4c59cb56 | ||
|
|
685a9d0219 | ||
|
|
1468601294 | ||
|
|
c87a215d0e | ||
|
|
e7aecb1a3b | ||
|
|
347e3839b8 | ||
|
|
fd123f7238 | ||
|
|
bb81467f9a | ||
|
|
2374962dc0 | ||
|
|
e4ad888bf9 | ||
|
|
0ae55c7e32 | ||
|
|
2818894143 | ||
|
|
b9c63ec687 | ||
|
|
e388608d96 | ||
|
|
d41e5ed347 | ||
|
|
d8a302eb36 | ||
|
|
f4f80dabf9 | ||
|
|
11812beb46 | ||
|
|
dff38affa2 | ||
|
|
ec1ff376a5 | ||
|
|
8f7164f96b | ||
|
|
42233228b4 | ||
|
|
00e9f2dfaa | ||
|
|
51748c6faf | ||
|
|
2d7d8c5b78 | ||
|
|
5e6d68123a | ||
|
|
6e4c46143f | ||
|
|
f419b39b09 | ||
|
|
fe5d4514ee | ||
|
|
60e75963a8 | ||
|
|
e965324911 | ||
|
|
781aedc353 | ||
|
|
3cf154053f | ||
|
|
8bf34a2307 | ||
|
|
d251b481f4 | ||
|
|
dc06b445c9 | ||
|
|
667a609b6e | ||
|
|
1f2ba9eba6 | ||
|
|
c07d0f901a | ||
|
|
bcdc02510d | ||
|
|
45331d1d1f | ||
|
|
a2376d1d21 | ||
|
|
7b40bd78d5 | ||
|
|
2960f3a30b | ||
|
|
888d47253c | ||
|
|
0a26450f7a |
@@ -149,6 +149,8 @@ jobs:
|
||||
main@node-17:
|
||||
docker:
|
||||
- image: cimg/node:17.9
|
||||
environment:
|
||||
NPM_CONFIG_ENGINE_STRICT: 'false'
|
||||
|
||||
<<: *main_steps
|
||||
|
||||
@@ -163,6 +165,8 @@ jobs:
|
||||
docker:
|
||||
- image: cimg/node:17.9
|
||||
- image: redis
|
||||
environment:
|
||||
NPM_CONFIG_ENGINE_STRICT: 'false'
|
||||
|
||||
<<: *integration_steps
|
||||
|
||||
@@ -239,6 +243,8 @@ jobs:
|
||||
services@node-17:
|
||||
docker:
|
||||
- image: cimg/node:17.9
|
||||
environment:
|
||||
NPM_CONFIG_ENGINE_STRICT: 'false'
|
||||
|
||||
<<: *services_steps
|
||||
|
||||
|
||||
14
.github/actions/close-bot/package-lock.json
generated
vendored
14
.github/actions/close-bot/package-lock.json
generated
vendored
@@ -9,14 +9,14 @@
|
||||
"version": "0.0.0",
|
||||
"license": "CC0",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.8.2",
|
||||
"@actions/core": "^1.9.0",
|
||||
"@actions/github": "^5.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/core": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.8.2.tgz",
|
||||
"integrity": "sha512-FXcBL7nyik8K5ODeCKlxi+vts7torOkoDAKfeh61EAkAy1HAvwn9uVzZBY0f15YcQTcZZ2/iSGBFHEuioZWfDA==",
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.0.tgz",
|
||||
"integrity": "sha512-5pbM693Ih59ZdUhgk+fts+bUWTnIdHV3kwOSr+QIoFHMLg7Gzhwm0cifDY/AG68ekEJAkHnQVpcy4f6GjmzBCA==",
|
||||
"dependencies": {
|
||||
"@actions/http-client": "^2.0.1"
|
||||
}
|
||||
@@ -226,9 +226,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.8.2.tgz",
|
||||
"integrity": "sha512-FXcBL7nyik8K5ODeCKlxi+vts7torOkoDAKfeh61EAkAy1HAvwn9uVzZBY0f15YcQTcZZ2/iSGBFHEuioZWfDA==",
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.0.tgz",
|
||||
"integrity": "sha512-5pbM693Ih59ZdUhgk+fts+bUWTnIdHV3kwOSr+QIoFHMLg7Gzhwm0cifDY/AG68ekEJAkHnQVpcy4f6GjmzBCA==",
|
||||
"requires": {
|
||||
"@actions/http-client": "^2.0.1"
|
||||
}
|
||||
|
||||
2
.github/actions/close-bot/package.json
vendored
2
.github/actions/close-bot/package.json
vendored
@@ -10,7 +10,7 @@
|
||||
"author": "chris48s",
|
||||
"license": "CC0",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.8.2",
|
||||
"@actions/core": "^1.9.0",
|
||||
"@actions/github": "^5.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,4 +8,4 @@ jobs:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v1
|
||||
uses: actions/dependency-review-action@v2
|
||||
|
||||
26
CHANGELOG.md
26
CHANGELOG.md
@@ -4,6 +4,32 @@ Note: this changelog is for the shields.io server. The changelog for the badge-m
|
||||
|
||||
---
|
||||
|
||||
## server-2022-08-01
|
||||
|
||||
- [pypi] Add Framework Version Badges support [#8261](https://github.com/badges/shields/issues/8261)
|
||||
- feat: add [GitlabForks] server [#8208](https://github.com/badges/shields/issues/8208)
|
||||
- Update PyPI api according to https://warehouse.pypa.io/api-reference/json.html [#8251](https://github.com/badges/shields/issues/8251)
|
||||
- Add [galaxytoolshed] Activity [#8164](https://github.com/badges/shields/issues/8164)
|
||||
- [greasyfork] Add Greasy Fork rating badges [#8087](https://github.com/badges/shields/issues/8087)
|
||||
- refactor(deps): Replace moment with dayjs [#8192](https://github.com/badges/shields/issues/8192)
|
||||
- add spaces round pipe in [conda] badge [#8189](https://github.com/badges/shields/issues/8189)
|
||||
- Add [ROS] version service [#8169](https://github.com/badges/shields/issues/8169)
|
||||
- feat: add [gitlabissues] service [#8108](https://github.com/badges/shields/issues/8108)
|
||||
- Dependency updates
|
||||
|
||||
## server-2022-07-03
|
||||
|
||||
- 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)
|
||||
- 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)
|
||||
- feat: add [gitlab] contributors service [#8084](https://github.com/badges/shields/issues/8084)
|
||||
- [greasyfork] Add Greasy Fork service badges [#8080](https://github.com/badges/shields/issues/8080)
|
||||
- Add [gitlablicense] services [#8024](https://github.com/badges/shields/issues/8024)
|
||||
- [Spack] Package Manager: Update Domain [#8046](https://github.com/badges/shields/issues/8046)
|
||||
- switch [jitpack] to use latestOk endpoint [#8041](https://github.com/badges/shields/issues/8041)
|
||||
- Dependency updates
|
||||
|
||||
## server-2022-06-01
|
||||
|
||||
- Update GitLab logo (2022) [#7984](https://github.com/badges/shields/issues/7984)
|
||||
|
||||
10
app.json
10
app.json
@@ -35,6 +35,16 @@
|
||||
"WEBLATE_API_KEY": {
|
||||
"description": "Configure the API key to be used for the Weblate service.",
|
||||
"required": false
|
||||
},
|
||||
"METRICS_INFLUX_ENABLED": {
|
||||
"description": "Disable influx metrics",
|
||||
"value": "false",
|
||||
"required": false
|
||||
},
|
||||
"REQUIRE_CLOUDFLARE": {
|
||||
"description": "Allow direct traffic",
|
||||
"value": "false",
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"formation": {
|
||||
|
||||
@@ -59,7 +59,9 @@ function _inferPullRequestFromTravisEnv(env) {
|
||||
}
|
||||
|
||||
function _inferPullRequestFromCircleEnv(env) {
|
||||
return parseGithubPullRequestUrl(env.CI_PULL_REQUEST)
|
||||
return parseGithubPullRequestUrl(
|
||||
env.CI_PULL_REQUEST || env.CIRCLE_PULL_REQUEST
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
13
cypress.config.js
Normal file
13
cypress.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'cypress'
|
||||
|
||||
export default defineConfig({
|
||||
fixturesFolder: false,
|
||||
env: {
|
||||
backend_url: 'http://localhost:8080',
|
||||
},
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {},
|
||||
baseUrl: 'http://localhost:3000',
|
||||
supportFile: false,
|
||||
},
|
||||
})
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"baseUrl": "http://localhost:3000",
|
||||
"fixturesFolder": false,
|
||||
"pluginsFile": false,
|
||||
"supportFile": false,
|
||||
"env": {
|
||||
"backend_url": "http://localhost:8080"
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,9 @@
|
||||
- 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:
|
||||
|
||||
@@ -58,7 +58,7 @@ The tests are also divided into several parts:
|
||||
[redis-token-persistence.integration]: https://github.com/badges/shields/blob/master/core/token-pooling/redis-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 for the core code is to reach 100% coverage of the code in the
|
||||
Our goal is to reach 100% coverage of the code in the
|
||||
frontend, core, and service helper functions when the unit and functional
|
||||
tests are run.
|
||||
|
||||
@@ -95,7 +95,7 @@ test this kind of logic through unit tests (e.g. of `render()` and
|
||||
callback with the four parameters `( queryParams, match, end, ask )` which
|
||||
is created in a legacy helper function in
|
||||
[`legacy-request-handler.js`][legacy-request-handler]. This callback
|
||||
delegates to a callback in `BaseService.register` with four different
|
||||
delegates to a callback in `BaseService.register` with three different
|
||||
parameters `( queryParams, match, sendBadge )`, which
|
||||
then runs `BaseService.invoke`. `BaseService.invoke` instantiates the
|
||||
service and runs `BaseService#handle`.
|
||||
|
||||
@@ -67,7 +67,7 @@ t.create('Build status')
|
||||
- All badges on shields can be requested in a number of formats. As well as calling https://img.shields.io/wercker/build/wercker/go-wercker-api.svg to generate  we can also call https://img.shields.io/wercker/build/wercker/go-wercker-api.json to request the same content as JSON. When writing service tests, we request the badge in JSON format so it is easier to make assertions about the content.
|
||||
- We don't need to explicitly call `/wercker/build/wercker/go-wercker-api.json` here, only `/build/wercker/go-wercker-api.json`. When we create a tester object with `createServiceTester()` the URL base defined in our service class (in this case `/wercker`) is used as the base URL for any requests made by the tester object.
|
||||
3. `expectBadge()` is a helper function which accepts either a string literal, a [RegExp][] or a [Joi][] schema for the different fields.
|
||||
Joi is a validation library that is build into IcedFrisby which you can use to
|
||||
Joi is a validation library that is built into IcedFrisby which you can use to
|
||||
match based on a set of allowed strings, regexes, or specific values. You can
|
||||
refer to their [API reference][joi api].
|
||||
4. We expect `label` to be a string literal `"build"`.
|
||||
|
||||
@@ -134,7 +134,7 @@ export default function EndpointPage(): JSX.Element {
|
||||
</p>
|
||||
<p>
|
||||
The endpoint badge is a better alternative than redirecting to the
|
||||
static badge enpoint or generating SVG on your server:
|
||||
static badge endpoint or generating SVG on your server:
|
||||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
@@ -142,7 +142,7 @@ export default function EndpointPage(): JSX.Element {
|
||||
Content and presentation are separate.
|
||||
</a>{' '}
|
||||
The service provider authors the badge, and Shields takes input from
|
||||
the user to format it. As a service provider you author the badge
|
||||
the user to format it. As a service provider, you author the badge
|
||||
but don't have to concern yourself with styling. You don't even have
|
||||
to pass the formatting options through to Shields.
|
||||
</li>
|
||||
@@ -152,12 +152,12 @@ export default function EndpointPage(): JSX.Element {
|
||||
</li>
|
||||
<li>
|
||||
A JSON response is easy to implement; easier than an HTTP redirect.
|
||||
It is trivial in almost any framework, and is more compatible with
|
||||
It is trivial in almost any framework and is more compatible with
|
||||
hosting environments such as{' '}
|
||||
<a href="https://runkit.com/docs/endpoint">RunKit endpoints</a>.
|
||||
</li>
|
||||
<li>
|
||||
As a service provider you can rely on the Shields CDN. There's no
|
||||
As a service provider, you can rely on the Shields CDN. There's no
|
||||
need to study the HTTP headers. Adjusting cache behavior is as
|
||||
simple as setting a property in the JSON response.
|
||||
</li>
|
||||
@@ -197,7 +197,7 @@ export default function EndpointPage(): JSX.Element {
|
||||
<dd>
|
||||
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.
|
||||
future, it may affect cache behavior.
|
||||
</dd>
|
||||
<dt>namedLogo</dt>
|
||||
<dd>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import originalSimpleIcons from 'simple-icons'
|
||||
import * as originalSimpleIcons from 'simple-icons/icons'
|
||||
import { svg2base64 } from './svg-helpers.js'
|
||||
|
||||
function loadSimpleIcons() {
|
||||
@@ -14,10 +14,10 @@ function loadSimpleIcons() {
|
||||
// https://github.com/badges/shields/issues/4273
|
||||
Object.keys(originalSimpleIcons).forEach(key => {
|
||||
const icon = originalSimpleIcons[key]
|
||||
const title = icon.title.toLowerCase()
|
||||
const legacyTitle = title.replace(/ /g, '-')
|
||||
const { title, slug, hex } = icon
|
||||
|
||||
icon.base64 = {
|
||||
default: svg2base64(icon.svg.replace('<svg', `<svg fill="#${icon.hex}"`)),
|
||||
default: svg2base64(icon.svg.replace('<svg', `<svg fill="#${hex}"`)),
|
||||
light: svg2base64(icon.svg.replace('<svg', `<svg fill="whitesmoke"`)),
|
||||
dark: svg2base64(icon.svg.replace('<svg', `<svg fill="#333"`)),
|
||||
}
|
||||
@@ -26,14 +26,17 @@ function loadSimpleIcons() {
|
||||
// (e.g. 'Hive'). If a by-title reference we generate for
|
||||
// backwards compatibility collides with a proper slug from Simple Icons
|
||||
// then do nothing, so that the proper slug will always map to the correct icon.
|
||||
if (!(title in originalSimpleIcons)) {
|
||||
simpleIcons[title] = icon
|
||||
// Starting in v7, the exported object with the full icon set has updated the keys
|
||||
// to include a lowercase `si` prefix, and utilizes proper case naming conventions.
|
||||
if (!(`si${title}` in originalSimpleIcons)) {
|
||||
simpleIcons[title.toLowerCase()] = icon
|
||||
}
|
||||
if (!(legacyTitle in originalSimpleIcons)) {
|
||||
simpleIcons[legacyTitle] = icon
|
||||
const legacyTitle = title.replace(/ /g, '-')
|
||||
if (!(`si${legacyTitle}` in originalSimpleIcons)) {
|
||||
simpleIcons[legacyTitle.toLowerCase()] = icon
|
||||
}
|
||||
|
||||
simpleIcons[key] = icon
|
||||
simpleIcons[slug] = icon
|
||||
})
|
||||
return simpleIcons
|
||||
}
|
||||
|
||||
2583
package-lock.json
generated
2583
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
64
package.json
64
package.json
@@ -24,26 +24,27 @@
|
||||
"@fontsource/lato": "^4.5.8",
|
||||
"@fontsource/lekton": "^4.5.9",
|
||||
"@renovate/pep440": "^1.0.0",
|
||||
"@sentry/node": "^6.19.7",
|
||||
"@sentry/node": "^7.8.0",
|
||||
"@shields_io/camp": "^18.1.1",
|
||||
"badge-maker": "file:badge-maker",
|
||||
"bytes": "^3.1.2",
|
||||
"camelcase": "^6.3.0",
|
||||
"camelcase": "^7.0.0",
|
||||
"chalk": "^5.0.1",
|
||||
"check-node-version": "^4.2.1",
|
||||
"cloudflare-middleware": "^1.0.4",
|
||||
"config": "^3.3.7",
|
||||
"cross-env": "^7.0.3",
|
||||
"dayjs": "^1.11.4",
|
||||
"decamelize": "^3.2.0",
|
||||
"emojic": "^1.1.17",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"fast-xml-parser": "^4.0.7",
|
||||
"fast-xml-parser": "^4.0.9",
|
||||
"glob": "^8.0.3",
|
||||
"global-agent": "^3.0.0",
|
||||
"got": "^12.1.0",
|
||||
"got": "^12.3.0",
|
||||
"graphql": "^15.6.1",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"ioredis": "5.0.5",
|
||||
"ioredis": "5.2.2",
|
||||
"joi": "17.6.0",
|
||||
"joi-extension-semver": "5.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
@@ -51,17 +52,16 @@
|
||||
"lodash.countby": "^4.6.0",
|
||||
"lodash.groupby": "^4.6.0",
|
||||
"lodash.times": "^4.3.2",
|
||||
"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.10.3",
|
||||
"qs": "^6.11.0",
|
||||
"query-string": "^7.1.1",
|
||||
"semver": "~7.3.7",
|
||||
"simple-icons": "6.23.0",
|
||||
"simple-icons": "7.5.0",
|
||||
"webextension-store-meta": "^1.0.5",
|
||||
"xmldom": "~0.6.0",
|
||||
"xpath": "~0.0.32"
|
||||
@@ -141,9 +141,9 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.2",
|
||||
"@babel/core": "^7.18.9",
|
||||
"@babel/polyfill": "^7.12.1",
|
||||
"@babel/register": "7.17.7",
|
||||
"@babel/register": "7.18.9",
|
||||
"@istanbuljs/schema": "^0.1.3",
|
||||
"@mapbox/react-click-to-select": "^2.2.1",
|
||||
"@types/chai": "^4.3.1",
|
||||
@@ -155,11 +155,11 @@
|
||||
"@types/react-modal": "^3.13.1",
|
||||
"@types/react-select": "^4.0.17",
|
||||
"@types/styled-components": "5.1.25",
|
||||
"@typescript-eslint/eslint-plugin": "^5.26.0",
|
||||
"@typescript-eslint/parser": "^5.15.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.31.0",
|
||||
"@typescript-eslint/parser": "^5.30.7",
|
||||
"babel-plugin-inline-react-svg": "^2.0.1",
|
||||
"babel-preset-gatsby": "^2.14.0",
|
||||
"c8": "^7.11.3",
|
||||
"babel-preset-gatsby": "^2.19.0",
|
||||
"c8": "^7.12.0",
|
||||
"caller": "^1.1.0",
|
||||
"chai": "^4.3.6",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
@@ -167,9 +167,9 @@
|
||||
"chai-string": "^1.4.0",
|
||||
"child-process-promise": "^2.2.1",
|
||||
"clipboard-copy": "^4.0.1",
|
||||
"concurrently": "^7.2.1",
|
||||
"cypress": "^9.7.0",
|
||||
"danger": "^11.0.7",
|
||||
"concurrently": "^7.3.0",
|
||||
"cypress": "^10.3.1",
|
||||
"danger": "^11.1.1",
|
||||
"danger-plugin-no-test-shortcuts": "^2.0.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"eslint": "^7.32.0",
|
||||
@@ -180,30 +180,30 @@
|
||||
"eslint-plugin-chai-friendly": "^0.7.2",
|
||||
"eslint-plugin-cypress": "^2.12.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsdoc": "^39.3.2",
|
||||
"eslint-plugin-mocha": "^10.0.5",
|
||||
"eslint-plugin-jsdoc": "^39.3.3",
|
||||
"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.30.0",
|
||||
"eslint-plugin-react-hooks": "^4.5.0",
|
||||
"eslint-plugin-react": "^7.30.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-sort-class-members": "^1.14.1",
|
||||
"fetch-ponyfill": "^7.1.0",
|
||||
"form-data": "^4.0.0",
|
||||
"gatsby": "4.6.2",
|
||||
"gatsby-plugin-catch-links": "^4.11.0",
|
||||
"gatsby-plugin-catch-links": "^4.19.0",
|
||||
"gatsby-plugin-page-creator": "^4.7.0",
|
||||
"gatsby-plugin-react-helmet": "^5.10.0",
|
||||
"gatsby-plugin-remove-trailing-slashes": "^4.9.0",
|
||||
"gatsby-plugin-styled-components": "^5.11.0",
|
||||
"gatsby-plugin-styled-components": "^5.19.0",
|
||||
"gatsby-plugin-typescript": "^4.11.1",
|
||||
"humanize-string": "^2.1.0",
|
||||
"icedfrisby": "4.0.0",
|
||||
"icedfrisby-nock": "^2.1.0",
|
||||
"is-svg": "^4.3.2",
|
||||
"js-yaml-loader": "^1.2.2",
|
||||
"jsdoc": "^3.6.10",
|
||||
"lint-staged": "^12.4.2",
|
||||
"jsdoc": "^3.6.11",
|
||||
"lint-staged": "^13.0.3",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.difference": "^4.5.0",
|
||||
"minimist": "^1.2.6",
|
||||
@@ -211,13 +211,13 @@
|
||||
"mocha-env-reporter": "^4.0.0",
|
||||
"mocha-junit-reporter": "^2.0.2",
|
||||
"mocha-yaml-loader": "^1.0.3",
|
||||
"nock": "13.2.4",
|
||||
"nock": "13.2.9",
|
||||
"node-mocks-http": "^1.11.0",
|
||||
"nodemon": "^2.0.16",
|
||||
"nodemon": "^2.0.19",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"open-cli": "^7.0.1",
|
||||
"portfinder": "^1.0.28",
|
||||
"prettier": "2.6.2",
|
||||
"prettier": "2.7.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-error-overlay": "^6.0.11",
|
||||
@@ -229,19 +229,19 @@
|
||||
"redis-server": "^1.2.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"sazerac": "^2.0.0",
|
||||
"simple-git-hooks": "^2.7.0",
|
||||
"simple-git-hooks": "^2.8.0",
|
||||
"sinon": "^14.0.0",
|
||||
"sinon-chai": "^3.7.0",
|
||||
"snap-shot-it": "^7.9.6",
|
||||
"start-server-and-test": "1.14.0",
|
||||
"styled-components": "^5.3.5",
|
||||
"ts-mocha": "^10.0.0",
|
||||
"tsd": "^0.20.0",
|
||||
"typescript": "^4.7.2",
|
||||
"tsd": "^0.22.0",
|
||||
"typescript": "^4.7.4",
|
||||
"url": "^0.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.13.0",
|
||||
"node": "^16.13.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"type": "module",
|
||||
|
||||
@@ -108,9 +108,7 @@ class AurVotes extends BaseAurService {
|
||||
|
||||
class AurVersion extends BaseAurService {
|
||||
static category = 'version'
|
||||
|
||||
static route = { base: 'aur/version', pattern: ':packageName' }
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'AUR version',
|
||||
@@ -119,6 +117,8 @@ class AurVersion extends BaseAurService {
|
||||
},
|
||||
]
|
||||
|
||||
static _cacheLength = 3600
|
||||
|
||||
static render({ version, outOfDate }) {
|
||||
const color = outOfDate === null ? 'blue' : 'orange'
|
||||
return { message: addv(version), color }
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import Joi from 'joi'
|
||||
import { isVPlusDottedVersionAtLeastOne } from '../test-validators.js'
|
||||
import {
|
||||
isVPlusDottedVersionAtLeastOne,
|
||||
isVPlusDottedVersionNClausesWithOptionalSuffix,
|
||||
} from '../test-validators.js'
|
||||
import { ServiceTester } from '../tester.js'
|
||||
export const t = new ServiceTester({
|
||||
id: 'BowerVersion',
|
||||
@@ -7,21 +9,17 @@ export const t = new ServiceTester({
|
||||
pathPrefix: '/bower',
|
||||
})
|
||||
|
||||
const isBowerPrereleaseVersion = Joi.string().regex(
|
||||
/^v\d+(\.\d+)?(\.\d+)?(-?[.\w\d])+?$/
|
||||
)
|
||||
|
||||
t.create('version').timeout(10000).get('/v/bootstrap.json').expectBadge({
|
||||
t.create('version').timeout(10000).get('/v/angular.json').expectBadge({
|
||||
label: 'bower',
|
||||
message: isVPlusDottedVersionAtLeastOne,
|
||||
})
|
||||
|
||||
t.create('pre version') // e.g. bower|v0.2.5-alpha-rc-pre
|
||||
t.create('pre version')
|
||||
.timeout(10000)
|
||||
.get('/v/bootstrap.json?include_prereleases')
|
||||
.get('/v/angular.json?include_prereleases')
|
||||
.expectBadge({
|
||||
label: 'bower',
|
||||
message: isBowerPrereleaseVersion,
|
||||
message: isVPlusDottedVersionNClausesWithOptionalSuffix,
|
||||
})
|
||||
|
||||
t.create('Version for Invalid Package')
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import moment from 'moment'
|
||||
import dayjs from 'dayjs'
|
||||
import pep440 from '@renovate/pep440'
|
||||
|
||||
/**
|
||||
@@ -182,7 +182,7 @@ function colorScale(steps, colors, reversed) {
|
||||
*/
|
||||
function age(date) {
|
||||
const colorByAge = colorScale([7, 30, 180, 365, 730], undefined, true)
|
||||
const daysElapsed = moment().diff(moment(date), 'days')
|
||||
const daysElapsed = dayjs().diff(dayjs(date), 'days')
|
||||
return colorByAge(daysElapsed)
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,22 +1,54 @@
|
||||
/**
|
||||
* Common functions and utilities for tasks related to dynamic badges.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import Joi from 'joi'
|
||||
import toArray from '../core/base-service/to-array.js'
|
||||
import validate from '../core/base-service/validate.js'
|
||||
import { InvalidResponse } from './index.js'
|
||||
|
||||
/**
|
||||
* Map of error codes and their corresponding error messages.
|
||||
*
|
||||
* @type {object}
|
||||
*/
|
||||
const errorMessages = {
|
||||
404: 'resource not found',
|
||||
}
|
||||
|
||||
/**
|
||||
* Joi schema for validating individual value.
|
||||
* Checks if the individual value is of type string or number.
|
||||
*
|
||||
* @type {object}
|
||||
*/
|
||||
const individualValueSchema = Joi.alternatives()
|
||||
.try(Joi.string(), Joi.number())
|
||||
.required()
|
||||
|
||||
/**
|
||||
* Joi schema for validating compound value.
|
||||
* Checks if the compound value is of type individualValueSchema, array of individualValueSchema or empty array.
|
||||
*
|
||||
* @type {object}
|
||||
*/
|
||||
const compoundValueSchema = Joi.alternatives().try(
|
||||
individualValueSchema,
|
||||
Joi.array().items(individualValueSchema).required(),
|
||||
Joi.array().length(0)
|
||||
)
|
||||
|
||||
/**
|
||||
* Look up the value in the data object by key and validate the value against compoundValueSchema.
|
||||
*
|
||||
* @param {object} attrs Refer to individual attributes
|
||||
* @param {object} attrs.data Object containing the data for validation
|
||||
* @param {string} attrs.key Key to retrieve the data from object for validation
|
||||
* @throws {InvalidResponse|Error} Error if Joi validation fails due to invalid or no schema
|
||||
* @returns {object} Value if Joi validation is success
|
||||
*/
|
||||
function transformAndValidate({ data, key }) {
|
||||
return validate(
|
||||
{
|
||||
@@ -30,6 +62,20 @@ function transformAndValidate({ data, key }) {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles rendering concerns of dynamic badges.
|
||||
* Determines the label of the badge according to the tag and defaultLabel.
|
||||
* Determines the message of the badge according to the prefix, suffix and value.
|
||||
* Sets the color of the badge to blue.
|
||||
*
|
||||
* @param {object} attrs Refer to individual attributes
|
||||
* @param {string} attrs.defaultLabel default badge label
|
||||
* @param {string} [attrs.tag] If provided then this value will be appended to the badge label, e.g. `foobar@v1.23`
|
||||
* @param {any} attrs.value Value or array of value to be used for the badge message
|
||||
* @param {string} [attrs.prefix] If provided then the badge message will use this value as a prefix
|
||||
* @param {string} [attrs.suffix] If provided then the badge message will use this value as a suffix
|
||||
* @returns {object} Badge with label, message and color properties
|
||||
*/
|
||||
function renderDynamicBadge({
|
||||
defaultLabel,
|
||||
tag,
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/**
|
||||
* Common functions and utilities for tasks related to endpoint badges.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import Joi from 'joi'
|
||||
import validate from '../core/base-service/validate.js'
|
||||
import { InvalidResponse } from './index.js'
|
||||
@@ -14,6 +20,11 @@ const optionalNumberWhenAnyLogoPresent = Joi.alternatives()
|
||||
.conditional('namedLogo', { is: Joi.string().required(), then: Joi.number() })
|
||||
.conditional('logoSvg', { is: Joi.string().required(), then: Joi.number() })
|
||||
|
||||
/**
|
||||
* Joi schema for validating endpoint.
|
||||
*
|
||||
* @type {object}
|
||||
*/
|
||||
const endpointSchema = Joi.object({
|
||||
schemaVersion: 1,
|
||||
label: Joi.string().allow('').required(),
|
||||
@@ -33,9 +44,18 @@ const endpointSchema = Joi.object({
|
||||
.oxor('namedLogo', 'logoSvg')
|
||||
.required()
|
||||
|
||||
// Strictly validate according to the endpoint schema. This rejects unknown /
|
||||
// invalid keys. Optionally it prints those keys in the message in order to
|
||||
// provide detailed feedback.
|
||||
/**
|
||||
* Strictly validate the data according to the endpoint schema.
|
||||
* This rejects unknown/invalid keys.
|
||||
* Optionally it prints those keys in the message to provide detailed feedback.
|
||||
*
|
||||
* @param {object} data Object containing the data for validation
|
||||
* @param {object} attrs Refer to individual attributes
|
||||
* @param {string} [attrs.prettyErrorMessage] If provided then error message is set to this value
|
||||
* @param {boolean} [attrs.includeKeys] If true then includes error details in error message, defaults to false
|
||||
* @throws {InvalidResponse|Error} Error if Joi validation fails due to invalid or no schema
|
||||
* @returns {object} Value if Joi validation is success
|
||||
*/
|
||||
function validateEndpointData(
|
||||
data,
|
||||
{ prettyErrorMessage = 'invalid response data', includeKeys = false } = {}
|
||||
@@ -56,6 +76,17 @@ function validateEndpointData(
|
||||
|
||||
const anySchema = Joi.any()
|
||||
|
||||
/**
|
||||
* Fetches data from the endpoint and validates the data.
|
||||
*
|
||||
* @param {object} serviceInstance Instance of Endpoint class
|
||||
* @param {object} attrs Refer to individual attributes
|
||||
* @param {string} attrs.url Endpoint URL
|
||||
* @param {object} attrs.errorMessages Object containing error messages for different error codes
|
||||
* @param {string} attrs.validationPrettyErrorMessage If provided then the error message is set to this value
|
||||
* @param {boolean} attrs.includeKeys If true then includes error details in error message
|
||||
* @returns {object} Data fetched from endpoint
|
||||
*/
|
||||
async function fetchEndpointData(
|
||||
serviceInstance,
|
||||
{ url, errorMessages, validationPrettyErrorMessage, includeKeys }
|
||||
|
||||
41
services/galaxytoolshed/galaxytoolshed-activity.service.js
Normal file
41
services/galaxytoolshed/galaxytoolshed-activity.service.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { formatDate } from '../text-formatters.js'
|
||||
import BaseGalaxyToolshedService from './galaxytoolshed-base.js'
|
||||
|
||||
export default class GalaxyToolshedCreatedDate extends BaseGalaxyToolshedService {
|
||||
static category = 'activity'
|
||||
static route = {
|
||||
base: 'galaxytoolshed/created-date',
|
||||
pattern: ':repository/:owner',
|
||||
}
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'Galaxy Toolshed (created date)',
|
||||
namedParams: {
|
||||
repository: 'sra_tools',
|
||||
owner: 'iuc',
|
||||
},
|
||||
staticPreview: this.render({
|
||||
date: this.render({ date: '2022-01-01' }),
|
||||
}),
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'created date',
|
||||
color: 'blue',
|
||||
}
|
||||
|
||||
static render({ date }) {
|
||||
return { message: formatDate(date) }
|
||||
}
|
||||
|
||||
async handle({ repository, owner }) {
|
||||
const response = await this.fetchLastOrderedInstallableRevisionsSchema({
|
||||
repository,
|
||||
owner,
|
||||
})
|
||||
const { create_time: date } = response[0]
|
||||
return this.constructor.render({ date })
|
||||
}
|
||||
}
|
||||
23
services/galaxytoolshed/galaxytoolshed-activity.tester.js
Normal file
23
services/galaxytoolshed/galaxytoolshed-activity.tester.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { isFormattedDate } from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('Created Date')
|
||||
.get('/sra_tools/iuc.json')
|
||||
.expectBadge({ label: 'created date', message: isFormattedDate })
|
||||
|
||||
t.create('Created Date - repository not found')
|
||||
.get('/sra_tool/iuc.json')
|
||||
.expectBadge({ label: 'created date', message: 'not found' })
|
||||
|
||||
t.create('Created Date - owner not found')
|
||||
.get('/sra_tools/iu.json')
|
||||
.expectBadge({ label: 'created date', message: 'not found' })
|
||||
|
||||
t.create('Created Date - changesetRevision not found')
|
||||
.get('/bioqc/badilla.json')
|
||||
.expectBadge({
|
||||
label: 'created date',
|
||||
message: 'changesetRevision not found',
|
||||
})
|
||||
55
services/galaxytoolshed/galaxytoolshed-base.js
Normal file
55
services/galaxytoolshed/galaxytoolshed-base.js
Normal file
@@ -0,0 +1,55 @@
|
||||
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({
|
||||
create_time: Joi.date().required(),
|
||||
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],
|
||||
})
|
||||
}
|
||||
}
|
||||
34
services/galaxytoolshed/galaxytoolshed-downloads.service.js
Normal file
34
services/galaxytoolshed/galaxytoolshed-downloads.service.js
Normal file
@@ -0,0 +1,34 @@
|
||||
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 })
|
||||
}
|
||||
}
|
||||
28
services/galaxytoolshed/galaxytoolshed-downloads.tester.js
Normal file
28
services/galaxytoolshed/galaxytoolshed-downloads.tester.js
Normal file
@@ -0,0 +1,28 @@
|
||||
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',
|
||||
})
|
||||
@@ -1,11 +1,11 @@
|
||||
import path from 'path'
|
||||
import Joi from 'joi'
|
||||
import gql from 'graphql-tag'
|
||||
import { metric } from '../text-formatters.js'
|
||||
import { InvalidParameter } from '../index.js'
|
||||
import { ConditionalGithubAuthV3Service } from './github-auth-service.js'
|
||||
import { GithubAuthV4Service } from './github-auth-service.js'
|
||||
import {
|
||||
documentation as commonDocumentation,
|
||||
errorMessagesFor,
|
||||
transformErrors,
|
||||
} from './github-helpers.js'
|
||||
|
||||
const documentation = `${commonDocumentation}
|
||||
@@ -22,28 +22,29 @@ const documentation = `${commonDocumentation}
|
||||
</p>
|
||||
`
|
||||
|
||||
const schema = Joi.alternatives(
|
||||
/*
|
||||
alternative empty object schema to provide a custom error message
|
||||
in the event a file path is provided by the user instead of a directory
|
||||
*/
|
||||
Joi.object({}).required(),
|
||||
Joi.array()
|
||||
.items(
|
||||
Joi.object({
|
||||
path: Joi.string().required(),
|
||||
type: Joi.string().required(),
|
||||
const schema = Joi.object({
|
||||
data: Joi.object({
|
||||
repository: Joi.object({
|
||||
object: Joi.object({
|
||||
entries: Joi.array().items(
|
||||
Joi.object({
|
||||
type: Joi.string().required(),
|
||||
extension: Joi.string().allow('').required(),
|
||||
})
|
||||
),
|
||||
})
|
||||
)
|
||||
.required()
|
||||
)
|
||||
.allow(null)
|
||||
.required(),
|
||||
}).required(),
|
||||
}).required(),
|
||||
}).required()
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
type: Joi.any().valid('dir', 'file'),
|
||||
extension: Joi.string(),
|
||||
})
|
||||
|
||||
export default class GithubDirectoryFileCount extends ConditionalGithubAuthV3Service {
|
||||
export default class GithubDirectoryFileCount extends GithubAuthV4Service {
|
||||
static category = 'size'
|
||||
|
||||
static route = {
|
||||
@@ -103,7 +104,7 @@ export default class GithubDirectoryFileCount extends ConditionalGithubAuthV3Ser
|
||||
title: 'GitHub repo file count (file extension)',
|
||||
pattern: ':user/:repo/:path',
|
||||
namedParams: { user: 'badges', repo: 'shields', path: 'services' },
|
||||
queryParams: { extension: 'js' },
|
||||
queryParams: { type: 'file', extension: 'js' },
|
||||
staticPreview: this.render({ count: 1 }),
|
||||
documentation,
|
||||
},
|
||||
@@ -118,10 +119,25 @@ export default class GithubDirectoryFileCount extends ConditionalGithubAuthV3Ser
|
||||
}
|
||||
|
||||
async fetch({ user, repo, path = '' }) {
|
||||
return this._requestJson({
|
||||
url: `/repos/${user}/${repo}/contents/${path}`,
|
||||
const expression = `HEAD:${path}`
|
||||
return this._requestGraphql({
|
||||
query: gql`
|
||||
query RepoFiles($user: String!, $repo: String!, $expression: String!) {
|
||||
repository(owner: $user, name: $repo) {
|
||||
object(expression: $expression) {
|
||||
... on Tree {
|
||||
entries {
|
||||
type
|
||||
extension
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { user, repo, expression },
|
||||
schema,
|
||||
errorMessages: errorMessagesFor('repo or directory not found'),
|
||||
transformErrors,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -137,11 +153,12 @@ export default class GithubDirectoryFileCount extends ConditionalGithubAuthV3Ser
|
||||
}
|
||||
|
||||
if (type) {
|
||||
files = files.filter(file => file.type === type)
|
||||
const objectType = type === 'dir' ? 'tree' : 'blob'
|
||||
files = files.filter(file => file.type === objectType)
|
||||
}
|
||||
|
||||
if (extension) {
|
||||
files = files.filter(file => path.extname(file.path) === `.${extension}`)
|
||||
files = files.filter(file => file.extension === `.${extension}`)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -150,7 +167,13 @@ export default class GithubDirectoryFileCount extends ConditionalGithubAuthV3Ser
|
||||
}
|
||||
|
||||
async handle({ user, repo, path }, { type, extension }) {
|
||||
const content = await this.fetch({ user, repo, path })
|
||||
const json = await this.fetch({ user, repo, path })
|
||||
if (json.data.repository.object === null) {
|
||||
throw new InvalidParameter({
|
||||
prettyMessage: 'directory not found',
|
||||
})
|
||||
}
|
||||
const content = json.data.repository.object.entries
|
||||
const { count } = this.constructor.transform(content, { type, extension })
|
||||
return this.constructor.render({ count })
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ import GithubDirectoryFileCount from './github-directory-file-count.service.js'
|
||||
|
||||
describe('GithubDirectoryFileCount', function () {
|
||||
const contents = [
|
||||
{ path: 'a', type: 'dir' },
|
||||
{ path: 'b', type: 'dir' },
|
||||
{ path: 'c.js', type: 'file' },
|
||||
{ path: 'd.js', type: 'file' },
|
||||
{ path: 'e.txt', type: 'file' },
|
||||
{ path: 'f', type: 'submodule' },
|
||||
{ extension: '', type: 'tree' },
|
||||
{ extension: '', type: 'tree' },
|
||||
{ extension: '.js', type: 'blob' },
|
||||
{ extension: '.js', type: 'blob' },
|
||||
{ extension: '.txt', type: 'blob' },
|
||||
{ extension: '', type: 'commit' },
|
||||
]
|
||||
|
||||
test(GithubDirectoryFileCount.transform, () => {
|
||||
|
||||
@@ -14,11 +14,18 @@ t.create('directory file count (custom path)')
|
||||
message: isMetric,
|
||||
})
|
||||
|
||||
t.create('directory file count (repo not found)')
|
||||
.get('/badges/not_existing_repository.json')
|
||||
.expectBadge({
|
||||
label: 'files',
|
||||
message: 'repo not found',
|
||||
})
|
||||
|
||||
t.create('directory file count (directory not found)')
|
||||
.get('/badges/shields/not_existing_directory.json')
|
||||
.expectBadge({
|
||||
label: 'files',
|
||||
message: 'repo or directory not found',
|
||||
message: 'directory not found',
|
||||
})
|
||||
|
||||
t.create('directory file count (not a directory)')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import gql from 'graphql-tag'
|
||||
import Joi from 'joi'
|
||||
import moment from 'moment'
|
||||
import dayjs from 'dayjs'
|
||||
import { metric, maybePluralize } from '../text-formatters.js'
|
||||
import { nonNegativeInteger } from '../validators.js'
|
||||
import { GithubAuthV4Service } from './github-auth-service.js'
|
||||
@@ -121,7 +121,7 @@ export default class GithubHacktoberfestCombinedStatus extends GithubAuthV4Servi
|
||||
// The global cutoff time is 11/1 noon UTC.
|
||||
// https://github.com/badges/shields/pull/4109#discussion_r330782093
|
||||
// We want to show "1 day left" on the last day so we add 1.
|
||||
daysLeft = moment(`${year}-11-01 12:00:00 Z`).diff(moment(), 'days') + 1
|
||||
daysLeft = dayjs(`${year}-11-01 12:00:00 Z`).diff(dayjs(), 'days') + 1
|
||||
}
|
||||
if (daysLeft < 0) {
|
||||
return {
|
||||
@@ -205,10 +205,7 @@ export default class GithubHacktoberfestCombinedStatus extends GithubAuthV4Servi
|
||||
}
|
||||
|
||||
static getCalendarPosition(year) {
|
||||
const daysToStart = moment(`${year}-10-01 00:00:00 Z`).diff(
|
||||
moment(),
|
||||
'days'
|
||||
)
|
||||
const daysToStart = dayjs(`${year}-10-01 00:00:00 Z`).diff(dayjs(), 'days')
|
||||
const isBefore = daysToStart > 0
|
||||
return { daysToStart, isBefore }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import moment from 'moment'
|
||||
import dayjs from 'dayjs'
|
||||
import Joi from 'joi'
|
||||
import { age } from '../color-formatters.js'
|
||||
import { formatDate } from '../text-formatters.js'
|
||||
@@ -51,7 +51,7 @@ export default class GithubReleaseDate extends GithubAuthV3Service {
|
||||
static defaultBadgeData = { label: 'release date' }
|
||||
|
||||
static render({ date }) {
|
||||
const releaseDate = moment(date)
|
||||
const releaseDate = dayjs(date)
|
||||
return {
|
||||
message: formatDate(releaseDate),
|
||||
color: age(releaseDate),
|
||||
|
||||
@@ -8,7 +8,7 @@ export default class GitLabBase extends BaseJsonService {
|
||||
|
||||
async fetch({ url, options, schema, errorMessages }) {
|
||||
return this._requestJson(
|
||||
this.authHelper.withBasicAuth({
|
||||
this.authHelper.withBearerAuthHeader({
|
||||
schema,
|
||||
url,
|
||||
options,
|
||||
@@ -18,10 +18,12 @@ export default class GitLabBase extends BaseJsonService {
|
||||
}
|
||||
|
||||
async fetchPage({ page, requestParams, schema }) {
|
||||
const { res, buffer } = await this._request({
|
||||
...requestParams,
|
||||
...{ options: { searchParams: { page } } },
|
||||
})
|
||||
const { res, buffer } = await this._request(
|
||||
this.authHelper.withBearerAuthHeader({
|
||||
...requestParams,
|
||||
...{ options: { searchParams: { page } } },
|
||||
})
|
||||
)
|
||||
|
||||
const json = this._parseJson(buffer)
|
||||
const data = this.constructor._validate(json, schema)
|
||||
@@ -35,7 +37,7 @@ export default class GitLabBase extends BaseJsonService {
|
||||
errorMessages,
|
||||
firstPageOnly = false,
|
||||
}) {
|
||||
const requestParams = this.authHelper.withBasicAuth({
|
||||
const requestParams = {
|
||||
url,
|
||||
options: {
|
||||
headers: { Accept: 'application/json' },
|
||||
@@ -43,7 +45,7 @@ export default class GitLabBase extends BaseJsonService {
|
||||
...options,
|
||||
},
|
||||
errorMessages,
|
||||
})
|
||||
}
|
||||
|
||||
const {
|
||||
res: { headers },
|
||||
|
||||
12
services/gitlab/gitlab-contributors-redirect.service.js
Normal file
12
services/gitlab/gitlab-contributors-redirect.service.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { redirector } from '../index.js'
|
||||
|
||||
// https://github.com/badges/shields/issues/8138
|
||||
export default redirector({
|
||||
category: 'build',
|
||||
route: {
|
||||
base: 'gitlab/v/contributor',
|
||||
pattern: ':project+',
|
||||
},
|
||||
transformPath: ({ project }) => `/gitlab/contributors/${project}`,
|
||||
dateAdded: new Date('2022-06-29'),
|
||||
})
|
||||
9
services/gitlab/gitlab-contributors-redirect.tester.js
Normal file
9
services/gitlab/gitlab-contributors-redirect.tester.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('Contributors redirect')
|
||||
.get('/gitlab-org/gitlab', {
|
||||
followRedirect: false,
|
||||
})
|
||||
.expectStatus(301)
|
||||
.expectHeader('Location', '/gitlab/contributors/gitlab-org/gitlab.svg')
|
||||
78
services/gitlab/gitlab-contributors.service.js
Normal file
78
services/gitlab/gitlab-contributors.service.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import Joi from 'joi'
|
||||
import { optionalUrl, nonNegativeInteger } from '../validators.js'
|
||||
import { renderContributorBadge } from '../contributor-count.js'
|
||||
import GitLabBase from './gitlab-base.js'
|
||||
|
||||
const schema = Joi.object({ 'x-total': nonNegativeInteger }).required()
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
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 )
|
||||
</p>
|
||||
`
|
||||
|
||||
const customDocumentation = `
|
||||
<p>
|
||||
Note that only network-accessible jihulab.com and other self-managed GitLab instances are supported.
|
||||
You may use your GitLab Project Id (e.g. 13953) or your Project Path (e.g. gitlab-cn/gitlab ) in <a href="https://jihulab.com">https://jihulab.com</a>
|
||||
</p>
|
||||
`
|
||||
|
||||
export default class GitlabContributors extends GitLabBase {
|
||||
static category = 'activity'
|
||||
static route = {
|
||||
base: 'gitlab/contributors',
|
||||
pattern: ':project+',
|
||||
queryParamSchema,
|
||||
}
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'GitLab contributors',
|
||||
namedParams: {
|
||||
project: 'gitlab-org/gitlab',
|
||||
},
|
||||
staticPreview: this.render({ contributorCount: 418 }),
|
||||
documentation,
|
||||
},
|
||||
{
|
||||
title: 'GitLab (self-managed) contributors',
|
||||
queryParams: { gitlab_url: 'https://jihulab.com' },
|
||||
namedParams: {
|
||||
project: 'gitlab-cn/gitlab',
|
||||
},
|
||||
staticPreview: this.render({ contributorCount: 415 }),
|
||||
documentation: customDocumentation,
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = { label: 'contributors' }
|
||||
|
||||
static render({ contributorCount }) {
|
||||
return renderContributorBadge({ contributorCount })
|
||||
}
|
||||
|
||||
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 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
|
||||
const contributorCount = data['x-total']
|
||||
return this.constructor.render({ contributorCount })
|
||||
}
|
||||
}
|
||||
42
services/gitlab/gitlab-contributors.tester.js
Normal file
42
services/gitlab/gitlab-contributors.tester.js
Normal file
@@ -0,0 +1,42 @@
|
||||
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')
|
||||
.expectBadge({
|
||||
label: 'contributors',
|
||||
message: isMetric,
|
||||
})
|
||||
|
||||
t.create('Contributors (repo not found)')
|
||||
.get('/guoxudong.io/shields-test/do-not-exist.json')
|
||||
.expectBadge({
|
||||
label: 'contributors',
|
||||
message: 'project not found',
|
||||
})
|
||||
|
||||
t.create('Mocking the missing x-total header')
|
||||
.get('/group/project.json')
|
||||
.intercept(nock =>
|
||||
nock('https://gitlab.com')
|
||||
.get(
|
||||
'/api/v4/projects/group%2Fproject/repository/contributors?page=1&per_page=1'
|
||||
)
|
||||
.reply(200)
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'contributors',
|
||||
message: 'invalid response data',
|
||||
})
|
||||
|
||||
t.create('Contributors (private repo)')
|
||||
.skipWhen(noGitLabToken)
|
||||
.get('/shields-ops-group/test.json')
|
||||
.expectBadge({
|
||||
label: 'contributors',
|
||||
message: isMetric,
|
||||
})
|
||||
@@ -68,14 +68,14 @@ export default class GitlabCoverage extends BaseSvgScrapingService {
|
||||
documentation,
|
||||
},
|
||||
{
|
||||
title: 'Gitlab code coverage (self-hosted)',
|
||||
title: 'Gitlab code coverage (self-managed)',
|
||||
namedParams: { user: 'GNOME', repo: 'at-spi2-core', branch: 'master' },
|
||||
queryParams: { gitlab_url: 'https://gitlab.gnome.org' },
|
||||
staticPreview: this.render({ coverage: 93 }),
|
||||
documentation,
|
||||
},
|
||||
{
|
||||
title: 'Gitlab code coverage (self-hosted, specific job)',
|
||||
title: 'Gitlab code coverage (self-managed, specific job)',
|
||||
namedParams: { user: 'GNOME', repo: 'libhandy', branch: 'master' },
|
||||
queryParams: {
|
||||
gitlab_url: 'https://gitlab.gnome.org',
|
||||
|
||||
77
services/gitlab/gitlab-forks.service.js
Normal file
77
services/gitlab/gitlab-forks.service.js
Normal file
@@ -0,0 +1,77 @@
|
||||
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({
|
||||
forks_count: nonNegativeInteger,
|
||||
}).required()
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
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>
|
||||
`
|
||||
|
||||
export default class GitlabForks extends GitLabBase {
|
||||
static category = 'social'
|
||||
|
||||
static route = {
|
||||
base: 'gitlab/forks',
|
||||
pattern: ':project+',
|
||||
queryParamSchema,
|
||||
}
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'GitLab forks',
|
||||
namedParams: {
|
||||
project: 'gitlab-org/gitlab',
|
||||
},
|
||||
queryParams: { gitlab_url: 'https://gitlab.com' },
|
||||
staticPreview: {
|
||||
label: 'Fork',
|
||||
message: '6.4k',
|
||||
style: 'social',
|
||||
},
|
||||
documentation,
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = { label: 'forks', namedLogo: 'gitlab' }
|
||||
|
||||
static render({ baseUrl, project, forkCount }) {
|
||||
return {
|
||||
message: metric(forkCount),
|
||||
color: 'blue',
|
||||
link: [
|
||||
`${baseUrl}/${project}/-/forks/new`,
|
||||
`${baseUrl}/${project}/-/forks`,
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
async fetch({ project, baseUrl }) {
|
||||
// https://docs.gitlab.com/ee/api/projects.html#get-single-project
|
||||
return super.fetch({
|
||||
schema,
|
||||
url: `${baseUrl}/api/v4/projects/${encodeURIComponent(project)}`,
|
||||
errorMessages: {
|
||||
404: 'project not found',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async handle({ project }, { gitlab_url: baseUrl = 'https://gitlab.com' }) {
|
||||
const { forks_count: forkCount } = await this.fetch({
|
||||
project,
|
||||
baseUrl,
|
||||
})
|
||||
return this.constructor.render({ baseUrl, project, forkCount })
|
||||
}
|
||||
}
|
||||
35
services/gitlab/gitlab-forks.tester.js
Normal file
35
services/gitlab/gitlab-forks.tester.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { isMetric } from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('Forks')
|
||||
.get('/gitlab-org/gitlab.json')
|
||||
.expectBadge({
|
||||
label: 'forks',
|
||||
message: isMetric,
|
||||
color: 'blue',
|
||||
link: [
|
||||
'https://gitlab.com/gitlab-org/gitlab/-/forks/new',
|
||||
'https://gitlab.com/gitlab-org/gitlab/-/forks',
|
||||
],
|
||||
})
|
||||
|
||||
t.create('Forks (self-managed)')
|
||||
.get('/gitlab-cn/gitlab.json?gitlab_url=https://jihulab.com')
|
||||
.expectBadge({
|
||||
label: 'forks',
|
||||
message: isMetric,
|
||||
color: 'blue',
|
||||
link: [
|
||||
'https://jihulab.com/gitlab-cn/gitlab/-/forks/new',
|
||||
'https://jihulab.com/gitlab-cn/gitlab/-/forks',
|
||||
],
|
||||
})
|
||||
|
||||
t.create('Forks (project not found)')
|
||||
.get('/user1/gitlab-does-not-have-this-repo.json')
|
||||
.expectBadge({
|
||||
label: 'forks',
|
||||
message: 'project not found',
|
||||
})
|
||||
286
services/gitlab/gitlab-issues.service.js
Normal file
286
services/gitlab/gitlab-issues.service.js
Normal file
@@ -0,0 +1,286 @@
|
||||
import Joi from 'joi'
|
||||
import { optionalUrl, nonNegativeInteger } from '../validators.js'
|
||||
import { metric } from '../text-formatters.js'
|
||||
import GitLabBase from './gitlab-base.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
statistics: Joi.object({
|
||||
counts: Joi.object({
|
||||
all: nonNegativeInteger,
|
||||
closed: nonNegativeInteger,
|
||||
opened: nonNegativeInteger,
|
||||
}).required(),
|
||||
}).allow(null),
|
||||
}).required()
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
labels: Joi.string(),
|
||||
gitlab_url: optionalUrl,
|
||||
}).required()
|
||||
|
||||
const documentation = `
|
||||
<p>
|
||||
You may use your GitLab Project Id (e.g. 278964) or your Project Path (e.g. gitlab-org/gitlab ).
|
||||
Note that only internet-accessible GitLab instances are supported, for example https://jihulab.com, https://gitlab.gnome.org, or https://gitlab.com/.
|
||||
</p>
|
||||
`
|
||||
|
||||
const labelDocumentation = `
|
||||
<p>
|
||||
If you want to use multiple labels then please use commas (<code>,</code>) to separate them, e.g. <code>foo,bar</code>.
|
||||
</p>
|
||||
`
|
||||
|
||||
export default class GitlabIssues extends GitLabBase {
|
||||
static category = 'issue-tracking'
|
||||
|
||||
static route = {
|
||||
base: 'gitlab/issues',
|
||||
pattern: ':variant(all|open|closed):raw(-raw)?/:project+',
|
||||
queryParamSchema,
|
||||
}
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'GitLab issues',
|
||||
pattern: 'open/:project+',
|
||||
namedParams: {
|
||||
project: 'gitlab-org/gitlab',
|
||||
},
|
||||
queryParams: { gitlab_url: 'https://gitlab.com' },
|
||||
staticPreview: {
|
||||
label: 'issues',
|
||||
message: '44k open',
|
||||
color: 'yellow',
|
||||
},
|
||||
documentation,
|
||||
},
|
||||
{
|
||||
title: 'GitLab issues',
|
||||
pattern: 'open-raw/:project+',
|
||||
namedParams: {
|
||||
project: 'gitlab-org/gitlab',
|
||||
},
|
||||
queryParams: { gitlab_url: 'https://gitlab.com' },
|
||||
staticPreview: {
|
||||
label: 'open issues',
|
||||
message: '44k',
|
||||
color: 'yellow',
|
||||
},
|
||||
documentation,
|
||||
},
|
||||
{
|
||||
title: 'GitLab issues by-label',
|
||||
pattern: 'open/:project+',
|
||||
namedParams: {
|
||||
project: 'gitlab-org/gitlab',
|
||||
},
|
||||
queryParams: {
|
||||
labels: 'test,failure::new',
|
||||
gitlab_url: 'https://gitlab.com',
|
||||
},
|
||||
staticPreview: {
|
||||
label: 'test,failure::new issues',
|
||||
message: '16 open',
|
||||
color: 'yellow',
|
||||
},
|
||||
documentation: documentation + labelDocumentation,
|
||||
},
|
||||
{
|
||||
title: 'GitLab issues by-label',
|
||||
pattern: 'open-raw/:project+',
|
||||
namedParams: {
|
||||
project: 'gitlab-org/gitlab',
|
||||
},
|
||||
queryParams: {
|
||||
labels: 'test,failure::new',
|
||||
gitlab_url: 'https://gitlab.com',
|
||||
},
|
||||
staticPreview: {
|
||||
label: 'open test,failure::new issues',
|
||||
message: '16',
|
||||
color: 'yellow',
|
||||
},
|
||||
documentation: documentation + labelDocumentation,
|
||||
},
|
||||
{
|
||||
title: 'GitLab closed issues',
|
||||
pattern: 'closed/:project+',
|
||||
namedParams: {
|
||||
project: 'gitlab-org/gitlab',
|
||||
},
|
||||
queryParams: { gitlab_url: 'https://gitlab.com' },
|
||||
staticPreview: {
|
||||
label: 'issues',
|
||||
message: '72k closed',
|
||||
color: 'yellow',
|
||||
},
|
||||
documentation,
|
||||
},
|
||||
{
|
||||
title: 'GitLab closed issues',
|
||||
pattern: 'closed-raw/:project+',
|
||||
namedParams: {
|
||||
project: 'gitlab-org/gitlab',
|
||||
},
|
||||
queryParams: { gitlab_url: 'https://gitlab.com' },
|
||||
staticPreview: {
|
||||
label: 'closed issues',
|
||||
message: '72k ',
|
||||
color: 'yellow',
|
||||
},
|
||||
documentation,
|
||||
},
|
||||
{
|
||||
title: 'GitLab closed issues by-label',
|
||||
pattern: 'closed/:project+',
|
||||
namedParams: {
|
||||
project: 'gitlab-org/gitlab',
|
||||
},
|
||||
queryParams: {
|
||||
labels: 'test,failure::new',
|
||||
gitlab_url: 'https://gitlab.com',
|
||||
},
|
||||
staticPreview: {
|
||||
label: 'test,failure::new issues',
|
||||
message: '4 closed',
|
||||
color: 'yellow',
|
||||
},
|
||||
documentation: documentation + labelDocumentation,
|
||||
},
|
||||
{
|
||||
title: 'GitLab closed issues by-label',
|
||||
pattern: 'closed-raw/:project+',
|
||||
namedParams: {
|
||||
project: 'gitlab-org/gitlab',
|
||||
},
|
||||
queryParams: {
|
||||
labels: 'test,failure::new',
|
||||
gitlab_url: 'https://gitlab.com',
|
||||
},
|
||||
staticPreview: {
|
||||
label: 'closed test,failure::new issues',
|
||||
message: '4',
|
||||
color: 'yellow',
|
||||
},
|
||||
documentation: documentation + labelDocumentation,
|
||||
},
|
||||
{
|
||||
title: 'GitLab all issues',
|
||||
pattern: 'all/:project+',
|
||||
namedParams: {
|
||||
project: 'gitlab-org/gitlab',
|
||||
},
|
||||
queryParams: { gitlab_url: 'https://gitlab.com' },
|
||||
staticPreview: {
|
||||
label: 'issues',
|
||||
message: '115k all',
|
||||
color: 'yellow',
|
||||
},
|
||||
documentation,
|
||||
},
|
||||
{
|
||||
title: 'GitLab all issues',
|
||||
pattern: 'all-raw/:project+',
|
||||
namedParams: {
|
||||
project: 'gitlab-org/gitlab',
|
||||
},
|
||||
queryParams: { gitlab_url: 'https://gitlab.com' },
|
||||
staticPreview: {
|
||||
label: 'all issues',
|
||||
message: '115k',
|
||||
color: 'yellow',
|
||||
},
|
||||
documentation,
|
||||
},
|
||||
{
|
||||
title: 'GitLab all issues by-label',
|
||||
pattern: 'all-raw/:project+',
|
||||
namedParams: {
|
||||
project: 'gitlab-org/gitlab',
|
||||
},
|
||||
queryParams: {
|
||||
labels: 'test,failure::new',
|
||||
gitlab_url: 'https://gitlab.com',
|
||||
},
|
||||
staticPreview: {
|
||||
label: 'all test,failure::new issues',
|
||||
message: '20',
|
||||
color: 'yellow',
|
||||
},
|
||||
documentation: documentation + labelDocumentation,
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = { label: 'issues', color: 'informational' }
|
||||
|
||||
static render({ variant, raw, labels, issueCount }) {
|
||||
const state = variant
|
||||
const isMultiLabel = labels && labels.includes(',')
|
||||
const labelText = labels ? `${isMultiLabel ? `${labels}` : labels} ` : ''
|
||||
|
||||
let labelPrefix = ''
|
||||
let messageSuffix = ''
|
||||
if (raw !== undefined) {
|
||||
labelPrefix = `${state} `
|
||||
} else {
|
||||
messageSuffix = state
|
||||
}
|
||||
return {
|
||||
label: `${labelPrefix}${labelText}issues`,
|
||||
message: `${metric(issueCount)}${
|
||||
messageSuffix ? ' ' : ''
|
||||
}${messageSuffix}`,
|
||||
color: issueCount > 0 ? 'yellow' : 'brightgreen',
|
||||
}
|
||||
}
|
||||
|
||||
async fetch({ project, baseUrl, labels }) {
|
||||
// https://docs.gitlab.com/ee/api/issues_statistics.html#get-project-issues-statistics
|
||||
return super.fetch({
|
||||
schema,
|
||||
url: `${baseUrl}/api/v4/projects/${encodeURIComponent(
|
||||
project
|
||||
)}/issues_statistics`,
|
||||
options: labels ? { searchParams: { labels } } : undefined,
|
||||
errorMessages: {
|
||||
404: 'project not found',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
static transform({ variant, statistics }) {
|
||||
const state = variant
|
||||
let issueCount
|
||||
switch (state) {
|
||||
case 'open':
|
||||
issueCount = statistics.counts.opened
|
||||
break
|
||||
case 'closed':
|
||||
issueCount = statistics.counts.closed
|
||||
break
|
||||
case 'all':
|
||||
issueCount = statistics.counts.all
|
||||
break
|
||||
}
|
||||
|
||||
return issueCount
|
||||
}
|
||||
|
||||
async handle(
|
||||
{ variant, raw, project },
|
||||
{ gitlab_url: baseUrl = 'https://gitlab.com', labels }
|
||||
) {
|
||||
const { statistics } = await this.fetch({
|
||||
project,
|
||||
baseUrl,
|
||||
labels,
|
||||
})
|
||||
return this.constructor.render({
|
||||
variant,
|
||||
raw,
|
||||
labels,
|
||||
issueCount: this.constructor.transform({ variant, statistics }),
|
||||
})
|
||||
}
|
||||
}
|
||||
147
services/gitlab/gitlab-issues.tester.js
Normal file
147
services/gitlab/gitlab-issues.tester.js
Normal file
@@ -0,0 +1,147 @@
|
||||
import Joi from 'joi'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
import {
|
||||
isMetric,
|
||||
isMetricOpenIssues,
|
||||
isMetricClosedIssues,
|
||||
} from '../test-validators.js'
|
||||
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('Issues (project not found)')
|
||||
.get('/open/guoxudong.io/shields-test/do-not-exist.json')
|
||||
.expectBadge({
|
||||
label: 'issues',
|
||||
message: 'project not found',
|
||||
})
|
||||
|
||||
/**
|
||||
* Opened issue number case
|
||||
*/
|
||||
t.create('Opened issues')
|
||||
.get('/open/guoxudong.io/shields-test/issue-test.json')
|
||||
.expectBadge({
|
||||
label: 'issues',
|
||||
message: isMetricOpenIssues,
|
||||
})
|
||||
|
||||
t.create('Open issues raw')
|
||||
.get('/open-raw/guoxudong.io/shields-test/issue-test.json')
|
||||
.expectBadge({
|
||||
label: 'open issues',
|
||||
message: isMetric,
|
||||
})
|
||||
|
||||
t.create('Open issues by label is > zero')
|
||||
.get('/open/guoxudong.io/shields-test/issue-test.json?labels=discussion')
|
||||
.expectBadge({
|
||||
label: 'discussion issues',
|
||||
message: isMetricOpenIssues,
|
||||
})
|
||||
|
||||
t.create('Open issues by multi-word label is > zero')
|
||||
.get(
|
||||
'/open/guoxudong.io/shields-test/issue-test.json?labels=discussion,enhancement'
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'discussion,enhancement issues',
|
||||
message: isMetricOpenIssues,
|
||||
})
|
||||
|
||||
t.create('Open issues by label (raw)')
|
||||
.get('/open-raw/guoxudong.io/shields-test/issue-test.json?labels=discussion')
|
||||
.expectBadge({
|
||||
label: 'open discussion issues',
|
||||
message: isMetric,
|
||||
})
|
||||
|
||||
t.create('Opened issues by Scoped labels')
|
||||
.get('/open/gitlab-org%2Fgitlab.json?labels=test,failure::new')
|
||||
.expectBadge({
|
||||
label: 'test,failure::new issues',
|
||||
message: isMetricOpenIssues,
|
||||
})
|
||||
|
||||
/**
|
||||
* Closed issue number case
|
||||
*/
|
||||
t.create('Closed issues')
|
||||
.get('/closed/guoxudong.io/shields-test/issue-test.json')
|
||||
.expectBadge({
|
||||
label: 'issues',
|
||||
message: isMetricClosedIssues,
|
||||
})
|
||||
|
||||
t.create('Closed issues raw')
|
||||
.get('/closed-raw/guoxudong.io/shields-test/issue-test.json')
|
||||
.expectBadge({
|
||||
label: 'closed issues',
|
||||
message: isMetric,
|
||||
})
|
||||
|
||||
t.create('Closed issues by label is > zero')
|
||||
.get('/closed/guoxudong.io/shields-test/issue-test.json?labels=bug')
|
||||
.expectBadge({
|
||||
label: 'bug issues',
|
||||
message: isMetricClosedIssues,
|
||||
})
|
||||
|
||||
t.create('Closed issues by multi-word label is > zero')
|
||||
.get('/closed/guoxudong.io/shields-test/issue-test.json?labels=bug,critical')
|
||||
.expectBadge({
|
||||
label: 'bug,critical issues',
|
||||
message: isMetricClosedIssues,
|
||||
})
|
||||
|
||||
t.create('Closed issues by label (raw)')
|
||||
.get('/closed-raw/guoxudong.io/shields-test/issue-test.json?labels=bug')
|
||||
.expectBadge({
|
||||
label: 'closed bug issues',
|
||||
message: isMetric,
|
||||
})
|
||||
|
||||
/**
|
||||
* All issue number case
|
||||
*/
|
||||
t.create('All issues')
|
||||
.get('/all/guoxudong.io/shields-test/issue-test.json')
|
||||
.expectBadge({
|
||||
label: 'issues',
|
||||
message: Joi.string().regex(
|
||||
/^([0-9]+[kMGTPEZY]?|[1-9]\.[1-9][kMGTPEZY]) all$/
|
||||
),
|
||||
})
|
||||
|
||||
t.create('All issues raw')
|
||||
.get('/all-raw/guoxudong.io/shields-test/issue-test.json')
|
||||
.expectBadge({
|
||||
label: 'all issues',
|
||||
message: isMetric,
|
||||
})
|
||||
|
||||
t.create('All issues by label is > zero')
|
||||
.get('/all/guoxudong.io/shields-test/issue-test.json?labels=discussion')
|
||||
.expectBadge({
|
||||
label: 'discussion issues',
|
||||
message: Joi.string().regex(
|
||||
/^([0-9]+[kMGTPEZY]?|[1-9]\.[1-9][kMGTPEZY]) all$/
|
||||
),
|
||||
})
|
||||
|
||||
t.create('All issues by multi-word label is > zero')
|
||||
.get(
|
||||
'/all/guoxudong.io/shields-test/issue-test.json?labels=discussion,enhancement'
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'discussion,enhancement issues',
|
||||
message: Joi.string().regex(
|
||||
/^([0-9]+[kMGTPEZY]?|[1-9]\.[1-9][kMGTPEZY]) all$/
|
||||
),
|
||||
})
|
||||
|
||||
t.create('All issues by label (raw)')
|
||||
.get('/all-raw/guoxudong.io/shields-test/issue-test.json?labels=discussion')
|
||||
.expectBadge({
|
||||
label: 'all discussion issues',
|
||||
message: isMetric,
|
||||
})
|
||||
12
services/gitlab/gitlab-license-redirect.service.js
Normal file
12
services/gitlab/gitlab-license-redirect.service.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { redirector } from '../index.js'
|
||||
|
||||
// https://github.com/badges/shields/issues/8138
|
||||
export default redirector({
|
||||
category: 'build',
|
||||
route: {
|
||||
base: 'gitlab/v/license',
|
||||
pattern: ':project+',
|
||||
},
|
||||
transformPath: ({ project }) => `/gitlab/license/${project}`,
|
||||
dateAdded: new Date('2022-06-29'),
|
||||
})
|
||||
9
services/gitlab/gitlab-license-redirect.tester.js
Normal file
9
services/gitlab/gitlab-license-redirect.tester.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('License redirect')
|
||||
.get('/gitlab-org/gitlab', {
|
||||
followRedirect: false,
|
||||
})
|
||||
.expectStatus(301)
|
||||
.expectHeader('Location', '/gitlab/license/gitlab-org/gitlab.svg')
|
||||
96
services/gitlab/gitlab-license.service.js
Normal file
96
services/gitlab/gitlab-license.service.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import Joi from 'joi'
|
||||
import { optionalUrl } from '../validators.js'
|
||||
import { renderLicenseBadge } from '../licenses.js'
|
||||
import GitLabBase from './gitlab-base.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
license: Joi.object({
|
||||
name: Joi.string().required(),
|
||||
}).allow(null),
|
||||
}).required()
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
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 )
|
||||
</p>
|
||||
`
|
||||
|
||||
const customDocumentation = `
|
||||
<p>
|
||||
Note that only internet-accessible GitLab instances are supported, for example https://jihulab.com, https://gitlab.gnome.org, or https://gitlab.com/.
|
||||
You may use your GitLab Project Id (e.g. 13953) or your Project Path (e.g. gitlab-cn/gitlab ) in <a href="https://jihulab.com">https://jihulab.com</a>
|
||||
</p>
|
||||
`
|
||||
|
||||
export default class GitlabLicense extends GitLabBase {
|
||||
static category = 'license'
|
||||
|
||||
static route = {
|
||||
base: 'gitlab/license',
|
||||
pattern: ':project+',
|
||||
queryParamSchema,
|
||||
}
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'GitLab',
|
||||
namedParams: {
|
||||
project: 'gitlab-org/gitlab',
|
||||
},
|
||||
staticPreview: {
|
||||
label: 'license',
|
||||
message: 'MIT License',
|
||||
color: 'green',
|
||||
},
|
||||
documentation,
|
||||
},
|
||||
{
|
||||
title: 'GitLab (self-managed)',
|
||||
namedParams: {
|
||||
project: 'gitlab-cn/gitlab',
|
||||
},
|
||||
queryParams: { gitlab_url: 'https://jihulab.com' },
|
||||
staticPreview: {
|
||||
label: 'license',
|
||||
message: 'MIT License',
|
||||
color: 'green',
|
||||
},
|
||||
documentation: customDocumentation,
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = { label: 'license' }
|
||||
|
||||
static render({ license }) {
|
||||
if (license) {
|
||||
return renderLicenseBadge({ license })
|
||||
} else {
|
||||
return { message: 'not specified' }
|
||||
}
|
||||
}
|
||||
|
||||
async fetch({ project, baseUrl }) {
|
||||
// https://docs.gitlab.com/ee/api/projects.html#get-single-project
|
||||
return super.fetch({
|
||||
schema,
|
||||
url: `${baseUrl}/api/v4/projects/${encodeURIComponent(project)}`,
|
||||
options: { searchParams: { license: '1' } },
|
||||
errorMessages: {
|
||||
404: 'repo not found',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async handle({ project }, { gitlab_url: baseUrl = 'https://gitlab.com' }) {
|
||||
const { license: licenseObject } = await this.fetch({
|
||||
project,
|
||||
baseUrl,
|
||||
})
|
||||
const license = licenseObject ? licenseObject.name : undefined
|
||||
return this.constructor.render({ license })
|
||||
}
|
||||
}
|
||||
69
services/gitlab/gitlab-license.tester.js
Normal file
69
services/gitlab/gitlab-license.tester.js
Normal file
@@ -0,0 +1,69 @@
|
||||
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()
|
||||
|
||||
t.create('License')
|
||||
.get('/guoxudong.io/shields-test/licenced-test.json')
|
||||
.expectBadge({
|
||||
label: 'license',
|
||||
message: 'MIT License',
|
||||
color: `${publicDomainLicenseColor}`,
|
||||
})
|
||||
|
||||
t.create('License for repo without a license')
|
||||
.get('/guoxudong.io/shields-test/no-license-test.json')
|
||||
.expectBadge({
|
||||
label: 'license',
|
||||
message: 'not specified',
|
||||
color: 'lightgrey',
|
||||
})
|
||||
|
||||
t.create('Other license').get('/gitlab-org/gitlab-foss.json').expectBadge({
|
||||
label: 'license',
|
||||
message: 'Other',
|
||||
color: unknownLicenseColor,
|
||||
})
|
||||
|
||||
t.create('License for unknown repo')
|
||||
.get('/user1/gitlab-does-not-have-this-repo.json')
|
||||
.expectBadge({
|
||||
label: 'license',
|
||||
message: 'repo not found',
|
||||
color: 'red',
|
||||
})
|
||||
|
||||
t.create('Mocking License')
|
||||
.get('/group/project.json')
|
||||
.intercept(nock =>
|
||||
nock('https://gitlab.com')
|
||||
.get('/api/v4/projects/group%2Fproject?license=1')
|
||||
.reply(200, {
|
||||
license: {
|
||||
key: 'apache-2.0',
|
||||
name: 'Apache License 2.0',
|
||||
nickname: '',
|
||||
html_url: 'http://choosealicense.com/licenses/apache-2.0/',
|
||||
source_url: '',
|
||||
},
|
||||
})
|
||||
)
|
||||
.expectBadge({
|
||||
label: '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}`,
|
||||
})
|
||||
@@ -54,7 +54,7 @@ class GitlabPipelineStatus extends BaseSvgScrapingService {
|
||||
documentation,
|
||||
},
|
||||
{
|
||||
title: 'Gitlab pipeline status (self-hosted)',
|
||||
title: 'Gitlab pipeline status (self-managed)',
|
||||
namedParams: { project: 'GNOME/pango' },
|
||||
queryParams: { gitlab_url: 'https://gitlab.gnome.org', branch: 'master' },
|
||||
staticPreview: this.render({ status: 'passed' }),
|
||||
|
||||
@@ -15,7 +15,7 @@ t.create('Pipeline status')
|
||||
|
||||
t.create('Pipeline status (nested groups)')
|
||||
.get(
|
||||
'/pipeline-status/megabyte-labs/dockerfile/ci-pipeline/ansible-lint.json?branch=master'
|
||||
'/pipeline-status/megabyte-labs/docker/ci-pipeline/ansible.json?branch=master'
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'build',
|
||||
|
||||
@@ -65,7 +65,7 @@ export default class GitLabRelease extends GitLabBase {
|
||||
staticPreview: renderVersionBadge({ version: 'v5.0.0-beta.1' }),
|
||||
},
|
||||
{
|
||||
title: 'GitLab Release (custom instance)',
|
||||
title: 'GitLab Release (self-managed)',
|
||||
namedParams: {
|
||||
project: 'GNOME/librsvg',
|
||||
},
|
||||
|
||||
48
services/gitlab/gitlab-release.spec.js
Normal file
48
services/gitlab/gitlab-release.spec.js
Normal file
@@ -0,0 +1,48 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -36,7 +36,7 @@ t.create('Release (release display name)')
|
||||
.get('/gitlab-org/gitlab.json?display_name=release')
|
||||
.expectBadge({ label: 'release', message: isGitLabDisplayVersion })
|
||||
|
||||
t.create('Release (custom instance')
|
||||
t.create('Release (custom instance)')
|
||||
.get('/GNOME/librsvg.json?gitlab_url=https://gitlab.gnome.org')
|
||||
.expectBadge({ label: 'release', message: isSemver, color: 'blue' })
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ export default class GitlabTag extends GitLabBase {
|
||||
staticPreview: this.render({ version: 'v5.0.0-beta.1', sort: 'semver' }),
|
||||
},
|
||||
{
|
||||
title: 'GitLab tag (custom instance)',
|
||||
title: 'GitLab tag (self-managed)',
|
||||
namedParams: {
|
||||
project: 'GNOME/librsvg',
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
.basicAuth({ user: '', pass: fakeToken })
|
||||
.matchHeader('Authorization', `Bearer ${fakeToken}`)
|
||||
.reply(200, [{ name: '1.9' }])
|
||||
|
||||
expect(
|
||||
|
||||
@@ -7,7 +7,7 @@ t.create('Tag (latest by date)')
|
||||
.expectBadge({ label: 'tag', message: 'v2.0.0', color: 'blue' })
|
||||
|
||||
t.create('Tag (nested groups)')
|
||||
.get('/megabyte-labs/docker/ci-pipeline/ansible-lint.json')
|
||||
.get('/megabyte-labs/docker/ci-pipeline/ansible.json')
|
||||
.expectBadge({ label: 'tag', message: isSemver, color: 'blue' })
|
||||
|
||||
t.create('Tag (project id latest by date)')
|
||||
|
||||
24
services/greasyfork/greasyfork-base.js
Normal file
24
services/greasyfork/greasyfork-base.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import Joi from 'joi'
|
||||
import { nonNegativeInteger } from '../validators.js'
|
||||
import { BaseJsonService } from '../index.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
daily_installs: nonNegativeInteger,
|
||||
total_installs: nonNegativeInteger,
|
||||
good_ratings: nonNegativeInteger,
|
||||
ok_ratings: nonNegativeInteger,
|
||||
bad_ratings: nonNegativeInteger,
|
||||
version: Joi.string().required(),
|
||||
license: Joi.string().allow(null).required(),
|
||||
}).required()
|
||||
|
||||
export default class BaseGreasyForkService extends BaseJsonService {
|
||||
static defaultBadgeData = { label: 'greasy fork' }
|
||||
|
||||
async fetch({ scriptId }) {
|
||||
return this._requestJson({
|
||||
schema,
|
||||
url: `https://greasyfork.org/scripts/${scriptId}.json`,
|
||||
})
|
||||
}
|
||||
}
|
||||
35
services/greasyfork/greasyfork-downloads.service.js
Normal file
35
services/greasyfork/greasyfork-downloads.service.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { renderDownloadsBadge } from '../downloads.js'
|
||||
import BaseGreasyForkService from './greasyfork-base.js'
|
||||
|
||||
export default class GreasyForkInstalls extends BaseGreasyForkService {
|
||||
static category = 'downloads'
|
||||
static route = { base: 'greasyfork', pattern: ':variant(dt|dd)/:scriptId' }
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'Greasy Fork',
|
||||
pattern: 'dd/:scriptId',
|
||||
namedParams: { scriptId: '407466' },
|
||||
staticPreview: renderDownloadsBadge({ downloads: 17, interval: 'day' }),
|
||||
},
|
||||
{
|
||||
title: 'Greasy Fork',
|
||||
pattern: 'dt/:scriptId',
|
||||
namedParams: { scriptId: '407466' },
|
||||
staticPreview: renderDownloadsBadge({ downloads: 3420 }),
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = { label: 'installs' }
|
||||
|
||||
async handle({ variant, scriptId }) {
|
||||
const data = await this.fetch({ scriptId })
|
||||
if (variant === 'dd') {
|
||||
const downloads = data.daily_installs
|
||||
const interval = 'day'
|
||||
return renderDownloadsBadge({ downloads, interval })
|
||||
}
|
||||
const downloads = data.total_installs
|
||||
return renderDownloadsBadge({ downloads })
|
||||
}
|
||||
}
|
||||
19
services/greasyfork/greasyfork-downloads.tester.js
Normal file
19
services/greasyfork/greasyfork-downloads.tester.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { createServiceTester } from '../tester.js'
|
||||
import { isMetric, isMetricOverTimePeriod } from '../test-validators.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('Daily Installs')
|
||||
.get('/dd/407466.json')
|
||||
.expectBadge({ label: 'installs', message: isMetricOverTimePeriod })
|
||||
|
||||
t.create('Daily Installs (not found)')
|
||||
.get('/dd/000000.json')
|
||||
.expectBadge({ label: 'installs', message: 'not found' })
|
||||
|
||||
t.create('Total Installs')
|
||||
.get('/dt/407466.json')
|
||||
.expectBadge({ label: 'installs', message: isMetric })
|
||||
|
||||
t.create('Total Installs (not found)')
|
||||
.get('/dt/000000.json')
|
||||
.expectBadge({ label: 'installs', message: 'not found' })
|
||||
34
services/greasyfork/greasyfork-license.service.js
Normal file
34
services/greasyfork/greasyfork-license.service.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { renderLicenseBadge } from '../licenses.js'
|
||||
import { InvalidResponse } from '../index.js'
|
||||
import BaseGreasyForkService from './greasyfork-base.js'
|
||||
|
||||
export default class GreasyForkLicense extends BaseGreasyForkService {
|
||||
static category = 'license'
|
||||
static route = { base: 'greasyfork', pattern: 'l/:scriptId' }
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'Greasy Fork',
|
||||
namedParams: { scriptId: '407466' },
|
||||
staticPreview: renderLicenseBadge({ licenses: ['MIT'] }),
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = { label: 'license' }
|
||||
|
||||
transform({ data }) {
|
||||
if (data.license === null) {
|
||||
throw new InvalidResponse({
|
||||
prettyMessage: 'license not found',
|
||||
})
|
||||
}
|
||||
// remove suffix " License" from data.license
|
||||
return { license: data.license.replace(/ License$/, '') }
|
||||
}
|
||||
|
||||
async handle({ scriptId }) {
|
||||
const data = await this.fetch({ scriptId })
|
||||
const { license } = this.transform({ data })
|
||||
return renderLicenseBadge({ licenses: [license] })
|
||||
}
|
||||
}
|
||||
11
services/greasyfork/greasyfork-license.tester.js
Normal file
11
services/greasyfork/greasyfork-license.tester.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('License (valid)').get('/l/407466.json').expectBadge({
|
||||
label: 'license',
|
||||
message: 'MIT',
|
||||
})
|
||||
|
||||
t.create('License (not found)')
|
||||
.get('/l/000000.json')
|
||||
.expectBadge({ label: 'license', message: 'not found' })
|
||||
40
services/greasyfork/greasyfork-rating.service.js
Normal file
40
services/greasyfork/greasyfork-rating.service.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { floorCount as floorCountColor } from '../color-formatters.js'
|
||||
import { metric } from '../text-formatters.js'
|
||||
import BaseGreasyForkService from './greasyfork-base.js'
|
||||
|
||||
export default class GreasyForkRatingCount extends BaseGreasyForkService {
|
||||
static category = 'rating'
|
||||
static route = { base: 'greasyfork', pattern: 'rating-count/:scriptId' }
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'Greasy Fork',
|
||||
namedParams: { scriptId: '407466' },
|
||||
staticPreview: this.render({ good: 17, ok: 2, bad: 3 }),
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = { label: 'rating' }
|
||||
|
||||
static render({ good, ok, bad }) {
|
||||
let color = 'lightgrey'
|
||||
const total = good + bad + ok
|
||||
if (total > 0) {
|
||||
const score = (good * 3 + ok * 2 + bad * 1) / total - 1
|
||||
color = floorCountColor(score, 1, 1.5, 2)
|
||||
}
|
||||
return {
|
||||
message: `${metric(good)} good, ${metric(ok)} ok, ${metric(bad)} bad`,
|
||||
color,
|
||||
}
|
||||
}
|
||||
|
||||
async handle({ scriptId }) {
|
||||
const data = await this.fetch({ scriptId })
|
||||
return this.constructor.render({
|
||||
good: data.good_ratings,
|
||||
ok: data.ok_ratings,
|
||||
bad: data.bad_ratings,
|
||||
})
|
||||
}
|
||||
}
|
||||
31
services/greasyfork/greasyfork-rating.spec.js
Normal file
31
services/greasyfork/greasyfork-rating.spec.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { test, given } from 'sazerac'
|
||||
import GreasyForkRatingCount from './greasyfork-rating.service.js'
|
||||
|
||||
describe('GreasyForkRatingCount', function () {
|
||||
test(GreasyForkRatingCount.render, () => {
|
||||
given({ good: 0, ok: 0, bad: 30 }).expect({
|
||||
message: '0 good, 0 ok, 30 bad',
|
||||
color: 'red',
|
||||
})
|
||||
given({ good: 10, ok: 20, bad: 30 }).expect({
|
||||
message: '10 good, 20 ok, 30 bad',
|
||||
color: 'yellow',
|
||||
})
|
||||
given({ good: 10, ok: 20, bad: 10 }).expect({
|
||||
message: '10 good, 20 ok, 10 bad',
|
||||
color: 'yellowgreen',
|
||||
})
|
||||
given({ good: 20, ok: 10, bad: 0 }).expect({
|
||||
message: '20 good, 10 ok, 0 bad',
|
||||
color: 'green',
|
||||
})
|
||||
given({ good: 30, ok: 0, bad: 0 }).expect({
|
||||
message: '30 good, 0 ok, 0 bad',
|
||||
color: 'brightgreen',
|
||||
})
|
||||
given({ good: 0, ok: 0, bad: 0 }).expect({
|
||||
message: '0 good, 0 ok, 0 bad',
|
||||
color: 'lightgrey',
|
||||
})
|
||||
})
|
||||
})
|
||||
14
services/greasyfork/greasyfork-rating.tester.js
Normal file
14
services/greasyfork/greasyfork-rating.tester.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import Joi from 'joi'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('Rating Count')
|
||||
.get('/rating-count/407466.json')
|
||||
.expectBadge({
|
||||
label: 'rating',
|
||||
message: Joi.string().regex(/^\d+ good, \d+ ok, \d+ bad$/),
|
||||
})
|
||||
|
||||
t.create('Rating Count (not found)')
|
||||
.get('/rating-count/000000.json')
|
||||
.expectBadge({ label: 'rating', message: 'not found' })
|
||||
20
services/greasyfork/greasyfork-version.service.js
Normal file
20
services/greasyfork/greasyfork-version.service.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
import BaseGreasyForkService from './greasyfork-base.js'
|
||||
|
||||
export default class GreasyForkVersion extends BaseGreasyForkService {
|
||||
static category = 'version'
|
||||
static route = { base: 'greasyfork', pattern: 'v/:scriptId' }
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'Greasy Fork',
|
||||
namedParams: { scriptId: '407466' },
|
||||
staticPreview: renderVersionBadge({ version: '3.9.3' }),
|
||||
},
|
||||
]
|
||||
|
||||
async handle({ scriptId }) {
|
||||
const data = await this.fetch({ scriptId })
|
||||
return renderVersionBadge({ version: data.version })
|
||||
}
|
||||
}
|
||||
12
services/greasyfork/greasyfork-version.tester.js
Normal file
12
services/greasyfork/greasyfork-version.tester.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { isVPlusDottedVersionAtLeastOne } from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('Version').get('/v/407466.json').expectBadge({
|
||||
label: 'greasy fork',
|
||||
message: isVPlusDottedVersionAtLeastOne,
|
||||
})
|
||||
|
||||
t.create('Version (not found)')
|
||||
.get('/v/000000.json')
|
||||
.expectBadge({ label: 'greasy fork', message: 'not found' })
|
||||
@@ -31,7 +31,7 @@ export default class JitPackVersion extends BaseJsonService {
|
||||
static defaultBadgeData = { label: 'jitpack' }
|
||||
|
||||
async fetch({ vcs, user, repo }) {
|
||||
const url = `https://jitpack.io/api/builds/com.${vcs}.${user}/${repo}/latest`
|
||||
const url = `https://jitpack.io/api/builds/com.${vcs}.${user}/${repo}/latestOk`
|
||||
|
||||
return this._requestJson({
|
||||
schema,
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/**
|
||||
* Common functions and utilities for tasks related to license badges.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import toArray from '../core/base-service/to-array.js'
|
||||
|
||||
const licenseTypes = {
|
||||
@@ -88,6 +94,11 @@ 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]
|
||||
@@ -99,6 +110,12 @@ 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]
|
||||
@@ -113,6 +130,17 @@ 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)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import moment from 'moment'
|
||||
import dayjs from 'dayjs'
|
||||
import semver from 'semver'
|
||||
import { getCachedResource } from '../../core/base-service/resource-cache.js'
|
||||
|
||||
@@ -23,7 +23,7 @@ async function getVersion(version) {
|
||||
}
|
||||
|
||||
function ltsVersionsScraper(versions) {
|
||||
const currentDate = moment().format(dateFormat)
|
||||
const currentDate = dayjs().format(dateFormat)
|
||||
return Object.keys(versions).filter(function (version) {
|
||||
const data = versions[version]
|
||||
return data.lts && data.lts < currentDate && data.end > currentDate
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import moment from 'moment'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const dateFormat = 'YYYY-MM-DD'
|
||||
|
||||
@@ -67,7 +67,7 @@ const mockVersionsSha = () => nock => {
|
||||
}
|
||||
|
||||
const mockReleaseSchedule = () => nock => {
|
||||
const currentDate = moment()
|
||||
const currentDate = dayjs()
|
||||
const schedule = {
|
||||
'v0.10': {
|
||||
start: '2013-03-11',
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { isVPlusDottedVersionAtLeastOne } from '../test-validators.js'
|
||||
import { isVPlusDottedVersionNClausesWithOptionalSuffix } from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('Nucleus (pluginId nucleus)').get('/nucleus.json').expectBadge({
|
||||
label: 'version',
|
||||
message: isVPlusDottedVersionAtLeastOne,
|
||||
message: isVPlusDottedVersionNClausesWithOptionalSuffix,
|
||||
})
|
||||
|
||||
t.create('Invalid Plugin (pluginId 1)').get('/1.json').expectBadge({
|
||||
|
||||
@@ -9,16 +9,11 @@ const schema = Joi.object({
|
||||
license: Joi.string().allow('').allow(null),
|
||||
classifiers: Joi.array().items(Joi.string()).required(),
|
||||
}).required(),
|
||||
releases: Joi.object()
|
||||
.pattern(
|
||||
Joi.string(),
|
||||
Joi.array()
|
||||
.items(
|
||||
Joi.object({
|
||||
packagetype: Joi.string().required(),
|
||||
})
|
||||
)
|
||||
.required()
|
||||
urls: Joi.array()
|
||||
.items(
|
||||
Joi.object({
|
||||
packagetype: Joi.string().required(),
|
||||
})
|
||||
)
|
||||
.required(),
|
||||
}).required()
|
||||
|
||||
@@ -1,45 +1,12 @@
|
||||
import PypiBase from './pypi-base.js'
|
||||
import { sortDjangoVersions, parseClassifiers } from './pypi-helpers.js'
|
||||
import { redirector } from '../index.js'
|
||||
|
||||
export default class PypiDjangoVersions extends PypiBase {
|
||||
static category = 'platform-support'
|
||||
|
||||
static route = this.buildRoute('pypi/djversions')
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'PyPI - Django Version',
|
||||
pattern: ':packageName',
|
||||
namedParams: { packageName: 'djangorestframework' },
|
||||
staticPreview: this.render({ versions: ['1.11', '2.0', '2.1'] }),
|
||||
keywords: ['python'],
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = { label: 'django versions' }
|
||||
|
||||
static render({ versions }) {
|
||||
if (versions.length > 0) {
|
||||
return {
|
||||
message: sortDjangoVersions(versions).join(' | '),
|
||||
color: 'blue',
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
message: 'missing',
|
||||
color: 'red',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handle({ egg }) {
|
||||
const packageData = await this.fetch({ egg })
|
||||
|
||||
const versions = parseClassifiers(
|
||||
packageData,
|
||||
/^Framework :: Django :: ([\d.]+)$/
|
||||
)
|
||||
|
||||
return this.constructor.render({ versions })
|
||||
}
|
||||
}
|
||||
export default redirector({
|
||||
category: 'platform-support',
|
||||
route: {
|
||||
base: 'pypi/djversions',
|
||||
pattern: ':packageName*',
|
||||
},
|
||||
transformPath: ({ packageName }) =>
|
||||
`/pypi/frameworkversions/django/${packageName}`,
|
||||
dateAdded: new Date('2022-07-28'),
|
||||
})
|
||||
|
||||
@@ -1,32 +1,24 @@
|
||||
import Joi from 'joi'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
const isPipeSeparatedDjangoVersions = Joi.string().regex(
|
||||
/^([1-9]\.[0-9]+(?: \| )?)+$/
|
||||
t.create(
|
||||
'redirect supported django versions (valid, package version in request)'
|
||||
)
|
||||
|
||||
t.create('supported django versions (valid, package version in request)')
|
||||
.get('/djangorestframework/3.7.3.json')
|
||||
.expectBadge({
|
||||
label: 'django versions',
|
||||
message: isPipeSeparatedDjangoVersions,
|
||||
})
|
||||
.expectRedirect(
|
||||
'/pypi/frameworkversions/django/djangorestframework/3.7.3.json'
|
||||
)
|
||||
|
||||
t.create('supported django versions (valid, no package version specified)')
|
||||
t.create(
|
||||
'redirect supported django versions (valid, no package version specified)'
|
||||
)
|
||||
.get('/djangorestframework.json')
|
||||
.expectBadge({
|
||||
label: 'django versions',
|
||||
message: isPipeSeparatedDjangoVersions,
|
||||
})
|
||||
.expectRedirect('/pypi/frameworkversions/django/djangorestframework.json')
|
||||
|
||||
t.create('supported django versions (no versions specified)')
|
||||
t.create('redirect supported django versions (no versions specified)')
|
||||
.get('/django/1.11.json')
|
||||
.expectBadge({ label: 'django versions', message: 'missing' })
|
||||
.expectRedirect('/pypi/frameworkversions/django/django/1.11.json')
|
||||
|
||||
t.create('supported django versions (invalid)')
|
||||
t.create('redirect supported django versions (invalid)')
|
||||
.get('/not-a-package.json')
|
||||
.expectBadge({
|
||||
label: 'django versions',
|
||||
message: 'package or version not found',
|
||||
})
|
||||
.expectRedirect('/pypi/frameworkversions/django/not-a-package.json')
|
||||
|
||||
103
services/pypi/pypi-framework-versions.service.js
Normal file
103
services/pypi/pypi-framework-versions.service.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import { InvalidResponse } from '../index.js'
|
||||
import PypiBase from './pypi-base.js'
|
||||
import { sortPypiVersions, parseClassifiers } from './pypi-helpers.js'
|
||||
|
||||
const frameworkNameMap = {
|
||||
'aws-cdk': {
|
||||
name: 'AWS CDK',
|
||||
classifier: 'AWS CDK',
|
||||
},
|
||||
django: {
|
||||
name: 'Django',
|
||||
classifier: 'Django',
|
||||
},
|
||||
'django-cms': {
|
||||
name: 'Django CMS',
|
||||
classifier: 'Django CMS',
|
||||
},
|
||||
jupyterlab: {
|
||||
name: 'JupyterLab',
|
||||
classifier: 'Jupyter :: JupyterLab',
|
||||
},
|
||||
odoo: {
|
||||
name: 'Odoo',
|
||||
classifier: 'Odoo',
|
||||
},
|
||||
plone: {
|
||||
name: 'Plone',
|
||||
classifier: 'Plone',
|
||||
},
|
||||
wagtail: {
|
||||
name: 'Wagtail',
|
||||
classifier: 'Wagtail',
|
||||
},
|
||||
zope: {
|
||||
name: 'Zope',
|
||||
classifier: 'Zope',
|
||||
},
|
||||
}
|
||||
|
||||
const documentation = `
|
||||
<p>
|
||||
This service currently support the following Frameworks: <br/>
|
||||
${Object.values(frameworkNameMap).map(obj => `<strong>${obj.name}</strong>`)}
|
||||
</p>
|
||||
`
|
||||
export default class PypiFrameworkVersion extends PypiBase {
|
||||
static category = 'platform-support'
|
||||
|
||||
static route = {
|
||||
base: 'pypi/frameworkversions',
|
||||
pattern: `:frameworkName(${Object.keys(frameworkNameMap).join(
|
||||
'|'
|
||||
)})/:packageName*`,
|
||||
}
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'PyPI - Versions from Framework Classifiers',
|
||||
namedParams: {
|
||||
frameworkName: 'Plone',
|
||||
packageName: 'plone.volto',
|
||||
},
|
||||
staticPreview: this.render({
|
||||
name: 'Plone',
|
||||
versions: ['5.2', '6.0'],
|
||||
}),
|
||||
keywords: ['python'],
|
||||
documentation,
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = { label: 'versions' }
|
||||
|
||||
static render({ name, versions }) {
|
||||
name = name ? name.toLowerCase() : ''
|
||||
const label = `${name} versions`
|
||||
return {
|
||||
label,
|
||||
message: sortPypiVersions(versions).join(' | '),
|
||||
color: 'blue',
|
||||
}
|
||||
}
|
||||
|
||||
async handle({ frameworkName, packageName }) {
|
||||
const classifier = frameworkNameMap[frameworkName]
|
||||
? frameworkNameMap[frameworkName].classifier
|
||||
: frameworkName
|
||||
const name = frameworkNameMap[frameworkName]
|
||||
? frameworkNameMap[frameworkName].name
|
||||
: frameworkName
|
||||
const regex = new RegExp(`^Framework :: ${classifier} :: ([\\d.]+)$`)
|
||||
const packageData = await this.fetch({ egg: packageName })
|
||||
const versions = parseClassifiers(packageData, regex)
|
||||
|
||||
if (versions.length === 0) {
|
||||
throw new InvalidResponse({
|
||||
prettyMessage: `${name} versions are missing for ${packageName}`,
|
||||
})
|
||||
}
|
||||
|
||||
return this.constructor.render({ name, versions })
|
||||
}
|
||||
}
|
||||
164
services/pypi/pypi-framework-versions.tester.js
Normal file
164
services/pypi/pypi-framework-versions.tester.js
Normal file
@@ -0,0 +1,164 @@
|
||||
import Joi from 'joi'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
const isPipeSeparatedFrameworkVersions = Joi.string().regex(
|
||||
/^([1-9]+(\.[0-9]+)?(?: \| )?)+$/
|
||||
)
|
||||
|
||||
t.create('supported django versions (valid, package version in request)')
|
||||
.get('/django/djangorestframework/3.7.3.json')
|
||||
.expectBadge({
|
||||
label: 'django versions',
|
||||
message: isPipeSeparatedFrameworkVersions,
|
||||
})
|
||||
|
||||
t.create('supported django versions (valid, no package version specified)')
|
||||
.get('/django/djangorestframework.json')
|
||||
.expectBadge({
|
||||
label: 'django versions',
|
||||
message: isPipeSeparatedFrameworkVersions,
|
||||
})
|
||||
|
||||
t.create('supported django versions (no versions specified)')
|
||||
.get('/django/django/1.11.json')
|
||||
.expectBadge({
|
||||
label: 'versions',
|
||||
message: 'Django versions are missing for django/1.11',
|
||||
})
|
||||
|
||||
t.create('supported django versions (invalid)')
|
||||
.get('/django/not-a-package.json')
|
||||
.expectBadge({
|
||||
label: 'versions',
|
||||
message: 'package or version not found',
|
||||
})
|
||||
|
||||
t.create('supported plone versions (valid, package version in request)')
|
||||
.get('/plone/plone.rest/1.6.2.json')
|
||||
.expectBadge({ label: 'plone versions', message: '4.3 | 5.0 | 5.1 | 5.2' })
|
||||
|
||||
t.create('supported plone versions (valid, no package version specified)')
|
||||
.get('/plone/plone.rest.json')
|
||||
.expectBadge({
|
||||
label: 'plone versions',
|
||||
message: isPipeSeparatedFrameworkVersions,
|
||||
})
|
||||
|
||||
t.create('supported plone versions (invalid)')
|
||||
.get('/plone/not-a-package.json')
|
||||
.expectBadge({
|
||||
label: 'versions',
|
||||
message: 'package or version not found',
|
||||
})
|
||||
|
||||
t.create('supported zope versions (valid, package version in request)')
|
||||
.get('/zope/plone/5.2.9.json')
|
||||
.expectBadge({ label: 'zope versions', message: '4' })
|
||||
|
||||
t.create('supported zope versions (valid, no package version specified)')
|
||||
.get('/zope/Plone.json')
|
||||
.expectBadge({
|
||||
label: 'zope versions',
|
||||
message: isPipeSeparatedFrameworkVersions,
|
||||
})
|
||||
|
||||
t.create('supported zope versions (invalid)')
|
||||
.get('/zope/not-a-package.json')
|
||||
.expectBadge({
|
||||
label: 'versions',
|
||||
message: 'package or version not found',
|
||||
})
|
||||
|
||||
t.create('supported wagtail versions (valid, package version in request)')
|
||||
.get('/wagtail/wagtail-headless-preview/0.3.0.json')
|
||||
.expectBadge({ label: 'wagtail versions', message: '2 | 3' })
|
||||
|
||||
t.create('supported wagtail versions (valid, no package version specified)')
|
||||
.get('/wagtail/wagtail-headless-preview.json')
|
||||
.expectBadge({
|
||||
label: 'wagtail versions',
|
||||
message: isPipeSeparatedFrameworkVersions,
|
||||
})
|
||||
|
||||
t.create('supported wagtail versions (invalid)')
|
||||
.get('/wagtail/not-a-package.json')
|
||||
.expectBadge({
|
||||
label: 'versions',
|
||||
message: 'package or version not found',
|
||||
})
|
||||
|
||||
t.create('supported django cms versions (valid, package version in request)')
|
||||
.get('/django-cms/djangocms-ads/1.1.0.json')
|
||||
.expectBadge({
|
||||
label: 'django cms versions',
|
||||
message: '3.7 | 3.8 | 3.9 | 3.10',
|
||||
})
|
||||
|
||||
t.create('supported django cms versions (valid, no package version specified)')
|
||||
.get('/django-cms/djangocms-ads.json')
|
||||
.expectBadge({
|
||||
label: 'django cms versions',
|
||||
message: isPipeSeparatedFrameworkVersions,
|
||||
})
|
||||
|
||||
t.create('supported django cms versions (invalid)')
|
||||
.get('/django-cms/not-a-package.json')
|
||||
.expectBadge({
|
||||
label: 'versions',
|
||||
message: 'package or version not found',
|
||||
})
|
||||
|
||||
t.create('supported odoo versions (valid, package version in request)')
|
||||
.get('/odoo/odoo-addon-sale-tier-validation/15.0.1.0.0.6.json')
|
||||
.expectBadge({ label: 'odoo versions', message: '15.0' })
|
||||
|
||||
t.create('supported odoo versions (valid, no package version specified)')
|
||||
.get('/odoo/odoo-addon-sale-tier-validation.json')
|
||||
.expectBadge({
|
||||
label: 'odoo versions',
|
||||
message: isPipeSeparatedFrameworkVersions,
|
||||
})
|
||||
|
||||
t.create('supported odoo versions (invalid)')
|
||||
.get('/odoo/not-a-package.json')
|
||||
.expectBadge({
|
||||
label: 'versions',
|
||||
message: 'package or version not found',
|
||||
})
|
||||
|
||||
t.create('supported aws cdk versions (valid, package version in request)')
|
||||
.get('/aws-cdk/aws-cdk.aws-glue-alpha/2.34.0a0.json')
|
||||
.expectBadge({ label: 'aws cdk versions', message: '2' })
|
||||
|
||||
t.create('supported aws cdk versions (valid, no package version specified)')
|
||||
.get('/aws-cdk/aws-cdk.aws-glue-alpha.json')
|
||||
.expectBadge({
|
||||
label: 'aws cdk versions',
|
||||
message: isPipeSeparatedFrameworkVersions,
|
||||
})
|
||||
|
||||
t.create('supported aws cdk versions (invalid)')
|
||||
.get('/aws-cdk/not-a-package.json')
|
||||
.expectBadge({
|
||||
label: 'versions',
|
||||
message: 'package or version not found',
|
||||
})
|
||||
|
||||
t.create('supported jupyterlab versions (valid, package version in request)')
|
||||
.get('/jupyterlab/structured-text/0.0.2.json')
|
||||
.expectBadge({ label: 'jupyterlab versions', message: '3' })
|
||||
|
||||
t.create('supported jupyterlab versions (valid, no package version specified)')
|
||||
.get('/jupyterlab/structured-text.json')
|
||||
.expectBadge({
|
||||
label: 'jupyterlab versions',
|
||||
message: isPipeSeparatedFrameworkVersions,
|
||||
})
|
||||
|
||||
t.create('supported jupyterlab versions (invalid)')
|
||||
.get('/jupyterlab/not-a-package.json')
|
||||
.expectBadge({
|
||||
label: 'versions',
|
||||
message: 'package or version not found',
|
||||
})
|
||||
@@ -6,7 +6,7 @@
|
||||
our own functions to parse and sort django versions
|
||||
*/
|
||||
|
||||
function parseDjangoVersionString(str) {
|
||||
function parsePypiVersionString(str) {
|
||||
if (typeof str !== 'string') {
|
||||
return false
|
||||
}
|
||||
@@ -20,18 +20,12 @@ function parseDjangoVersionString(str) {
|
||||
}
|
||||
|
||||
// Sort an array of django versions low to high.
|
||||
function sortDjangoVersions(versions) {
|
||||
function sortPypiVersions(versions) {
|
||||
return versions.sort((a, b) => {
|
||||
if (
|
||||
parseDjangoVersionString(a).major === parseDjangoVersionString(b).major
|
||||
) {
|
||||
return (
|
||||
parseDjangoVersionString(a).minor - parseDjangoVersionString(b).minor
|
||||
)
|
||||
if (parsePypiVersionString(a).major === parsePypiVersionString(b).major) {
|
||||
return parsePypiVersionString(a).minor - parsePypiVersionString(b).minor
|
||||
} else {
|
||||
return (
|
||||
parseDjangoVersionString(a).major - parseDjangoVersionString(b).major
|
||||
)
|
||||
return parsePypiVersionString(a).major - parsePypiVersionString(b).major
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -88,16 +82,12 @@ function getLicenses(packageData) {
|
||||
}
|
||||
|
||||
function getPackageFormats(packageData) {
|
||||
const {
|
||||
info: { version },
|
||||
releases,
|
||||
} = packageData
|
||||
const releasesForVersion = releases[version]
|
||||
const { urls } = packageData
|
||||
return {
|
||||
hasWheel: releasesForVersion.some(({ packagetype }) =>
|
||||
hasWheel: urls.some(({ packagetype }) =>
|
||||
['wheel', 'bdist_wheel'].includes(packagetype)
|
||||
),
|
||||
hasEgg: releasesForVersion.some(({ packagetype }) =>
|
||||
hasEgg: urls.some(({ packagetype }) =>
|
||||
['egg', 'bdist_egg'].includes(packagetype)
|
||||
),
|
||||
}
|
||||
@@ -105,8 +95,8 @@ function getPackageFormats(packageData) {
|
||||
|
||||
export {
|
||||
parseClassifiers,
|
||||
parseDjangoVersionString,
|
||||
sortDjangoVersions,
|
||||
parsePypiVersionString,
|
||||
sortPypiVersions,
|
||||
getLicenses,
|
||||
getPackageFormats,
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { test, given, forCases } from 'sazerac'
|
||||
import {
|
||||
parseClassifiers,
|
||||
parseDjangoVersionString,
|
||||
sortDjangoVersions,
|
||||
parsePypiVersionString,
|
||||
sortPypiVersions,
|
||||
getLicenses,
|
||||
getPackageFormats,
|
||||
} from './pypi-helpers.js'
|
||||
@@ -60,7 +60,7 @@ describe('PyPI helpers', function () {
|
||||
given(classifiersFixture, /^(?!.*)*$/).expect([])
|
||||
})
|
||||
|
||||
test(parseDjangoVersionString, function () {
|
||||
test(parsePypiVersionString, function () {
|
||||
given('1').expect({ major: 1, minor: 0 })
|
||||
given('1.0').expect({ major: 1, minor: 0 })
|
||||
given('7.2').expect({ major: 7, minor: 2 })
|
||||
@@ -69,7 +69,7 @@ describe('PyPI helpers', function () {
|
||||
given('foo').expect({ major: 0, minor: 0 })
|
||||
})
|
||||
|
||||
test(sortDjangoVersions, function () {
|
||||
test(sortPypiVersions, function () {
|
||||
// Each of these includes a different variant: 2.0, 2, and 2.0rc1.
|
||||
given(['2.0', '1.9', '10', '1.11', '2.1', '2.11']).expect([
|
||||
'1.9',
|
||||
@@ -164,34 +164,17 @@ describe('PyPI helpers', function () {
|
||||
|
||||
test(getPackageFormats, () => {
|
||||
given({
|
||||
info: { version: '2.19.1' },
|
||||
releases: {
|
||||
'1.0.4': [{ packagetype: 'sdist' }],
|
||||
'2.19.1': [{ packagetype: 'bdist_wheel' }, { packagetype: 'sdist' }],
|
||||
},
|
||||
urls: [{ packagetype: 'bdist_wheel' }, { packagetype: 'sdist' }],
|
||||
}).expect({ hasWheel: true, hasEgg: false })
|
||||
given({
|
||||
info: { version: '1.0.4' },
|
||||
releases: {
|
||||
'1.0.4': [{ packagetype: 'sdist' }],
|
||||
'2.19.1': [{ packagetype: 'bdist_wheel' }, { packagetype: 'sdist' }],
|
||||
},
|
||||
urls: [{ packagetype: 'sdist' }],
|
||||
}).expect({ hasWheel: false, hasEgg: false })
|
||||
given({
|
||||
info: { version: '0.8.2' },
|
||||
releases: {
|
||||
0.8: [{ packagetype: 'sdist' }],
|
||||
'0.8.1': [
|
||||
{ packagetype: 'bdist_egg' },
|
||||
{ packagetype: 'bdist_egg' },
|
||||
{ packagetype: 'sdist' },
|
||||
],
|
||||
'0.8.2': [
|
||||
{ packagetype: 'bdist_egg' },
|
||||
{ packagetype: 'bdist_egg' },
|
||||
{ packagetype: 'sdist' },
|
||||
],
|
||||
},
|
||||
urls: [
|
||||
{ packagetype: 'bdist_egg' },
|
||||
{ packagetype: 'bdist_egg' },
|
||||
{ packagetype: 'sdist' },
|
||||
],
|
||||
}).expect({ hasWheel: false, hasEgg: true })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -24,7 +24,7 @@ t.create('license (from trove classifier)')
|
||||
license: '',
|
||||
classifiers: ['License :: OSI Approved :: MIT License'],
|
||||
},
|
||||
releases: {},
|
||||
urls: [],
|
||||
})
|
||||
)
|
||||
.expectBadge({
|
||||
@@ -46,7 +46,7 @@ t.create('license (as acronym from trove classifier)')
|
||||
'License :: OSI Approved :: GNU General Public License (GPL)',
|
||||
],
|
||||
},
|
||||
releases: {},
|
||||
urls: [],
|
||||
})
|
||||
)
|
||||
.expectBadge({
|
||||
|
||||
@@ -43,7 +43,7 @@ t.create('no trove classifiers')
|
||||
license: 'foo',
|
||||
classifiers: [],
|
||||
},
|
||||
releases: {},
|
||||
urls: [],
|
||||
})
|
||||
)
|
||||
.expectBadge({
|
||||
|
||||
154
services/ros/ros-version.service.js
Normal file
154
services/ros/ros-version.service.js
Normal file
@@ -0,0 +1,154 @@
|
||||
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+$/, '')
|
||||
}
|
||||
}
|
||||
44
services/ros/ros-version.service.spec.js
Normal file
44
services/ros/ros-version.service.spec.js
Normal file
@@ -0,0 +1,44 @@
|
||||
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')
|
||||
})
|
||||
})
|
||||
28
services/ros/ros-version.tester.js
Normal file
28
services/ros/ros-version.tester.js
Normal file
@@ -0,0 +1,28 @@
|
||||
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',
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import Joi from 'joi'
|
||||
import moment from 'moment'
|
||||
import dayjs from 'dayjs'
|
||||
import { renderDownloadsBadge } from '../downloads.js'
|
||||
import { nonNegativeInteger } from '../validators.js'
|
||||
import { BaseJsonService } from '../index.js'
|
||||
@@ -15,15 +15,15 @@ const intervalMap = {
|
||||
},
|
||||
dw: {
|
||||
// 6 days, since date range is inclusive,
|
||||
startDate: endDate => moment(endDate).subtract(6, 'days'),
|
||||
startDate: endDate => dayjs(endDate).subtract(6, 'days'),
|
||||
interval: 'week',
|
||||
},
|
||||
dm: {
|
||||
startDate: endDate => moment(endDate).subtract(30, 'days'),
|
||||
startDate: endDate => dayjs(endDate).subtract(30, 'days'),
|
||||
interval: 'month',
|
||||
},
|
||||
dt: {
|
||||
startDate: () => moment(0),
|
||||
startDate: () => dayjs(0),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ export default class Sourceforge extends BaseJsonService {
|
||||
folder ? `${folder}/` : ''
|
||||
}stats/json`
|
||||
// get yesterday since today is incomplete
|
||||
const endDate = moment().subtract(24, 'hours')
|
||||
const endDate = dayjs().subtract(24, 'hours')
|
||||
const startDate = intervalMap[interval].startDate(endDate)
|
||||
const options = {
|
||||
searchParams: {
|
||||
|
||||
@@ -31,7 +31,7 @@ export default class SpackVersion extends BaseJsonService {
|
||||
async fetch({ packageName }) {
|
||||
return this._requestJson({
|
||||
schema,
|
||||
url: `https://spack.github.io/packages/data/packages/${packageName}.json`,
|
||||
url: `https://packages.spack.io/data/packages/${packageName}.json`,
|
||||
errorMessages: {
|
||||
404: 'package not found',
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import moment from 'moment'
|
||||
import dayjs from 'dayjs'
|
||||
import Joi from 'joi'
|
||||
import { nonNegativeInteger } from '../validators.js'
|
||||
import { BaseJsonService } from '../index.js'
|
||||
@@ -19,10 +19,10 @@ export default class StackExchangeMonthlyQuestions extends BaseJsonService {
|
||||
static examples = [
|
||||
{
|
||||
title: 'Stack Exchange monthly questions',
|
||||
namedParams: { stackexchangesite: 'stackoverflow', query: 'momentjs' },
|
||||
namedParams: { stackexchangesite: 'stackoverflow', query: 'dayjs' },
|
||||
staticPreview: this.render({
|
||||
stackexchangesite: 'stackoverflow',
|
||||
query: 'momentjs',
|
||||
query: 'dayjs',
|
||||
numValue: 2000,
|
||||
}),
|
||||
keywords: ['stackexchange', 'stackoverflow'],
|
||||
@@ -41,12 +41,12 @@ export default class StackExchangeMonthlyQuestions extends BaseJsonService {
|
||||
}
|
||||
|
||||
async handle({ stackexchangesite, query }) {
|
||||
const today = moment().toDate()
|
||||
const prevMonthStart = moment(today)
|
||||
const today = dayjs().toDate()
|
||||
const prevMonthStart = dayjs(today)
|
||||
.subtract(1, 'months')
|
||||
.startOf('month')
|
||||
.unix()
|
||||
const prevMonthEnd = moment(today)
|
||||
const prevMonthEnd = dayjs(today)
|
||||
.subtract(1, 'months')
|
||||
.endOf('month')
|
||||
.unix()
|
||||
|
||||
@@ -2,10 +2,10 @@ import { isMetricOverTimePeriod } from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('Monthly Questions for StackOverflow Momentjs')
|
||||
.get('/stackoverflow/qm/momentjs.json')
|
||||
t.create('Monthly Questions for StackOverflow dayjs')
|
||||
.get('/stackoverflow/qm/dayjs.json')
|
||||
.expectBadge({
|
||||
label: 'stackoverflow momentjs questions',
|
||||
label: 'stackoverflow dayjs questions',
|
||||
message: isMetricOverTimePeriod,
|
||||
})
|
||||
|
||||
|
||||
@@ -79,6 +79,8 @@ const isMetricWithPattern = nestedRegexp => {
|
||||
|
||||
const isMetricOpenIssues = isMetricWithPattern(/ open/)
|
||||
|
||||
const isMetricClosedIssues = isMetricWithPattern(/ closed/)
|
||||
|
||||
const isMetricOverMetric = isMetricWithPattern(
|
||||
/\/([1-9][0-9]*[kMGTPEZY]?|[1-9]\.[1-9][kMGTPEZY])/
|
||||
)
|
||||
@@ -167,6 +169,7 @@ export {
|
||||
isMetricAllowNegative,
|
||||
isMetricWithPattern,
|
||||
isMetricOpenIssues,
|
||||
isMetricClosedIssues,
|
||||
isMetricOverMetric,
|
||||
isMetricOverTimePeriod,
|
||||
isZeroOverTimePeriod,
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
* Commonly-used functions for formatting text in badge labels. Includes
|
||||
* ordinal numbers, currency codes, star ratings, versions, etc.
|
||||
*/
|
||||
import moment from 'moment'
|
||||
moment().format()
|
||||
import dayjs from 'dayjs'
|
||||
import calendar from 'dayjs/plugin/calendar.js'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime.js'
|
||||
dayjs.extend(calendar)
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
function starRating(rating, max = 5) {
|
||||
const flooredRating = Math.floor(rating)
|
||||
@@ -109,7 +112,7 @@ function maybePluralize(singular, countable, plural) {
|
||||
}
|
||||
|
||||
function formatDate(d) {
|
||||
const date = moment(d)
|
||||
const date = dayjs(d)
|
||||
const dateString = date.calendar(null, {
|
||||
lastDay: '[yesterday]',
|
||||
sameDay: '[today]',
|
||||
@@ -117,12 +120,12 @@ function formatDate(d) {
|
||||
sameElse: 'MMMM YYYY',
|
||||
})
|
||||
// Trim current year from date string
|
||||
return dateString.replace(` ${moment().year()}`, '').toLowerCase()
|
||||
return dateString.replace(` ${dayjs().year()}`, '').toLowerCase()
|
||||
}
|
||||
|
||||
function formatRelativeDate(timestamp) {
|
||||
return moment()
|
||||
.to(moment.unix(parseInt(timestamp, 10)))
|
||||
return dayjs()
|
||||
.to(dayjs.unix(parseInt(timestamp, 10)))
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import moment from 'moment'
|
||||
import dayjs from 'dayjs'
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat.js'
|
||||
import { InvalidResponse } from '../index.js'
|
||||
import { formatDate } from '../text-formatters.js'
|
||||
import { age as ageColor } from '../color-formatters.js'
|
||||
import { documentation, BaseWordpress } from './wordpress-base.js'
|
||||
dayjs.extend(customParseFormat)
|
||||
|
||||
const extensionData = {
|
||||
plugin: {
|
||||
capt: 'Plugin',
|
||||
exampleSlug: 'bbpress',
|
||||
lastUpdateFormat: 'YYYY-MM-DD hh:mma GMT',
|
||||
lastUpdateFormat: 'YYYY-MM-DD hh:mma [GMT]',
|
||||
},
|
||||
theme: {
|
||||
capt: 'Theme',
|
||||
@@ -50,7 +52,7 @@ function LastUpdateForType(extensionType) {
|
||||
}
|
||||
|
||||
transform(lastUpdate) {
|
||||
const date = moment(lastUpdate, lastUpdateFormat)
|
||||
const date = dayjs(lastUpdate, lastUpdateFormat)
|
||||
|
||||
if (date.isValid()) {
|
||||
return date.format('YYYY-MM-DD')
|
||||
|
||||
Reference in New Issue
Block a user