Compare commits

..

5 Commits

Author SHA1 Message Date
chris48s
8ce5a38207 try this 2022-07-20 19:48:03 +01:00
chris48s
c2dccd15c7 override settings 2022-07-20 19:40:26 +01:00
chris48s
10daa3cb2d try passing env var to job 2022-07-20 19:01:49 +01:00
chris48s
5bd4f02053 constrain to node 16 2022-07-20 18:57:51 +01:00
chris48s
493317736b do nothing 2022-07-12 21:09:58 +01:00
76 changed files with 9484 additions and 10008 deletions

View File

@@ -1,5 +1,47 @@
version: 2
main_steps: &main_steps
steps:
- checkout
- run:
name: Install dependencies
command: |
npm ci
environment:
# https://docs.cypress.io/guides/getting-started/installing-cypress.html#Skipping-installation
# We don't need to install the Cypress binary in jobs that aren't actually running Cypress.
CYPRESS_INSTALL_BINARY: 0
- run:
name: Linter
when: always
command: npm run lint
- run:
name: Core tests
when: always
environment:
mocha_reporter: mocha-junit-reporter
MOCHA_FILE: junit/core/results.xml
command: npm run test:core
- run:
name: Entrypoint tests
when: always
environment:
mocha_reporter: mocha-junit-reporter
MOCHA_FILE: junit/entrypoint/results.xml
command: npm run test:entrypoint
- store_test_results:
path: junit
- run:
name: 'Prettier check (quick fix: `npm run prettier`)'
when: always
command: npm run prettier:check
integration_steps: &integration_steps
steps:
- checkout
@@ -98,6 +140,20 @@ package_steps: &package_steps
path: junit
jobs:
main:
docker:
- image: cimg/node:16.15
<<: *main_steps
main@node-17:
docker:
- image: cimg/node:17.9
environment:
NPM_CONFIG_ENGINE_STRICT: 'false'
<<: *main_steps
integration:
docker:
- image: cimg/node:16.15
@@ -241,6 +297,14 @@ workflows:
on-commit:
jobs:
- main:
filters:
branches:
ignore: gh-pages
- main@node-17:
filters:
branches:
ignore: gh-pages
- integration@node-17:
filters:
branches:
@@ -282,6 +346,12 @@ workflows:
# filters:
# branches:
# ignore: gh-pages
# - main:
# requires:
# - npm-install
# - main@node-latest:
# requires:
# - npm-install
# - frontend:
# requires:
# - npm-install

View File

@@ -21,7 +21,7 @@ A clear and concise description of the new badge.
Where can we get the data from?
- Is there a public API?
- Does the API require an API key?
- Does the API requires an API key?
- Link to the API documentation.
-->

View File

@@ -9,17 +9,16 @@
"version": "0.0.0",
"license": "CC0",
"dependencies": {
"@actions/core": "^1.9.1",
"@actions/core": "^1.9.0",
"@actions/github": "^5.0.3"
}
},
"node_modules/@actions/core": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.1.tgz",
"integrity": "sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==",
"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",
"uuid": "^8.3.2"
"@actions/http-client": "^2.0.1"
}
},
"node_modules/@actions/github": {
@@ -205,14 +204,6 @@
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
@@ -235,12 +226,11 @@
},
"dependencies": {
"@actions/core": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.1.tgz",
"integrity": "sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==",
"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",
"uuid": "^8.3.2"
"@actions/http-client": "^2.0.1"
}
},
"@actions/github": {
@@ -403,11 +393,6 @@
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
},
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
},
"webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",

View File

@@ -10,7 +10,7 @@
"author": "chris48s",
"license": "CC0",
"dependencies": {
"@actions/core": "^1.9.1",
"@actions/core": "^1.9.0",
"@actions/github": "^5.0.3"
}
}

View File

@@ -1,21 +0,0 @@
name: 'Core tests'
description: 'Run core and entrypoint tests'
runs:
using: 'composite'
steps:
- name: Core tests
if: always()
run: npm run test:core -- --reporter json --reporter-option 'output=reports/core.json'
shell: bash
- name: Entrypoint tests
if: always()
run: npm run test:entrypoint -- --reporter json --reporter-option 'output=reports/entrypoint.json'
shell: bash
- name: Write Markdown Summary
if: always()
run: |
node scripts/mocha2md.js Core reports/core.json >> $GITHUB_STEP_SUMMARY
node scripts/mocha2md.js Entrypoint reports/entrypoint.json >> $GITHUB_STEP_SUMMARY
shell: bash

View File

@@ -1,25 +0,0 @@
name: 'Set up project'
description: 'Set up project'
inputs:
node-version:
description: 'Version Spec of the version to use. Examples: 12.x, 10.15.1, >=10.15.0.'
required: true
cypress:
description: 'Install Cypress binary: 0 or 1'
# https://docs.cypress.io/guides/getting-started/installing-cypress.html#Skipping-installation
# We don't need to install the Cypress binary in jobs that aren't actually running Cypress.
required: false
default: 0
runs:
using: 'composite'
steps:
- name: Install Node JS ${{ inputs.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ inputs.node-version }}
- name: Install dependencies
env:
CYPRESS_INSTALL_BINARY: ${{ inputs.cypress }}
run: npm ci
shell: bash

View File

@@ -1,26 +0,0 @@
name: Lint
on:
pull_request:
push:
branches-ignore:
- 'gh-pages'
jobs:
test-lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup
uses: ./.github/actions/setup
with:
node-version: 16
- name: ESLint
if: always()
run: npm run lint
- name: 'Prettier check (quick fix: `npm run prettier`)'
if: always()
run: npm run prettier:check

View File

@@ -1,23 +0,0 @@
name: Main@node 17
on:
pull_request:
push:
branches-ignore:
- 'gh-pages'
jobs:
test-main-17:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup
uses: ./.github/actions/setup
with:
node-version: 17
env:
NPM_CONFIG_ENGINE_STRICT: 'false'
- name: Core tests
uses: ./.github/actions/core-tests

View File

@@ -1,26 +0,0 @@
name: Main
on:
pull_request:
push:
branches-ignore:
- 'gh-pages'
jobs:
test-main:
strategy:
matrix:
os: ['ubuntu-latest', 'windows-latest']
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup
uses: ./.github/actions/setup
with:
node-version: 16
- name: Core tests
uses: ./.github/actions/core-tests

View File

@@ -4,35 +4,6 @@ Note: this changelog is for the shields.io server. The changelog for the badge-m
---
## server-2022-09-04
- fix frontend compile for users running on Windows [#8350](https://github.com/badges/shields/issues/8350)
- [DockerSize] Docker image size multi arch [#8290](https://github.com/badges/shields/issues/8290)
- upgrade gatsby [#8334](https://github.com/badges/shields/issues/8334)
- Custom domains for [JitPack] artifacts [#8333](https://github.com/badges/shields/issues/8333)
- fix [dockerstars] service [#8316](https://github.com/badges/shields/issues/8316)
- [BountySource] Fix: Broken Badge generation for decimal activity values [#8315](https://github.com/badges/shields/issues/8315)
- feat: add [gitlabmergerequests] service [#8166](https://github.com/badges/shields/issues/8166)
- Fix terminology for [ROS] version service [#8292](https://github.com/badges/shields/issues/8292)
- feat: add [GitlabStars] service [#8209](https://github.com/badges/shields/issues/8209)
- Fix invalid `rst` format when `alt` or `target` is present [#8275](https://github.com/badges/shields/issues/8275)
- [GithubGistLastCommit] GitHub gist last commit [#8272](https://github.com/badges/shields/issues/8272)
- [GitHub] GitHub file size for a specific branch [#8262](https://github.com/badges/shields/issues/8262)
- Dependency updates
## 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)

View File

@@ -38,12 +38,12 @@
},
"METRICS_INFLUX_ENABLED": {
"description": "Disable influx metrics",
"value": "false",
"value": "0",
"required": false
},
"REQUIRE_CLOUDFLARE": {
"description": "Allow direct traffic",
"value": "false",
"value": "0",
"required": false
}
},

View File

@@ -13,13 +13,6 @@ const serviceDir = path.join(
'services'
)
function toUnixPath(path) {
// glob does not allow \ as a path separator
// see https://github.com/isaacs/node-glob/blob/main/changelog.md#80
// so we need to convert to use / for use with glob
return path.replace(/\\/g, '/')
}
class InvalidService extends Error {
constructor(message) {
super(message)
@@ -29,9 +22,7 @@ class InvalidService extends Error {
async function loadServiceClasses(servicePaths) {
if (!servicePaths) {
servicePaths = glob.sync(
toUnixPath(path.join(serviceDir, '**', '*.service.js'))
)
servicePaths = glob.sync(path.join(serviceDir, '**', '*.service.js'))
}
const serviceClasses = []
@@ -51,8 +42,8 @@ async function loadServiceClasses(servicePaths) {
if (serviceClass && serviceClass.prototype instanceof BaseService) {
// Decorate each service class with the directory that contains it.
serviceClass.serviceFamily = servicePath
.replace(toUnixPath(serviceDir), '')
.split('/')[1]
.replace(serviceDir, '')
.split(path.sep)[1]
serviceClass.validateDefinition()
return serviceClasses.push(serviceClass)
}

View File

@@ -1,7 +1,3 @@
import { registerCommand } from 'cypress-wait-for-stable-dom'
registerCommand()
describe('Main page', function () {
const backendUrl = Cypress.env('backend_url')
const SEARCH_INPUT = 'input[placeholder="search / project URL"]'
@@ -13,13 +9,8 @@ describe('Main page', function () {
.should('have.attr', 'src', previewUrl)
}
function visitAndWait(page) {
cy.visit(page)
cy.waitForStableDOM({ pollInterval: 1000, timeout: 10000 })
}
it('Search for badges', function () {
visitAndWait('/')
cy.visit('/')
cy.get(SEARCH_INPUT).type('pypi')
@@ -27,7 +18,7 @@ describe('Main page', function () {
})
it('Shows badge from category', function () {
visitAndWait('/category/chat')
cy.visit('/category/chat')
expectBadgeExample(
'Discourse status',
@@ -38,7 +29,7 @@ describe('Main page', function () {
it('Suggest badges', function () {
const badgeUrl = `${backendUrl}/github/issues/badges/shields`
visitAndWait('/')
cy.visit('/')
cy.get(SEARCH_INPUT).type('https://github.com/badges/shields')
cy.contains('Suggest badges').click()
@@ -48,7 +39,7 @@ describe('Main page', function () {
it('Customization form is filled with suggested badge details', function () {
const badgeUrl = `${backendUrl}/github/issues/badges/shields`
visitAndWait('/')
cy.visit('/')
cy.get(SEARCH_INPUT).type('https://github.com/badges/shields')
cy.contains('Suggest badges').click()
@@ -60,7 +51,7 @@ describe('Main page', function () {
it('Customizate suggested badge', function () {
const badgeUrl = `${backendUrl}/github/issues/badges/shields`
visitAndWait('/')
cy.visit('/')
cy.get(SEARCH_INPUT).type('https://github.com/badges/shields')
cy.contains('Suggest badges').click()
cy.contains(badgeUrl).click()
@@ -71,7 +62,7 @@ describe('Main page', function () {
})
it('Do not duplicate example parameters', function () {
visitAndWait('/category/funding')
cy.visit('/category/funding')
cy.contains('GitHub Sponsors').click()
cy.get('[name="style"]').should($style => {

View File

@@ -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 to reach 100% coverage of the code in the
Our goal is for the core code 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 three different
delegates to a callback in `BaseService.register` with four different
parameters `( queryParams, match, sendBadge )`, which
then runs `BaseService.invoke`. `BaseService.invoke` instantiates the
service and runs `BaseService#handle`.

View File

@@ -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 ![](https://img.shields.io/wercker/build/wercker/go-wercker-api.svg) 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 built into IcedFrisby which you can use to
Joi is a validation library that is build 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"`.

View File

@@ -51,14 +51,14 @@ test(reStructuredText, () => {
'.. image:: https://img.shields.io/badge'
)
given('https://img.shields.io/badge', undefined, 'Example').expect(
'.. image:: https://img.shields.io/badge\n :alt: Example'
'.. image:: https://img.shields.io/badge :alt: Example'
)
given(
'https://img.shields.io/badge',
'https://example.com/example',
'Example'
).expect(
'.. image:: https://img.shields.io/badge\n :alt: Example\n :target: https://example.com/example'
'.. image:: https://img.shields.io/badge :alt: Example :target: https://example.com/example'
)
})

View File

@@ -33,10 +33,10 @@ export function reStructuredText(
): string {
let result = `.. image:: ${badgeUrl}`
if (title) {
result += `\n :alt: ${title}`
result += ` :alt: ${title}`
}
if (link) {
result += `\n :target: ${link}`
result += ` :target: ${link}`
}
return result
}

View File

@@ -33,7 +33,7 @@ describe('Logo helpers', function () {
test(prepareNamedLogo, () => {
// NPM uses multiple colors so the color param should be ignored
const npmLogo =
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZD0iTTAgMGg0MHY0MEgwVjB6IiBmaWxsPSIjY2IwMDAwIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTcgN2gyNnYyNmgtN1YxNGgtNnYxOUg3eiIvPjwvc3ZnPg=='
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZD0iTTAgMGg0MHY0MEgwVjB6IiBmaWxsPSIjY2IwMDAwIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTcgN2gyNnYyNmgtN1YxNGgtNnYxOUg3eiIvPjwvc3ZnPgo='
given({ name: 'npm' }).expect(npmLogo)
given({ name: 'npm', color: 'blue' }).expect(npmLogo)
@@ -115,7 +115,7 @@ describe('Logo helpers', function () {
undefined
)
given('npm', {}).expect(
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZD0iTTAgMGg0MHY0MEgwVjB6IiBmaWxsPSIjY2IwMDAwIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTcgN2gyNnYyNmgtN1YxNGgtNnYxOUg3eiIvPjwvc3ZnPg=='
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZD0iTTAgMGg0MHY0MEgwVjB6IiBmaWxsPSIjY2IwMDAwIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTcgN2gyNnYyNmgtN1YxNGgtNnYxOUg3eiIvPjwvc3ZnPgo='
)
})
})

View File

@@ -1,7 +1,5 @@
function svg2base64(svg) {
return `data:image/svg+xml;base64,${Buffer.from(svg.trim()).toString(
'base64'
)}`
return `data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}`
}
export { svg2base64 }

16711
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,10 +21,10 @@
"url": "https://github.com/badges/shields"
},
"dependencies": {
"@fontsource/lato": "^4.5.9",
"@fontsource/lekton": "^4.5.10",
"@fontsource/lato": "^4.5.8",
"@fontsource/lekton": "^4.5.9",
"@renovate/pep440": "^1.0.0",
"@sentry/node": "^7.12.1",
"@sentry/node": "^7.5.1",
"@shields_io/camp": "^18.1.1",
"badge-maker": "file:badge-maker",
"bytes": "^3.1.2",
@@ -34,17 +34,16 @@
"cloudflare-middleware": "^1.0.4",
"config": "^3.3.7",
"cross-env": "^7.0.3",
"dayjs": "^1.11.5",
"decamelize": "^3.2.0",
"emojic": "^1.1.17",
"escape-string-regexp": "^4.0.0",
"fast-xml-parser": "^4.0.9",
"fast-xml-parser": "^4.0.8",
"glob": "^8.0.3",
"global-agent": "^3.0.0",
"got": "^12.3.1",
"got": "^12.1.0",
"graphql": "^15.6.1",
"graphql-tag": "^2.12.6",
"ioredis": "5.2.3",
"ioredis": "5.1.0",
"joi": "17.6.0",
"joi-extension-semver": "5.0.0",
"js-yaml": "^4.1.0",
@@ -52,16 +51,17 @@
"lodash.countby": "^4.6.0",
"lodash.groupby": "^4.6.0",
"lodash.times": "^4.3.2",
"moment": "^2.29.4",
"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.1.0",
"prom-client": "^14.0.1",
"qs": "^6.11.0",
"query-string": "^7.1.1",
"semver": "~7.3.7",
"simple-icons": "7.9.0",
"simple-icons": "7.4.0",
"webextension-store-meta": "^1.0.5",
"xmldom": "~0.6.0",
"xpath": "~0.0.32"
@@ -141,12 +141,12 @@
]
},
"devDependencies": {
"@babel/core": "^7.18.13",
"@babel/core": "^7.18.6",
"@babel/polyfill": "^7.12.1",
"@babel/register": "7.18.9",
"@babel/register": "7.18.6",
"@istanbuljs/schema": "^0.1.3",
"@mapbox/react-click-to-select": "^2.2.1",
"@types/chai": "^4.3.3",
"@types/chai": "^4.3.1",
"@types/lodash.debounce": "^4.0.7",
"@types/lodash.groupby": "^4.6.7",
"@types/mocha": "^9.1.1",
@@ -154,12 +154,12 @@
"@types/react-helmet": "^6.1.5",
"@types/react-modal": "^3.13.1",
"@types/react-select": "^4.0.17",
"@types/styled-components": "5.1.26",
"@typescript-eslint/eslint-plugin": "^5.36.1",
"@typescript-eslint/parser": "^5.30.7",
"@types/styled-components": "5.1.25",
"@typescript-eslint/eslint-plugin": "^5.30.5",
"@typescript-eslint/parser": "^5.27.0",
"babel-plugin-inline-react-svg": "^2.0.1",
"babel-preset-gatsby": "^2.22.0",
"c8": "^7.12.0",
"babel-preset-gatsby": "^2.14.0",
"c8": "^7.11.3",
"caller": "^1.1.0",
"chai": "^4.3.6",
"chai-as-promised": "^7.1.1",
@@ -167,10 +167,9 @@
"chai-string": "^1.4.0",
"child-process-promise": "^2.2.1",
"clipboard-copy": "^4.0.1",
"concurrently": "^7.3.0",
"cypress": "^10.7.0",
"cypress-wait-for-stable-dom": "^0.0.3",
"danger": "^11.1.2",
"concurrently": "^7.2.2",
"cypress": "^10.3.0",
"danger": "^11.1.1",
"danger-plugin-no-test-shortcuts": "^2.0.0",
"deepmerge": "^4.2.2",
"eslint": "^7.32.0",
@@ -181,29 +180,29 @@
"eslint-plugin-chai-friendly": "^0.7.2",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsdoc": "^39.3.6",
"eslint-plugin-mocha": "^10.1.0",
"eslint-plugin-jsdoc": "^39.3.3",
"eslint-plugin-mocha": "^10.0.5",
"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.31.1",
"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.22.0",
"gatsby-plugin-catch-links": "^4.19.0",
"gatsby-plugin-page-creator": "^4.22.0",
"gatsby-plugin-react-helmet": "^5.22.0",
"gatsby": "4.6.2",
"gatsby-plugin-catch-links": "^4.11.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.19.0",
"gatsby-plugin-typescript": "^4.22.0",
"gatsby-plugin-styled-components": "^5.11.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.11",
"jsdoc": "^3.6.10",
"lint-staged": "^13.0.3",
"lodash.debounce": "^4.0.8",
"lodash.difference": "^4.5.0",
@@ -212,12 +211,12 @@
"mocha-env-reporter": "^4.0.0",
"mocha-junit-reporter": "^2.0.2",
"mocha-yaml-loader": "^1.0.3",
"nock": "13.2.9",
"nock": "13.2.8",
"node-mocks-http": "^1.11.0",
"nodemon": "^2.0.19",
"npm-run-all": "^4.1.5",
"open-cli": "^7.0.1",
"portfinder": "^1.0.32",
"portfinder": "^1.0.28",
"prettier": "2.7.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
@@ -237,8 +236,8 @@
"start-server-and-test": "1.14.0",
"styled-components": "^5.3.5",
"ts-mocha": "^10.0.0",
"tsd": "^0.23.0",
"typescript": "^4.8.2",
"tsd": "^0.22.0",
"typescript": "^4.7.4",
"url": "^0.11.0"
},
"engines": {

View File

@@ -1,36 +0,0 @@
import fs from 'fs'
let data
let title
try {
if (process.argv.length < 4) {
throw new Error()
}
title = process.argv[2]
data = JSON.parse(fs.readFileSync(process.argv[3]))
} catch (e) {
process.stdout.write('failed to write summary\n')
process.exit(1)
}
process.stdout.write(`# ${title}\n\n`)
if (data.stats.passes > 0) {
process.stdout.write(`${data.stats.passes} passed\n`)
}
if (data.stats.failures > 0) {
process.stdout.write(`${data.stats.failures} failed\n\n`)
}
if (data.stats.failures > 0) {
for (const test of data.tests) {
if (test.err && Object.keys(test.err).length > 0) {
process.stdout.write(`### ${test.title}\n\n`)
process.stdout.write(`${test.fullTitle}\n\n`)
process.stdout.write('```\n')
process.stdout.write(`${test.err.stack}\n`)
process.stdout.write('```\n\n')
}
}
}

View File

@@ -1,8 +1,9 @@
import Joi from 'joi'
import { nonNegativeInteger } from '../validators.js'
import { metric } from '../text-formatters.js'
import { BaseJsonService } from '../index.js'
const schema = Joi.object({ activity_total: Joi.number().required() })
const schema = Joi.object({ activity_total: nonNegativeInteger })
export default class Bountysource extends BaseJsonService {
static category = 'funding'

View File

@@ -5,7 +5,7 @@
* @module
*/
import dayjs from 'dayjs'
import moment from 'moment'
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 = dayjs().diff(dayjs(date), 'days')
const daysElapsed = moment().diff(moment(date), 'days')
return colorByAge(daysElapsed)
}

View File

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

View File

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

View File

@@ -1,36 +1,71 @@
const sizeDataNoTagSemVerSort = [
{
full_size: 300000000,
name: 'v4.0.0-alpha2',
images: [
{ architecture: 'amd64', size: 219939484 },
{ architecture: 'arm64', size: 200000000 },
],
},
{
full_size: 400000000,
name: 'v4.2.4',
images: [
{ architecture: 'amd64', size: 220000000 },
{ architecture: 'arm64', size: 210000000 },
],
},
{
full_size: 100000000,
name: 'v3.9.7',
images: [
{ architecture: 'amd64', size: 120000000 },
{ architecture: 'arm64', size: 110000000 },
],
},
{
full_size: 500000000,
name: 'latest',
images: [
{ architecture: 'amd64', size: 560000000 },
{ architecture: 'arm64', size: 460000000 },
],
},
{ name: 'master', full_size: 13449470 },
{ name: 'feature-smtps-support', full_size: 13449638 },
{ name: 'latest', full_size: 13448411 },
{ name: '4', full_size: 13448411 },
{ name: '4.3', full_size: 13448411 },
{ name: '4.3.0', full_size: 13448411 },
{ name: '4.2', full_size: 13443674 },
{ name: '4.2.0', full_size: 13443674 },
{ name: '4.1', full_size: 19244435 },
{ name: '4.1.0', full_size: 19244435 },
{ name: 'v4.0.0-alpha2', full_size: 10933605 },
{ name: 'v4.0.0-alpha1', full_size: 10933644 },
{ name: '4.0.0', full_size: 11512227 },
{ name: '4.0', full_size: 11512227 },
{ name: 'v2.1.9', full_size: 29739490 },
{ name: 'v2.1.10', full_size: 29739842 },
{ name: 'v3.0.0', full_size: 32882980 },
{ name: 'v3.0.1', full_size: 32880923 },
{ name: 'v3.1.0', full_size: 32441549 },
{ name: 'v3.1.1', full_size: 32441767 },
{ name: 'v3.1.2', full_size: 32442741 },
{ name: 'v3.1.3', full_size: 32442629 },
{ name: 'v3.1.4', full_size: 32478607 },
{ name: 'v3.2.0', full_size: 33489914 },
{ name: 'v3.3.0', full_size: 33628545 },
{ name: 'v3.3.1', full_size: 33629018 },
{ name: 'v3.3.3', full_size: 33628988 },
{ name: 'v3.3.4', full_size: 33629019 },
{ name: 'v3.3.6', full_size: 33628753 },
{ name: 'v3.3.7', full_size: 33629556 },
{ name: 'v3.3.8', full_size: 33644261 },
{ name: 'v3.3.9', full_size: 33644175 },
{ name: 'v3.3.10', full_size: 33644406 },
{ name: 'v3.3.11', full_size: 33644430 },
{ name: 'v3.3.12', full_size: 33644703 },
{ name: 'v3.3.13', full_size: 33644377 },
{ name: 'v3.3.15', full_size: 33644581 },
{ name: 'v3.3.16', full_size: 33644663 },
{ name: 'v3.3.17', full_size: 33644228 },
{ name: 'v3.3.18', full_size: 33644466 },
{ name: 'v3.3.19', full_size: 33644724 },
{ name: 'v3.4.0', full_size: 34918552 },
{ name: 'v3.4.2', full_size: 33605129 },
{ name: 'v3.5.0', full_size: 33582915 },
{ name: 'v3.6.0', full_size: 34789944 },
{ name: 'develop', full_size: 38129308 },
{ name: 'v3.7.0', full_size: 38179583 },
{ name: 'v3.7.1', full_size: 38614944 },
{ name: 'v3.8.0', full_size: 42962384 },
{ name: 'v3.8.1', full_size: 40000713 },
{ name: 'v3.8.2', full_size: 40000567 },
{ name: 'v3.8.3', full_size: 40040963 },
{ name: 'v3.9.0', full_size: 40044357 },
{ name: 'v3.9.1', full_size: 40048123 },
{ name: 'v3.9.2', full_size: 40047663 },
{ name: 'v3.9.3', full_size: 40048204 },
{ name: 'v3.9.4', full_size: 40049571 },
{ name: 'v3.9.5', full_size: 40049695 },
{ name: 'v3.10.0', full_size: 39940736 },
{ name: 'v3.11.0', full_size: 39928170 },
{ name: 'v3.12.0', full_size: 39966770 },
{ name: 'v3.13.0', full_size: 38556045 },
{ name: 'v3.14.0', full_size: 38574008 },
{ name: 'v3.15.0', full_size: 38578507 },
{ name: 'v3.16.0', full_size: 38852598 },
{ name: 'v3.16.1', full_size: 38851702 },
{ name: 'v3.16.2', full_size: 38969822 },
]
const versionDataNoTagDateSort = {
count: 4,

View File

@@ -1,28 +1,7 @@
import Joi from 'joi'
// see https://github.com/badges/shields/pull/1690
import { NotFound } from '../index.js'
const dockerBlue = '066da5'
// Valid architecture values: https://golang.org/doc/install/source#environment (GOARCH)
const archSchema = Joi.alternatives(
Joi.string().valid(
'amd64',
'arm',
'arm64',
's390x',
'386',
'ppc64',
'ppc64le',
'wasm',
'mips',
'mipsle',
'mips64',
'mips64le',
'riscv64'
),
Joi.number().valid(386).cast('string')
)
function buildDockerUrl(badgeName, includeTagRoute) {
if (includeTagRoute) {
return {
@@ -76,7 +55,6 @@ function getDigestSemVerMatches({ data, digest }) {
}
export {
archSchema,
dockerBlue,
buildDockerUrl,
getDockerHubUser,

View File

@@ -4,7 +4,6 @@ import { nonNegativeInteger } from '../validators.js'
import { latest } from '../version.js'
import { BaseJsonService, NotFound } from '../index.js'
import {
archSchema,
buildDockerUrl,
getDockerHubUser,
getMultiPageData,
@@ -13,12 +12,6 @@ import {
const buildSchema = Joi.object({
name: Joi.string().required(),
full_size: nonNegativeInteger.required(),
images: Joi.array().items(
Joi.object({
size: nonNegativeInteger.required(),
architecture: Joi.string().required(),
})
),
}).required()
const pagedSchema = Joi.object({
@@ -27,37 +20,14 @@ const pagedSchema = Joi.object({
Joi.object({
name: Joi.string().required(),
full_size: nonNegativeInteger.required(),
images: Joi.array().items(
Joi.object({
size: nonNegativeInteger.required(),
architecture: Joi.string().required(),
})
),
})
),
}).required()
const queryParamSchema = Joi.object({
sort: Joi.string().valid('date', 'semver').default('date'),
arch: archSchema,
}).required()
// If user provided the arch parameter,
// check if any of the returned images has an architecture matching the arch parameter provided.
// If yes, return the size of the image with this arch.
// If not, throw the `NotFound` error.
// For details see: https://github.com/badges/shields/issues/8238
function getImageSizeForArch(images, arch) {
const imgWithArch = Object.values(images).find(
img => img.architecture === arch
)
if (!imgWithArch) {
throw new NotFound({ prettyMessage: 'architecture not found' })
}
return imgWithArch.size
}
export default class DockerSize extends BaseJsonService {
static category = 'size'
static route = { ...buildDockerUrl('image-size', true), queryParamSchema }
@@ -76,14 +46,6 @@ export default class DockerSize extends BaseJsonService {
queryParams: { sort: 'semver' },
staticPreview: this.render({ size: 136000000 }),
},
{
title:
'Docker Image Size with architecture (latest by date/latest semver)',
pattern: ':user/:repo',
namedParams: { user: 'library', repo: 'mysql' },
queryParams: { sort: 'date', arch: 'amd64' },
staticPreview: this.render({ size: 146000000 }),
},
{
title: 'Docker Image Size (tag)',
pattern: ':user/:repo/:tag',
@@ -111,83 +73,30 @@ export default class DockerSize extends BaseJsonService {
})
}
getSizeFromImageByLatestDate(data, arch) {
if (data.count === 0) {
throw new NotFound({ prettyMessage: 'repository not found' })
} else {
const latestEntry = data.results[0]
if (arch) {
return { size: getImageSizeForArch(latestEntry.images, arch) }
transform({ tag, sort, data }) {
if (!tag && sort === 'date') {
if (data.count === 0) {
throw new NotFound({ prettyMessage: 'repository not found' })
} else {
return { size: latestEntry.full_size }
return { size: data.results[0].full_size }
}
}
}
getSizeFromImageByLatestSemver(data, arch) {
// If no tag is specified, and sorting is by semver, first filter out the entry containing the latest semver from the response with Docker images.
// If no architecture is supplied by the user, return `full_size` from this entry.
// If the architecture is supplied by the user, check if any of the returned images for this entry has an architecture matching the arch parameter supplied by the user.
// If yes, return the size of the image with this arch.
// If not, throw the `NotFound` error.
const [matches, versions, images] = data.reduce(
([m, v, i], d) => {
m[d.name] = d.full_size
v.push(d.name)
i[d.name] = d.images
return [m, v, i]
},
[{}, [], {}]
)
const version = latest(versions)
let sizeOfImgWithArch
if (arch) {
Object.keys(images).forEach(ver => {
if (ver === version) {
sizeOfImgWithArch = getImageSizeForArch(images[ver], arch)
return { size: sizeOfImgWithArch }
}
})
if (sizeOfImgWithArch) {
return { size: sizeOfImgWithArch }
} else {
throw new NotFound({ prettyMessage: 'architecture not found' })
}
} else {
} else if (!tag && sort === 'semver') {
const [matches, versions] = data.reduce(
([m, v], d) => {
m[d.name] = d.full_size
v.push(d.name)
return [m, v]
},
[{}, []]
)
const version = latest(versions)
return { size: matches[version] }
}
}
getSizeFromTag(data, arch) {
// If the tag is specified, and the architecture is supplied by the user,
// check if any of the returned images has an architecture matching the arch parameter supplied by the user.
// If yes, return the size of the image with this arch.
// If no, throw the `NotFound` error.
// If no architecture is supplied by the user, return the value of the `full_size` from the response (the image with the `latest` tag).
if (arch) {
return { size: getImageSizeForArch(data.images, arch) }
} else {
return { size: data.full_size }
}
}
transform({ tag, sort, data, arch }) {
if (!tag && sort === 'date') {
return this.getSizeFromImageByLatestDate(data, arch)
} else if (!tag && sort === 'semver') {
return this.getSizeFromImageByLatestSemver(data, arch)
} else {
return this.getSizeFromTag(data, arch)
}
}
async handle({ user, repo, tag }, { sort, arch }) {
async handle({ user, repo, tag }, { sort }) {
let data
if (!tag && sort === 'date') {
@@ -202,7 +111,7 @@ export default class DockerSize extends BaseJsonService {
data = await this.fetch({ user, repo, tag })
}
const { size } = await this.transform({ tag, sort, data, arch })
const { size } = await this.transform({ tag, sort, data })
return this.constructor.render({ size })
}
}

View File

@@ -3,99 +3,40 @@ import DockerSize from './docker-size.service.js'
import { sizeDataNoTagSemVerSort } from './docker-fixtures.js'
describe('DockerSize', function () {
test(DockerSize.prototype.getSizeFromImageByLatestDate, () => {
given(
{
count: 0,
results: [],
},
'amd64'
).expectError('Not Found: repository not found')
given(
{
count: 1,
results: [
{
full_size: 300000000,
name: 'next',
images: [{ architecture: 'amd64', size: 219939484 }],
},
],
},
'amd64'
).expect({
test(DockerSize.prototype.transform, () => {
given({
tag: '',
sort: 'date',
data: { results: [{ name: 'next', full_size: 219939484 }] },
}).expect({
size: 219939484,
})
given({
count: 1,
results: [
{
full_size: 300000000,
name: 'next',
images: [
{ architecture: 'amd64', size: 219939484 },
{ architecture: 'arm64', size: 200000000 },
],
},
],
}).expect({
size: 300000000,
})
given(
{
count: 1,
tag: '',
sort: 'date',
data: {
results: [
{
full_size: 300000000,
name: 'next',
images: [
{ architecture: 'amd64', size: 219939484 },
{ architecture: 'arm64', size: 200000000 },
],
},
{ name: 'latest', full_size: 74661264 },
{ name: 'arm64v8-latest', full_size: 76310416 },
{ name: 'arm32v7-latest', full_size: 68001970 },
{ name: 'amd64-latest', full_size: 74661264 },
],
},
'arm64777'
).expectError('Not Found: architecture not found')
})
test(DockerSize.prototype.getSizeFromTag, () => {
given(
{
full_size: 300000000,
name: 'next',
images: [{ architecture: 'amd64', size: 219939484 }],
},
'amd64'
).expect({
size: 219939484,
}).expect({
size: 74661264,
})
given({
full_size: 300000000,
name: 'next',
images: [{ architecture: 'amd64', size: 219939484 }],
tag: '',
sort: 'semver',
data: sizeDataNoTagSemVerSort,
}).expect({
size: 300000000,
size: 13448411,
})
given(
{
full_size: 300000000,
name: 'next',
images: [{ architecture: 'amd64', size: 219939484 }],
},
'arm64777'
).expectError('Not Found: architecture not found')
})
test(DockerSize.prototype.getSizeFromImageByLatestSemver, () => {
given(sizeDataNoTagSemVerSort, 'amd64').expect({
size: 220000000,
given({
tag: 'latest',
data: { name: 'latest', full_size: 13448411 },
}).expect({
size: 13448411,
})
given(sizeDataNoTagSemVerSort).expect({
size: 400000000,
})
given(sizeDataNoTagSemVerSort, 'nonexistentArch').expectError(
'Not Found: architecture not found'
)
})
})

View File

@@ -9,13 +9,6 @@ t.create('docker image size (valid, library)')
message: isFileSize,
})
t.create('docker image size (valid, library, arch parameter )')
.get('/_/mysql.json?arch=amd64')
.expectBadge({
label: 'image size',
message: isFileSize,
})
t.create('docker image size (valid, library with tag)')
.get('/_/alpine/latest.json')
.expectBadge({
@@ -48,19 +41,5 @@ t.create('docker image size (invalid, unknown repository)')
.get('/_/not-a-real-repo.json')
.expectBadge({
label: 'image size',
message: 'repository or tag not found',
})
t.create('docker image size (invalid, wrong sorting method)')
.get('/jrottenberg/ffmpeg/3.2-alpine.json?sort=daterrr')
.expectBadge({
label: 'image size',
message: 'invalid query parameter: sort',
})
t.create('docker image size (invalid, nonexisting arch)')
.get('/jrottenberg/ffmpeg/3.2-alpine.json?arch=nonexistingArch')
.expectBadge({
label: 'image size',
message: 'invalid query parameter: arch',
message: 'repository not found',
})

View File

@@ -1,18 +1,13 @@
import Joi from 'joi'
import { metric } from '../text-formatters.js'
import { nonNegativeInteger } from '../validators.js'
import { BaseJsonService } from '../index.js'
import { BaseService } from '../index.js'
import {
dockerBlue,
buildDockerUrl,
getDockerHubUser,
} from './docker-helpers.js'
const schema = Joi.object({
star_count: nonNegativeInteger.required(),
}).required()
export default class DockerStars extends BaseJsonService {
export default class DockerStars extends BaseService {
static category = 'rating'
static route = buildDockerUrl('stars')
static examples = [
@@ -36,17 +31,18 @@ export default class DockerStars extends BaseJsonService {
}
async fetch({ user, repo }) {
return this._requestJson({
schema,
url: `https://hub.docker.com/v2/repositories/${getDockerHubUser(
user
)}/${repo}/`,
const url = `https://hub.docker.com/v2/repositories/${getDockerHubUser(
user
)}/${repo}/stars/count/`
const { buffer } = await this._request({
url,
errorMessages: { 404: 'repo not found' },
})
return this.constructor._validate(buffer, nonNegativeInteger)
}
async handle({ user, repo }) {
const { star_count } = await this.fetch({ user, repo })
return this.constructor.render({ stars: star_count })
const stars = await this.fetch({ user, repo })
return this.constructor.render({ stars })
}
}

View File

@@ -3,7 +3,6 @@ import { nonNegativeInteger } from '../validators.js'
import { latest, renderVersionBadge } from '../version.js'
import { BaseJsonService, NotFound, InvalidResponse } from '../index.js'
import {
archSchema,
buildDockerUrl,
getDockerHubUser,
getMultiPageData,
@@ -27,7 +26,23 @@ const buildSchema = Joi.object({
const queryParamSchema = Joi.object({
sort: Joi.string().valid('date', 'semver').default('date'),
arch: archSchema.default('amd64'),
arch: Joi.string()
// Valid architecture values: https://golang.org/doc/install/source#environment (GOARCH)
.valid(
'amd64',
'arm',
'arm64',
's390x',
'386',
'ppc64',
'ppc64le',
'wasm',
'mips',
'mipsle',
'mips64',
'mips64le'
)
.default('amd64'),
}).required()
export default class DockerVersion extends BaseJsonService {

View File

@@ -1,41 +0,0 @@
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 })
}
}

View File

@@ -1,23 +0,0 @@
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',
})

View File

@@ -9,7 +9,6 @@ const orderedInstallableRevisionsSchema = Joi.array()
const repositoryRevisionInstallInfoSchema = Joi.array()
.ordered(
Joi.object({
create_time: Joi.date().required(),
times_downloaded: nonNegativeInteger,
}).required()
)

View File

@@ -1,47 +0,0 @@
import Joi from 'joi'
import { formatDate } from '../text-formatters.js'
import { age as ageColor } from '../color-formatters.js'
import { GithubAuthV3Service } from './github-auth-service.js'
import { documentation, errorMessagesFor } from './github-helpers.js'
const schema = Joi.object({
updated_at: Joi.string().required(),
}).required()
export default class GithubGistLastCommit extends GithubAuthV3Service {
static category = 'activity'
static route = { base: 'github-gist/last-commit', pattern: ':gistId' }
static examples = [
{
title: 'GitHub Gist last commit',
namedParams: {
gistId: '8710649',
},
staticPreview: this.render({ commitDate: '2022-07-29T20:01:41Z' }),
keywords: ['latest'],
documentation,
},
]
static defaultBadgeData = { label: 'last commit' }
static render({ commitDate }) {
return {
message: formatDate(commitDate),
color: ageColor(Date.parse(commitDate)),
}
}
async fetch({ gistId }) {
return this._requestJson({
url: `/gists/${gistId}`,
schema,
errorMessages: errorMessagesFor('gist not found'),
})
}
async handle({ gistId }) {
const { updated_at: commitDate } = await this.fetch({ gistId })
return this.constructor.render({ commitDate })
}
}

View File

@@ -1,24 +0,0 @@
import { createServiceTester } from '../tester.js'
export const t = await createServiceTester()
t.create('last commit in gist (ancient)').get('/871064.json').expectBadge({
label: 'last commit',
message: 'september 2015',
color: 'red',
})
// not checking the color badge, since in August 2022 it is orange but later it will become red
t.create('last commit in gist (still ancient but slightly less so)')
.get('/870071abadfd66a28bf539677332f12b.json')
.expectBadge({
label: 'last commit',
message: 'october 2020',
})
t.create('last commit in gist (gist not found)')
.get('/55555555555555.json')
.expectBadge({
label: 'last commit',
message: 'gist not found',
color: 'red',
})

View File

@@ -1,6 +1,6 @@
import gql from 'graphql-tag'
import Joi from 'joi'
import dayjs from 'dayjs'
import moment from 'moment'
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 = dayjs(`${year}-11-01 12:00:00 Z`).diff(dayjs(), 'days') + 1
daysLeft = moment(`${year}-11-01 12:00:00 Z`).diff(moment(), 'days') + 1
}
if (daysLeft < 0) {
return {
@@ -205,7 +205,10 @@ export default class GithubHacktoberfestCombinedStatus extends GithubAuthV4Servi
}
static getCalendarPosition(year) {
const daysToStart = dayjs(`${year}-10-01 00:00:00 Z`).diff(dayjs(), 'days')
const daysToStart = moment(`${year}-10-01 00:00:00 Z`).diff(
moment(),
'days'
)
const isBefore = daysToStart > 0
return { daysToStart, isBefore }
}

View File

@@ -1,4 +1,4 @@
import dayjs from 'dayjs'
import moment from 'moment'
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 = dayjs(date)
const releaseDate = moment(date)
return {
message: formatDate(releaseDate),
color: age(releaseDate),

View File

@@ -5,10 +5,6 @@ import { NotFound } from '../index.js'
import { GithubAuthV3Service } from './github-auth-service.js'
import { documentation, errorMessagesFor } from './github-helpers.js'
const queryParamSchema = Joi.object({
branch: Joi.string(),
}).required()
const schema = Joi.alternatives(
Joi.object({
size: nonNegativeInteger,
@@ -22,7 +18,6 @@ export default class GithubSize extends GithubAuthV3Service {
static route = {
base: 'github/size',
pattern: ':user/:repo/:path*',
queryParamSchema,
}
static examples = [
@@ -37,20 +32,6 @@ export default class GithubSize extends GithubAuthV3Service {
keywords: ['repo'],
documentation,
},
{
title: 'GitHub file size in bytes on a specified ref (branch/commit/tag)',
namedParams: {
user: 'webcaetano',
repo: 'craft',
path: 'build/phaser-craft.min.js',
},
staticPreview: this.render({ size: 9170 }),
keywords: ['repo'],
documentation,
queryParams: {
branch: 'master',
},
},
]
static render({ size }) {
@@ -60,25 +41,16 @@ export default class GithubSize extends GithubAuthV3Service {
}
}
async fetch({ user, repo, path, branch }) {
if (branch) {
return this._requestJson({
url: `/repos/${user}/${repo}/contents/${path}?ref=${branch}`,
schema,
errorMessages: errorMessagesFor('repo, branch or file not found'),
})
} else {
return this._requestJson({
url: `/repos/${user}/${repo}/contents/${path}`,
schema,
errorMessages: errorMessagesFor('repo or file not found'),
})
}
async fetch({ user, repo, path }) {
return this._requestJson({
url: `/repos/${user}/${repo}/contents/${path}`,
schema,
errorMessages: errorMessagesFor('repo or file not found'),
})
}
async handle({ user, repo, path }, queryParams) {
const branch = queryParams.branch
const body = await this.fetch({ user, repo, path, branch })
async handle({ user, repo, path }) {
const body = await this.fetch({ user, repo, path })
if (Array.isArray(body)) {
throw new NotFound({ prettyMessage: 'not a regular file' })
}

View File

@@ -10,22 +10,6 @@ t.create('File size 404')
.get('/webcaetano/craft/build/does-not-exist.min.js.json')
.expectBadge({ label: 'size', message: 'repo or file not found' })
t.create('File size for nonexisting branch')
.get('/webcaetano/craft/build/phaser-craft.min.js.json?branch=notARealBranch')
.expectBadge({ label: 'size', message: 'repo, branch or file not found' })
t.create('File size for "not a regular file"')
.get('/webcaetano/craft/build.json')
.expectBadge({ label: 'size', message: 'not a regular file' })
t.create('File size for a specified branch')
.get('/webcaetano/craft/build/craft.min.js.json?branch=version-2')
.expectBadge({ label: 'size', message: isFileSize })
t.create('File size for a specified tag')
.get('/webcaetano/craft/build/phaser-craft.min.js.json?branch=2.1.2')
.expectBadge({ label: 'size', message: isFileSize })
t.create('File size for a specified commit')
.get('/webcaetano/craft/build/phaser-craft.min.js.json?branch=b848dbb')
.expectBadge({ label: 'size', message: isFileSize })

View File

@@ -1,77 +0,0 @@
import Joi from 'joi'
import { optionalUrl, nonNegativeInteger } from '../validators.js'
import { metric } from '../text-formatters.js'
import GitLabBase from './gitlab-base.js'
const schema = Joi.object({
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 })
}
}

View File

@@ -1,35 +0,0 @@
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',
})

View File

@@ -1,351 +0,0 @@
import Joi from 'joi'
import { optionalUrl, nonNegativeInteger } from '../validators.js'
import { metric } from '../text-formatters.js'
import GitLabBase from './gitlab-base.js'
// The total number of MR is in the `x-total` field in the headers.
// https://docs.gitlab.com/ee/api/index.html#other-pagination-headers
const schema = Joi.object({
'x-total': Joi.number().integer(),
'x-page': nonNegativeInteger,
})
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/.
<a href="https://docs.gitlab.com/ee/user/gitlab_com/index.html#pagination-response-headers">GitLab's API </a> only reports up to 10k Merge Requests, so badges for projects that have more than 10k will not have an exact count.
</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 GitlabMergeRequests extends GitLabBase {
static category = 'issue-tracking'
static route = {
base: 'gitlab/merge-requests',
pattern: ':variant(all|open|closed|locked|merged):raw(-raw)?/:project+',
queryParamSchema,
}
static examples = [
{
title: 'GitLab merge requests',
pattern: 'open/:project+',
namedParams: {
project: 'gitlab-org/gitlab',
},
queryParams: { gitlab_url: 'https://gitlab.com' },
staticPreview: {
label: 'merge requests',
message: '1.4k open',
color: 'blue',
},
documentation,
},
{
title: 'GitLab merge requests',
pattern: 'open-raw/:project+',
namedParams: {
project: 'gitlab-org/gitlab',
},
queryParams: { gitlab_url: 'https://gitlab.com' },
staticPreview: {
label: 'open merge requests',
message: '1.4k',
color: 'blue',
},
documentation,
},
{
title: 'GitLab merge requests by-label',
pattern: 'open/:project+',
namedParams: {
project: 'gitlab-org/gitlab',
},
queryParams: {
labels: 'test,type::feature',
gitlab_url: 'https://gitlab.com',
},
staticPreview: {
label: 'test,failure::new merge requests',
message: '3 open',
color: 'blue',
},
documentation: documentation + labelDocumentation,
},
{
title: 'GitLab merge requests by-label',
pattern: 'open-raw/:project+',
namedParams: {
project: 'gitlab-org/gitlab',
},
queryParams: {
labels: 'gitlab-org/gitlab',
gitlab_url: 'https://gitlab.com',
},
staticPreview: {
label: 'open test,failure::new merge requests',
message: '3',
color: 'blue',
},
documentation: documentation + labelDocumentation,
},
{
title: 'GitLab closed merge requests',
pattern: 'closed/:project+',
namedParams: {
project: 'gitlab-org/gitlab',
},
queryParams: { gitlab_url: 'https://gitlab.com' },
staticPreview: {
label: 'merge requests',
message: 'more than 10k closed',
color: 'blue',
},
documentation,
},
{
title: 'GitLab closed merge requests',
pattern: 'closed-raw/:project+',
namedParams: {
project: 'gitlab-org/gitlab',
},
queryParams: { gitlab_url: 'https://gitlab.com' },
staticPreview: {
label: 'closed merge requests',
message: 'more than 10k',
color: 'blue',
},
documentation,
},
{
title: 'GitLab closed merge requests by-label',
pattern: 'closed/:project+',
namedParams: {
project: 'gitlab-org/gitlab',
},
queryParams: {
labels: 'test,type::feature',
gitlab_url: 'https://gitlab.com',
},
staticPreview: {
label: 'test,failure::new merge requests',
message: '32 closed',
color: 'blue',
},
documentation: documentation + labelDocumentation,
},
{
title: 'GitLab closed merge requests by-label',
pattern: 'closed-raw/:project+',
namedParams: {
project: 'gitlab-org/gitlab',
},
queryParams: {
labels: 'test,type::feature',
gitlab_url: 'https://gitlab.com',
},
staticPreview: {
label: 'closed test,failure::new merge requests',
message: '32',
color: 'blue',
},
documentation: documentation + labelDocumentation,
},
{
title: 'GitLab all merge requests',
pattern: 'all/:project+',
namedParams: {
project: 'gitlab-org/gitlab',
},
queryParams: { gitlab_url: 'https://gitlab.com' },
staticPreview: {
label: 'merge requests',
message: 'more than 10k all',
color: 'blue',
},
documentation,
},
{
title: 'GitLab all merge requests',
pattern: 'all-raw/:project+',
namedParams: {
project: 'gitlab-org/gitlab',
},
queryParams: { gitlab_url: 'https://gitlab.com' },
staticPreview: {
label: 'all merge requests',
message: 'more than 10k',
color: 'blue',
},
documentation,
},
{
title: 'GitLab all merge requests 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 merge requests',
message: '12',
color: 'blue',
},
documentation: documentation + labelDocumentation,
},
{
title: 'GitLab locked merge requests',
pattern: 'locked/:project+',
namedParams: {
project: 'gitlab-org/gitlab',
},
queryParams: { gitlab_url: 'https://gitlab.com' },
staticPreview: {
label: 'merge requests',
message: '0 locked',
color: 'blue',
},
documentation,
},
{
title: 'GitLab locked merge requests by-label',
pattern: 'closed/:project+',
namedParams: {
project: 'gitlab-org/gitlab',
},
queryParams: {
labels: 'test,type::feature',
gitlab_url: 'https://gitlab.com',
},
staticPreview: {
label: 'test,failure::new merge requests',
message: '0 locked',
color: 'blue',
},
documentation: documentation + labelDocumentation,
},
{
title: 'GitLab merged merge requests',
pattern: 'merged/:project+',
namedParams: {
project: 'gitlab-org/gitlab',
},
queryParams: { gitlab_url: 'https://gitlab.com' },
staticPreview: {
label: 'merge requests',
message: 'more than 10k merged',
color: 'blue',
},
documentation,
},
{
title: 'GitLab merged merge requests by-label',
pattern: 'merged/:project+',
namedParams: {
project: 'gitlab-org/gitlab',
},
queryParams: {
labels: 'test,type::feature',
gitlab_url: 'https://gitlab.com',
},
staticPreview: {
label: 'test,failure::new merge requests',
message: '68 merged',
color: 'blue',
},
documentation: documentation + labelDocumentation,
},
]
static defaultBadgeData = { label: 'merge requests' }
static render({ variant, raw, labels, mergeRequestCount }) {
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
}
const message = `${mergeRequestCount > 10000 ? 'more than ' : ''}${metric(
mergeRequestCount
)}${messageSuffix ? ' ' : ''}${messageSuffix}`
return {
label: `${labelPrefix}${labelText}merge requests`,
message,
color: 'blue',
}
}
async fetch({ project, baseUrl, variant, labels }) {
// https://docs.gitlab.com/ee/api/merge_requests.html#list-project-merge-requests
const { res } = await this._request(
this.authHelper.withBearerAuthHeader({
url: `${baseUrl}/api/v4/projects/${encodeURIComponent(
project
)}/merge_requests`,
options: {
searchParams: {
state: variant === 'open' ? 'opened' : variant,
page: '1',
per_page: '1',
labels,
},
},
errorMessages: {
404: 'project not found',
},
})
)
return this.constructor._validate(res.headers, schema)
}
static transform(data) {
if (data['x-total'] !== undefined) {
return data['x-total']
} else {
// https://docs.gitlab.com/ee/api/index.html#pagination-response-headers
// For performance reasons, if a query returns more than 10,000 records, GitLab doesnt return `x-total` header.
// Displayed on the page as "more than 10k".
return 10001
}
}
async handle(
{ variant, raw, project },
{ gitlab_url: baseUrl = 'https://gitlab.com', labels }
) {
const data = await this.fetch({
project,
baseUrl,
variant,
labels,
})
const mergeRequestCount = this.constructor.transform(data)
return this.constructor.render({
variant,
raw,
labels,
mergeRequestCount,
})
}
}

View File

@@ -1,92 +0,0 @@
import { test, given } from 'sazerac'
import nock from 'nock'
import { expect } from 'chai'
import { cleanUpNockAfterEach, defaultContext } from '../test-helpers.js'
import GitlabMergeRequests from './gitlab-merge-requests.service.js'
describe('GitlabMergeRequests', function () {
test(GitlabMergeRequests.render, () => {
given({ variant: 'open', mergeRequestCount: 1399 }).expect({
label: 'merge requests',
message: '1.4k open',
color: 'blue',
})
given({ variant: 'open', raw: '-raw', mergeRequestCount: 1399 }).expect({
label: 'open merge requests',
message: '1.4k',
color: 'blue',
})
given({
variant: 'open',
labels: 'discussion,enhancement',
mergeRequestCount: 15,
}).expect({
label: 'discussion,enhancement merge requests',
message: '15 open',
color: 'blue',
})
given({
variant: 'open',
raw: '-raw',
labels: 'discussion,enhancement',
mergeRequestCount: 15,
}).expect({
label: 'open discussion,enhancement merge requests',
message: '15',
color: 'blue',
})
given({ variant: 'open', mergeRequestCount: 0 }).expect({
label: 'merge requests',
message: '0 open',
color: 'blue',
})
given({ variant: 'open', mergeRequestCount: 10001 }).expect({
label: 'merge requests',
message: 'more than 10k open',
color: 'blue',
})
})
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/merge_requests?state=opened&page=1&per_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, {}, { 'x-total': '100', 'x-page': '1' })
expect(
await GitlabMergeRequests.invoke(
defaultContext,
config,
{ project: 'foo/bar', variant: 'open' },
{}
)
).to.deep.equal({
label: 'merge requests',
message: '100 open',
color: 'blue',
})
scope.done()
})
})
})

View File

@@ -1,172 +0,0 @@
import Joi from 'joi'
import { createServiceTester } from '../tester.js'
import {
isMetric,
isMetricOpenIssues,
isMetricClosedIssues,
} from '../test-validators.js'
export const t = await createServiceTester()
t.create('Merge Requests (project not found)')
.get('/open/guoxudong.io/shields-test/do-not-exist.json')
.expectBadge({
label: 'merge requests',
message: 'project not found',
})
/**
* Opened issue number case
*/
t.create('Opened merge requests')
.get('/open/guoxudong.io/shields-test/issue-test.json')
.expectBadge({
label: 'merge requests',
message: isMetricOpenIssues,
})
t.create('Open merge requests raw')
.get('/open-raw/guoxudong.io/shields-test/issue-test.json')
.expectBadge({
label: 'open merge requests',
message: isMetric,
})
t.create('Open merge requests by label is > zero')
.get('/open/guoxudong.io/shields-test/issue-test.json?labels=discussion')
.expectBadge({
label: 'discussion merge requests',
message: isMetricOpenIssues,
})
t.create('Open merge requests by multi-word label is > zero')
.get(
'/open/guoxudong.io/shields-test/issue-test.json?labels=discussion,enhancement'
)
.expectBadge({
label: 'discussion,enhancement merge requests',
message: isMetricOpenIssues,
})
t.create('Open merge requests by label (raw)')
.get('/open-raw/guoxudong.io/shields-test/issue-test.json?labels=discussion')
.expectBadge({
label: 'open discussion merge requests',
message: isMetric,
})
t.create('Opened merge requests by Scoped labels')
.get('/open/gitlab-org%2Fgitlab.json?labels=test,failure::new')
.expectBadge({
label: 'test,failure::new merge requests',
message: Joi.alternatives(isMetricOpenIssues, Joi.equal('0 open')),
})
/**
* Closed issue number case
*/
t.create('Closed merge requests')
.get('/closed/guoxudong.io/shields-test/issue-test.json')
.expectBadge({
label: 'merge requests',
message: isMetricClosedIssues,
})
t.create('Closed merge requests raw')
.get('/closed-raw/guoxudong.io/shields-test/issue-test.json')
.expectBadge({
label: 'closed merge requests',
message: isMetric,
})
t.create('Closed merge requests by label is > zero')
.get('/closed/guoxudong.io/shields-test/issue-test.json?labels=bug')
.expectBadge({
label: 'bug merge requests',
message: Joi.alternatives(isMetricClosedIssues, Joi.equal('0 closed')),
})
t.create('Closed merge requests by multi-word label is > zero')
.get('/closed/guoxudong.io/shields-test/issue-test.json?labels=bug,critical')
.expectBadge({
label: 'bug,critical merge requests',
message: Joi.alternatives(isMetricClosedIssues, Joi.equal('0 closed')),
})
t.create('Closed merge requests by label (raw)')
.get(
'/closed-raw/guoxudong.io/shields-test/issue-test.json?labels=enhancement'
)
.expectBadge({
label: 'closed enhancement merge requests',
message: isMetric,
})
/**
* All issue number case
*/
t.create('All merge requests')
.get('/all/guoxudong.io/shields-test/issue-test.json')
.expectBadge({
label: 'merge requests',
message: Joi.string().regex(
/^([0-9]+[kMGTPEZY]?|[1-9]\.[1-9][kMGTPEZY]) all$/
),
})
t.create('All merge requests raw')
.get('/all-raw/guoxudong.io/shields-test/issue-test.json')
.expectBadge({
label: 'all merge requests',
message: isMetric,
})
t.create('All merge requests by label is > zero')
.get('/all/guoxudong.io/shields-test/issue-test.json?labels=discussion')
.expectBadge({
label: 'discussion merge requests',
message: Joi.string().regex(
/^([0-9]+[kMGTPEZY]?|[1-9]\.[1-9][kMGTPEZY]) all$/
),
})
t.create('All merge requests by multi-word label is > zero')
.get(
'/all/guoxudong.io/shields-test/issue-test.json?labels=discussion,enhancement'
)
.expectBadge({
label: 'discussion,enhancement merge requests',
message: Joi.string().regex(
/^([0-9]+[kMGTPEZY]?|[1-9]\.[1-9][kMGTPEZY]) all$/
),
})
t.create('All merge requests by label (raw)')
.get('/all-raw/guoxudong.io/shields-test/issue-test.json?labels=discussion')
.expectBadge({
label: 'all discussion merge requests',
message: isMetric,
})
t.create('more than 10k merge requests')
.get('/all/gitlab-org%2Fgitlab.json')
.expectBadge({
label: 'merge requests',
message: 'more than 10k all',
})
t.create('locked merge requests')
.get('/locked/gitlab-org%2Fgitlab.json')
.expectBadge({
label: 'merge requests',
message: Joi.string().regex(
/^([0-9]+[kMGTPEZY]?|[1-9]\.[1-9][kMGTPEZY]) locked$/
),
})
t.create('Opened merge requests (self-managed)')
.get('/open/gitlab-cn/gitlab.json?gitlab_url=https://jihulab.com')
.expectBadge({
label: 'merge requests',
message: isMetricOpenIssues,
})

View File

@@ -1,74 +0,0 @@
import Joi from 'joi'
import { optionalUrl, nonNegativeInteger } from '../validators.js'
import { metric } from '../text-formatters.js'
import GitLabBase from './gitlab-base.js'
const schema = Joi.object({
star_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 GitlabStars extends GitLabBase {
static category = 'social'
static route = {
base: 'gitlab/stars',
pattern: ':project+',
queryParamSchema,
}
static examples = [
{
title: 'GitLab stars',
namedParams: {
project: 'gitlab-org/gitlab',
},
queryParams: { gitlab_url: 'https://gitlab.com' },
staticPreview: {
label: 'stars',
message: '3.9k',
style: 'social',
},
documentation,
},
]
static defaultBadgeData = { label: 'stars', namedLogo: 'gitlab' }
static render({ baseUrl, project, starCount }) {
return {
message: metric(starCount),
color: 'blue',
link: [`${baseUrl}/${project}`, `${baseUrl}/${project}/-/starrers`],
}
}
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 { star_count: starCount } = await this.fetch({
project,
baseUrl,
})
return this.constructor.render({ baseUrl, project, starCount })
}
}

View File

@@ -1,35 +0,0 @@
import { isMetric } from '../test-validators.js'
import { createServiceTester } from '../tester.js'
export const t = await createServiceTester()
t.create('Stars')
.get('/gitlab-org/gitlab.json')
.expectBadge({
label: 'stars',
message: isMetric,
color: 'blue',
link: [
'https://gitlab.com/gitlab-org/gitlab',
'https://gitlab.com/gitlab-org/gitlab/-/starrers',
],
})
t.create('Stars (self-managed)')
.get('/gitlab-cn/gitlab.json?gitlab_url=https://jihulab.com')
.expectBadge({
label: 'stars',
message: isMetric,
color: 'blue',
link: [
'https://jihulab.com/gitlab-cn/gitlab',
'https://jihulab.com/gitlab-cn/gitlab/-/starrers',
],
})
t.create('Stars (project not found)')
.get('/user1/gitlab-does-not-have-this-repo.json')
.expectBadge({
label: 'stars',
message: 'project not found',
})

View File

@@ -1,40 +0,0 @@
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,
})
}
}

View File

@@ -1,31 +0,0 @@
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',
})
})
})

View File

@@ -1,14 +0,0 @@
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' })

View File

@@ -3,24 +3,12 @@ import { redirector } from '../index.js'
export default [
redirector({
category: 'version',
name: 'JitpackVersionGitHubRedirect',
route: {
base: 'jitpack/v',
pattern: ':user/:repo',
pattern: ':groupId/:artifactId',
},
transformPath: ({ user, repo }) =>
`/jitpack/version/com.github.${user}/${repo}`,
dateAdded: new Date('2022-08-21'),
}),
redirector({
category: 'version',
name: 'JitpackVersionVcsRedirect',
route: {
base: 'jitpack/v',
pattern: ':vcs(github|bitbucket|gitlab|gitee)/:user/:repo',
},
transformPath: ({ vcs, user, repo }) =>
`/jitpack/version/com.${vcs}.${user}/${repo}`,
dateAdded: new Date('2022-08-21'),
transformPath: ({ groupId, artifactId }) =>
`/jitpack/v/github/${groupId}/${artifactId}`,
dateAdded: new Date('2019-03-31'),
}),
]

View File

@@ -6,10 +6,6 @@ export const t = new ServiceTester({
pathPrefix: '/jitpack/v',
})
t.create('jitpack version redirect (no vcs)')
t.create('jitpack version redirect')
.get('/jitpack/maven-simple.svg')
.expectRedirect('/jitpack/version/com.github.jitpack/maven-simple.svg')
t.create('jitpack version redirect (github)')
.get('/github/jitpack/maven-simple.svg')
.expectRedirect('/jitpack/version/com.github.jitpack/maven-simple.svg')
.expectRedirect('/jitpack/v/github/jitpack/maven-simple.svg')

View File

@@ -10,19 +10,18 @@ const schema = Joi.object({
export default class JitPackVersion extends BaseJsonService {
static category = 'version'
// Changed endpoint to allow any groupId, custom domains included
// See: https://github.com/badges/shields/issues/8312
static route = {
base: 'jitpack/version',
pattern: ':groupId/:artifactId',
base: 'jitpack/v',
pattern: ':vcs(github|bitbucket|gitlab|gitee)/:user/:repo',
}
static examples = [
{
title: 'JitPack',
namedParams: {
groupId: 'com.github.jitpack',
artifactId: 'maven-simple',
vcs: 'github',
user: 'jitpack',
repo: 'maven-simple',
},
staticPreview: renderVersionBadge({ version: 'v1.1' }),
keywords: ['java', 'maven'],
@@ -31,8 +30,8 @@ export default class JitPackVersion extends BaseJsonService {
static defaultBadgeData = { label: 'jitpack' }
async fetch({ groupId, artifactId }) {
const url = `https://jitpack.io/api/builds/${groupId}/${artifactId}/latestOk`
async fetch({ vcs, user, repo }) {
const url = `https://jitpack.io/api/builds/com.${vcs}.${user}/${repo}/latestOk`
return this._requestJson({
schema,
@@ -41,8 +40,8 @@ export default class JitPackVersion extends BaseJsonService {
})
}
async handle({ groupId, artifactId }) {
const { version } = await this.fetch({ groupId, artifactId })
async handle({ vcs, user, repo }) {
const { version } = await this.fetch({ vcs, user, repo })
return renderVersionBadge({ version })
}
}

View File

@@ -6,9 +6,9 @@ export const t = await createServiceTester()
const isAnyV = Joi.string().regex(/^v.+$/)
t.create('version (groupId)')
.get('/com.github.erayerdin/kappdirs.json')
.get('/github/erayerdin/kappdirs.json')
.expectBadge({ label: 'jitpack', message: isAnyV })
t.create('unknown package')
.get('/com.github.some-bogus-user/project.json')
.get('/github/some-bogus-user/project.json')
.expectBadge({ label: 'jitpack', message: 'project not found or private' })

View File

@@ -1,4 +1,4 @@
import dayjs from 'dayjs'
import moment from 'moment'
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 = dayjs().format(dateFormat)
const currentDate = moment().format(dateFormat)
return Object.keys(versions).filter(function (version) {
const data = versions[version]
return data.lts && data.lts < currentDate && data.end > currentDate

View File

@@ -1,7 +1,7 @@
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import dayjs from 'dayjs'
import moment from 'moment'
const dateFormat = 'YYYY-MM-DD'
@@ -67,7 +67,7 @@ const mockVersionsSha = () => nock => {
}
const mockReleaseSchedule = () => nock => {
const currentDate = dayjs()
const currentDate = moment()
const schedule = {
'v0.10': {
start: '2013-03-11',

View File

@@ -9,11 +9,16 @@ const schema = Joi.object({
license: Joi.string().allow('').allow(null),
classifiers: Joi.array().items(Joi.string()).required(),
}).required(),
urls: Joi.array()
.items(
Joi.object({
packagetype: Joi.string().required(),
})
releases: Joi.object()
.pattern(
Joi.string(),
Joi.array()
.items(
Joi.object({
packagetype: Joi.string().required(),
})
)
.required()
)
.required(),
}).required()

View File

@@ -1,12 +1,45 @@
import { redirector } from '../index.js'
import PypiBase from './pypi-base.js'
import { sortDjangoVersions, parseClassifiers } from './pypi-helpers.js'
export default redirector({
category: 'platform-support',
route: {
base: 'pypi/djversions',
pattern: ':packageName*',
},
transformPath: ({ packageName }) =>
`/pypi/frameworkversions/django/${packageName}`,
dateAdded: new Date('2022-07-28'),
})
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 })
}
}

View File

@@ -1,24 +1,32 @@
import Joi from 'joi'
import { createServiceTester } from '../tester.js'
export const t = await createServiceTester()
t.create(
'redirect supported django versions (valid, package version in request)'
const isPipeSeparatedDjangoVersions = Joi.string().regex(
/^([1-9]\.[0-9]+(?: \| )?)+$/
)
t.create('supported django versions (valid, package version in request)')
.get('/djangorestframework/3.7.3.json')
.expectRedirect(
'/pypi/frameworkversions/django/djangorestframework/3.7.3.json'
)
.expectBadge({
label: 'django versions',
message: isPipeSeparatedDjangoVersions,
})
t.create(
'redirect supported django versions (valid, no package version specified)'
)
t.create('supported django versions (valid, no package version specified)')
.get('/djangorestframework.json')
.expectRedirect('/pypi/frameworkversions/django/djangorestframework.json')
.expectBadge({
label: 'django versions',
message: isPipeSeparatedDjangoVersions,
})
t.create('redirect supported django versions (no versions specified)')
t.create('supported django versions (no versions specified)')
.get('/django/1.11.json')
.expectRedirect('/pypi/frameworkversions/django/django/1.11.json')
.expectBadge({ label: 'django versions', message: 'missing' })
t.create('redirect supported django versions (invalid)')
t.create('supported django versions (invalid)')
.get('/not-a-package.json')
.expectRedirect('/pypi/frameworkversions/django/not-a-package.json')
.expectBadge({
label: 'django versions',
message: 'package or version not found',
})

View File

@@ -1,103 +0,0 @@
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 })
}
}

View File

@@ -1,164 +0,0 @@
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',
})

View File

@@ -6,7 +6,7 @@
our own functions to parse and sort django versions
*/
function parsePypiVersionString(str) {
function parseDjangoVersionString(str) {
if (typeof str !== 'string') {
return false
}
@@ -20,12 +20,18 @@ function parsePypiVersionString(str) {
}
// Sort an array of django versions low to high.
function sortPypiVersions(versions) {
function sortDjangoVersions(versions) {
return versions.sort((a, b) => {
if (parsePypiVersionString(a).major === parsePypiVersionString(b).major) {
return parsePypiVersionString(a).minor - parsePypiVersionString(b).minor
if (
parseDjangoVersionString(a).major === parseDjangoVersionString(b).major
) {
return (
parseDjangoVersionString(a).minor - parseDjangoVersionString(b).minor
)
} else {
return parsePypiVersionString(a).major - parsePypiVersionString(b).major
return (
parseDjangoVersionString(a).major - parseDjangoVersionString(b).major
)
}
})
}
@@ -82,12 +88,16 @@ function getLicenses(packageData) {
}
function getPackageFormats(packageData) {
const { urls } = packageData
const {
info: { version },
releases,
} = packageData
const releasesForVersion = releases[version]
return {
hasWheel: urls.some(({ packagetype }) =>
hasWheel: releasesForVersion.some(({ packagetype }) =>
['wheel', 'bdist_wheel'].includes(packagetype)
),
hasEgg: urls.some(({ packagetype }) =>
hasEgg: releasesForVersion.some(({ packagetype }) =>
['egg', 'bdist_egg'].includes(packagetype)
),
}
@@ -95,8 +105,8 @@ function getPackageFormats(packageData) {
export {
parseClassifiers,
parsePypiVersionString,
sortPypiVersions,
parseDjangoVersionString,
sortDjangoVersions,
getLicenses,
getPackageFormats,
}

View File

@@ -1,8 +1,8 @@
import { test, given, forCases } from 'sazerac'
import {
parseClassifiers,
parsePypiVersionString,
sortPypiVersions,
parseDjangoVersionString,
sortDjangoVersions,
getLicenses,
getPackageFormats,
} from './pypi-helpers.js'
@@ -60,7 +60,7 @@ describe('PyPI helpers', function () {
given(classifiersFixture, /^(?!.*)*$/).expect([])
})
test(parsePypiVersionString, function () {
test(parseDjangoVersionString, 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(sortPypiVersions, function () {
test(sortDjangoVersions, 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,17 +164,34 @@ describe('PyPI helpers', function () {
test(getPackageFormats, () => {
given({
urls: [{ packagetype: 'bdist_wheel' }, { packagetype: 'sdist' }],
info: { version: '2.19.1' },
releases: {
'1.0.4': [{ packagetype: 'sdist' }],
'2.19.1': [{ packagetype: 'bdist_wheel' }, { packagetype: 'sdist' }],
},
}).expect({ hasWheel: true, hasEgg: false })
given({
urls: [{ packagetype: 'sdist' }],
info: { version: '1.0.4' },
releases: {
'1.0.4': [{ packagetype: 'sdist' }],
'2.19.1': [{ packagetype: 'bdist_wheel' }, { packagetype: 'sdist' }],
},
}).expect({ hasWheel: false, hasEgg: false })
given({
urls: [
{ packagetype: 'bdist_egg' },
{ packagetype: 'bdist_egg' },
{ packagetype: 'sdist' },
],
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' },
],
},
}).expect({ hasWheel: false, hasEgg: true })
})
})

View File

@@ -24,7 +24,7 @@ t.create('license (from trove classifier)')
license: '',
classifiers: ['License :: OSI Approved :: MIT License'],
},
urls: [],
releases: {},
})
)
.expectBadge({
@@ -46,7 +46,7 @@ t.create('license (as acronym from trove classifier)')
'License :: OSI Approved :: GNU General Public License (GPL)',
],
},
urls: [],
releases: {},
})
)
.expectBadge({

View File

@@ -43,7 +43,7 @@ t.create('no trove classifiers')
license: 'foo',
classifiers: [],
},
urls: [],
releases: {},
})
)
.expectBadge({

View File

@@ -1,166 +0,0 @@
import gql from 'graphql-tag'
import Joi from 'joi'
import yaml from 'js-yaml'
import { renderVersionBadge } from '../version.js'
import { GithubAuthV4Service } from '../github/github-auth-service.js'
import { NotFound, InvalidResponse } from '../index.js'
const tagsSchema = Joi.object({
data: Joi.object({
repository: Joi.object({
refs: Joi.object({
edges: Joi.array()
.items({
node: Joi.object({
name: Joi.string().required(),
}).required(),
})
.required(),
}).required(),
}).required(),
}).required(),
}).required()
const contentSchema = Joi.object({
data: Joi.object({
repository: Joi.object({
object: Joi.object({
text: Joi.string().required(),
}).allow(null),
}).required(),
}).required(),
}).required()
const distroSchema = Joi.object({
repositories: Joi.object().required(),
})
const repoSchema = Joi.object({
release: Joi.object({
version: Joi.string().required(),
}).required(),
})
const documentation = `
<p>
To use this badge, specify the ROS <a href="http://docs.ros.org">distribution</a>
(e.g. <code>noetic</code> or <code>humble</code>) and the package repository name
(in the case of single-package repos, this may be the same as the package name).
This badge determines which versions are part of an official ROS distribution by
fetching from the <a href="https://github.com/ros/rosdistro">rosdistro</a> YAML files,
at the tag corresponding to the latest release.
</p>
`
export default class RosVersion extends GithubAuthV4Service {
static category = 'version'
static route = { base: 'ros/v', pattern: ':distro/:repoName' }
static examples = [
{
title: 'ROS Package Index',
namedParams: { distro: 'humble', repoName: 'vision_msgs' },
staticPreview: {
...renderVersionBadge({ version: '4.0.0' }),
label: 'ros | humble',
},
documentation,
},
]
static defaultBadgeData = { label: 'ros' }
async handle({ distro, repoName }) {
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,
repoName
)
return { ...renderVersionBadge({ version }), label: `ros | ${distro}` }
}
static _parseReleaseVersionFromDistro(distroYaml, repoName) {
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[repoName]) {
throw new NotFound({ prettyMessage: `repo not found: ${repoName}` })
}
const repoInfo = this._validate(
validatedDistro.repositories[repoName],
repoSchema,
{
prettyErrorMessage: `invalid section for ${repoName} in distribution.yml`,
}
)
// Strip off "release inc" suffix
return repoInfo.release.version.replace(/-\d+$/, '')
}
}

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import Joi from 'joi'
import dayjs from 'dayjs'
import moment from 'moment'
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 => dayjs(endDate).subtract(6, 'days'),
startDate: endDate => moment(endDate).subtract(6, 'days'),
interval: 'week',
},
dm: {
startDate: endDate => dayjs(endDate).subtract(30, 'days'),
startDate: endDate => moment(endDate).subtract(30, 'days'),
interval: 'month',
},
dt: {
startDate: () => dayjs(0),
startDate: () => moment(0),
},
}
@@ -78,7 +78,7 @@ export default class Sourceforge extends BaseJsonService {
folder ? `${folder}/` : ''
}stats/json`
// get yesterday since today is incomplete
const endDate = dayjs().subtract(24, 'hours')
const endDate = moment().subtract(24, 'hours')
const startDate = intervalMap[interval].startDate(endDate)
const options = {
searchParams: {

View File

@@ -1,4 +1,4 @@
import dayjs from 'dayjs'
import moment from 'moment'
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: 'dayjs' },
namedParams: { stackexchangesite: 'stackoverflow', query: 'momentjs' },
staticPreview: this.render({
stackexchangesite: 'stackoverflow',
query: 'dayjs',
query: 'momentjs',
numValue: 2000,
}),
keywords: ['stackexchange', 'stackoverflow'],
@@ -41,12 +41,12 @@ export default class StackExchangeMonthlyQuestions extends BaseJsonService {
}
async handle({ stackexchangesite, query }) {
const today = dayjs().toDate()
const prevMonthStart = dayjs(today)
const today = moment().toDate()
const prevMonthStart = moment(today)
.subtract(1, 'months')
.startOf('month')
.unix()
const prevMonthEnd = dayjs(today)
const prevMonthEnd = moment(today)
.subtract(1, 'months')
.endOf('month')
.unix()

View File

@@ -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 dayjs')
.get('/stackoverflow/qm/dayjs.json')
t.create('Monthly Questions for StackOverflow Momentjs')
.get('/stackoverflow/qm/momentjs.json')
.expectBadge({
label: 'stackoverflow dayjs questions',
label: 'stackoverflow momentjs questions',
message: isMetricOverTimePeriod,
})

View File

@@ -2,11 +2,8 @@
* Commonly-used functions for formatting text in badge labels. Includes
* ordinal numbers, currency codes, star ratings, versions, etc.
*/
import dayjs from 'dayjs'
import calendar from 'dayjs/plugin/calendar.js'
import relativeTime from 'dayjs/plugin/relativeTime.js'
dayjs.extend(calendar)
dayjs.extend(relativeTime)
import moment from 'moment'
moment().format()
function starRating(rating, max = 5) {
const flooredRating = Math.floor(rating)
@@ -112,7 +109,7 @@ function maybePluralize(singular, countable, plural) {
}
function formatDate(d) {
const date = dayjs(d)
const date = moment(d)
const dateString = date.calendar(null, {
lastDay: '[yesterday]',
sameDay: '[today]',
@@ -120,12 +117,12 @@ function formatDate(d) {
sameElse: 'MMMM YYYY',
})
// Trim current year from date string
return dateString.replace(` ${dayjs().year()}`, '').toLowerCase()
return dateString.replace(` ${moment().year()}`, '').toLowerCase()
}
function formatRelativeDate(timestamp) {
return dayjs()
.to(dayjs.unix(parseInt(timestamp, 10)))
return moment()
.to(moment.unix(parseInt(timestamp, 10)))
.toLowerCase()
}

View File

@@ -1,16 +1,14 @@
import dayjs from 'dayjs'
import customParseFormat from 'dayjs/plugin/customParseFormat.js'
import moment from 'moment'
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',
@@ -52,7 +50,7 @@ function LastUpdateForType(extensionType) {
}
transform(lastUpdate) {
const date = dayjs(lastUpdate, lastUpdateFormat)
const date = moment(lastUpdate, lastUpdateFormat)
if (date.isValid()) {
return date.format('YYYY-MM-DD')

View File

@@ -10,7 +10,6 @@
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "react",
"typeRoots": ["node_modules/@types", "frontend/types"],
"skipLibCheck": true
"typeRoots": ["node_modules/@types", "frontend/types"]
}
}