Compare commits
1 Commits
server-202
...
integratio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7da38f8354 |
@@ -1,5 +1,69 @@
|
||||
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
|
||||
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: |
|
||||
npm ci
|
||||
environment:
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
|
||||
- run:
|
||||
name: Integration tests
|
||||
when: always
|
||||
environment:
|
||||
mocha_reporter: mocha-junit-reporter
|
||||
MOCHA_FILE: junit/integration/results.xml
|
||||
command: npm run test:integration
|
||||
|
||||
- store_test_results:
|
||||
path: junit
|
||||
|
||||
services_steps: &services_steps
|
||||
steps:
|
||||
- checkout
|
||||
@@ -25,7 +89,83 @@ services_steps: &services_steps
|
||||
- store_test_results:
|
||||
path: junit
|
||||
|
||||
package_steps: &package_steps
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- run:
|
||||
name: Install node and npm
|
||||
command: |
|
||||
set +e
|
||||
export NVM_DIR="/opt/circleci/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||
nvm install v14
|
||||
nvm use v14
|
||||
npm install -g npm
|
||||
|
||||
# Run the package tests on each currently supported node version. See:
|
||||
# https://github.com/badges/shields/blob/master/badge-maker/README.md#node-version-support
|
||||
# https://nodejs.org/en/about/releases/
|
||||
|
||||
- run:
|
||||
environment:
|
||||
mocha_reporter: mocha-junit-reporter
|
||||
MOCHA_FILE: junit/badge-maker/v12/results.xml
|
||||
NODE_VERSION: v12
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
NPM_CONFIG_ENGINE_STRICT: 'false'
|
||||
name: Run package tests on Node 12
|
||||
command: scripts/run_package_tests.sh
|
||||
|
||||
- run:
|
||||
environment:
|
||||
mocha_reporter: mocha-junit-reporter
|
||||
MOCHA_FILE: junit/badge-maker/v14/results.xml
|
||||
NODE_VERSION: v14
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
NPM_CONFIG_ENGINE_STRICT: 'false'
|
||||
name: Run package tests on Node 14
|
||||
command: scripts/run_package_tests.sh
|
||||
|
||||
- run:
|
||||
environment:
|
||||
mocha_reporter: mocha-junit-reporter
|
||||
MOCHA_FILE: junit/badge-maker/v16/results.xml
|
||||
NODE_VERSION: v16
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
name: Run package tests on Node 16
|
||||
command: scripts/run_package_tests.sh
|
||||
|
||||
- store_test_results:
|
||||
path: junit
|
||||
|
||||
jobs:
|
||||
main:
|
||||
docker:
|
||||
- image: cimg/node:16.15
|
||||
|
||||
<<: *main_steps
|
||||
|
||||
main@node-17:
|
||||
docker:
|
||||
- image: cimg/node:17.9
|
||||
|
||||
<<: *main_steps
|
||||
|
||||
integration:
|
||||
docker:
|
||||
- image: cimg/node:16.15
|
||||
- image: redis
|
||||
|
||||
<<: *integration_steps
|
||||
|
||||
integration@node-17:
|
||||
docker:
|
||||
- image: cimg/node:17.9
|
||||
- image: redis
|
||||
|
||||
<<: *integration_steps
|
||||
|
||||
danger:
|
||||
docker:
|
||||
- image: cimg/node:16.15
|
||||
@@ -84,6 +224,12 @@ jobs:
|
||||
when: always
|
||||
command: npm run build
|
||||
|
||||
package:
|
||||
machine:
|
||||
image: 'ubuntu-2004:202111-02'
|
||||
|
||||
<<: *package_steps
|
||||
|
||||
services:
|
||||
docker:
|
||||
- image: cimg/node:16.15
|
||||
@@ -93,20 +239,78 @@ jobs:
|
||||
services@node-17:
|
||||
docker:
|
||||
- image: cimg/node:17.9
|
||||
environment:
|
||||
NPM_CONFIG_ENGINE_STRICT: 'false'
|
||||
|
||||
<<: *services_steps
|
||||
|
||||
e2e:
|
||||
docker:
|
||||
- image: cypress/base:16.14.0
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- restore_cache:
|
||||
name: Restore Cypress binary
|
||||
keys:
|
||||
- v2-cypress-dependencies-{{ checksum "package-lock.json" }}
|
||||
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: |
|
||||
npm ci
|
||||
|
||||
- run:
|
||||
name: Frontend build
|
||||
command: GATSBY_BASE_URL=http://localhost:8080 npm run build
|
||||
|
||||
- run:
|
||||
name: Run tests
|
||||
environment:
|
||||
CYPRESS_REPORTER: junit
|
||||
MOCHA_FILE: junit/e2e/results.xml
|
||||
command: npm run e2e-on-build
|
||||
|
||||
- store_test_results:
|
||||
path: junit
|
||||
|
||||
- store_artifacts:
|
||||
path: cypress/videos
|
||||
|
||||
- store_artifacts:
|
||||
path: cypress/screenshots
|
||||
|
||||
- save_cache:
|
||||
name: Cache Cypress binary
|
||||
paths:
|
||||
# https://docs.cypress.io/guides/getting-started/installing-cypress.html#Binary-cache
|
||||
- ~/.cache/Cypress
|
||||
key: v2-cypress-dependencies-{{ checksum "package-lock.json" }}
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
|
||||
on-commit:
|
||||
jobs:
|
||||
- main:
|
||||
filters:
|
||||
branches:
|
||||
ignore: gh-pages
|
||||
- main@node-17:
|
||||
filters:
|
||||
branches:
|
||||
ignore: gh-pages
|
||||
- integration@node-17:
|
||||
filters:
|
||||
branches:
|
||||
ignore: gh-pages
|
||||
- frontend:
|
||||
filters:
|
||||
branches:
|
||||
ignore: gh-pages
|
||||
- package:
|
||||
filters:
|
||||
branches:
|
||||
ignore: gh-pages
|
||||
- services:
|
||||
filters:
|
||||
branches:
|
||||
@@ -126,12 +330,22 @@ workflows:
|
||||
- master
|
||||
- gh-pages
|
||||
- /dependabot\/.*/
|
||||
- e2e:
|
||||
filters:
|
||||
branches:
|
||||
ignore: gh-pages
|
||||
# on-commit-with-cache:
|
||||
# jobs:
|
||||
# - npm-install:
|
||||
# filters:
|
||||
# branches:
|
||||
# ignore: gh-pages
|
||||
# - main:
|
||||
# requires:
|
||||
# - npm-install
|
||||
# - main@node-latest:
|
||||
# requires:
|
||||
# - npm-install
|
||||
# - frontend:
|
||||
# requires:
|
||||
# - npm-install
|
||||
|
||||
@@ -144,8 +144,6 @@ rules:
|
||||
func-style: ['error', 'declaration', { 'allowArrowFunctions': true }]
|
||||
new-cap: ['error', { 'capIsNew': true }]
|
||||
import/order: ['error', { 'newlines-between': 'never' }]
|
||||
quotes:
|
||||
['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }]
|
||||
|
||||
# Account for destructuring responses from upstream services,
|
||||
# many of which do not follow camelcase
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/3_Badge_request.md
vendored
2
.github/ISSUE_TEMPLATE/3_Badge_request.md
vendored
@@ -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.
|
||||
-->
|
||||
|
||||
|
||||
2
.github/actions/close-bot/index.js
vendored
2
.github/actions/close-bot/index.js
vendored
@@ -27,7 +27,7 @@ async function run() {
|
||||
state: 'closed',
|
||||
})
|
||||
|
||||
core.debug('Done.')
|
||||
core.debug(`Done.`)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
47
.github/actions/close-bot/package-lock.json
generated
vendored
47
.github/actions/close-bot/package-lock.json
generated
vendored
@@ -9,23 +9,22 @@
|
||||
"version": "0.0.0",
|
||||
"license": "CC0",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.0",
|
||||
"@actions/github": "^5.1.1"
|
||||
"@actions/core": "^1.9.0",
|
||||
"@actions/github": "^5.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/core": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz",
|
||||
"integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==",
|
||||
"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": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@actions/github/-/github-5.1.1.tgz",
|
||||
"integrity": "sha512-Nk59rMDoJaV+mHCOJPXuvB1zIbomlKS0dmSIqPGxd0enAXBnOfn4VWF+CGtRCwXZG9Epa54tZA7VIRlJDS8A6g==",
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@actions/github/-/github-5.0.3.tgz",
|
||||
"integrity": "sha512-myjA/pdLQfhUGLtRZC/J4L1RXOG4o6aYdiEq+zr5wVVKljzbFld+xv10k1FX6IkIJtNxbAq44BdwSNpQ015P0A==",
|
||||
"dependencies": {
|
||||
"@actions/http-client": "^2.0.1",
|
||||
"@octokit/core": "^3.6.0",
|
||||
@@ -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,18 +226,17 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz",
|
||||
"integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==",
|
||||
"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": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@actions/github/-/github-5.1.1.tgz",
|
||||
"integrity": "sha512-Nk59rMDoJaV+mHCOJPXuvB1zIbomlKS0dmSIqPGxd0enAXBnOfn4VWF+CGtRCwXZG9Epa54tZA7VIRlJDS8A6g==",
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@actions/github/-/github-5.0.3.tgz",
|
||||
"integrity": "sha512-myjA/pdLQfhUGLtRZC/J4L1RXOG4o6aYdiEq+zr5wVVKljzbFld+xv10k1FX6IkIJtNxbAq44BdwSNpQ015P0A==",
|
||||
"requires": {
|
||||
"@actions/http-client": "^2.0.1",
|
||||
"@octokit/core": "^3.6.0",
|
||||
@@ -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",
|
||||
|
||||
4
.github/actions/close-bot/package.json
vendored
4
.github/actions/close-bot/package.json
vendored
@@ -10,7 +10,7 @@
|
||||
"author": "chris48s",
|
||||
"license": "CC0",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.0",
|
||||
"@actions/github": "^5.1.1"
|
||||
"@actions/core": "^1.9.0",
|
||||
"@actions/github": "^5.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
21
.github/actions/core-tests/action.yml
vendored
21
.github/actions/core-tests/action.yml
vendored
@@ -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
|
||||
20
.github/actions/integration-tests/action.yml
vendored
20
.github/actions/integration-tests/action.yml
vendored
@@ -1,20 +0,0 @@
|
||||
name: 'Integration tests'
|
||||
description: 'Run integration tests'
|
||||
inputs:
|
||||
github-token:
|
||||
description: 'The GITHUB_TOKEN secret'
|
||||
required: true
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Integration Tests
|
||||
if: always()
|
||||
run: npm run test:integration -- --reporter json --reporter-option 'output=reports/integration-tests.json'
|
||||
env:
|
||||
GH_TOKEN: '${{ inputs.github-token }}'
|
||||
shell: bash
|
||||
|
||||
- name: Write Markdown Summary
|
||||
if: always()
|
||||
run: node scripts/mocha2md.js Integration reports/integration-tests.json >> $GITHUB_STEP_SUMMARY
|
||||
shell: bash
|
||||
26
.github/actions/package-tests/action.yml
vendored
26
.github/actions/package-tests/action.yml
vendored
@@ -1,26 +0,0 @@
|
||||
name: 'Package tests'
|
||||
description: 'Run package tests and check types'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Tests
|
||||
if: always()
|
||||
run: npm run test:package -- --reporter json --reporter-option 'output=reports/package-tests.json'
|
||||
shell: bash
|
||||
|
||||
- name: Type Checks
|
||||
if: always()
|
||||
run: |
|
||||
set -o pipefail
|
||||
npm run check-types:package 2>&1 | tee reports/package-types.txt
|
||||
shell: bash
|
||||
|
||||
- name: Write Markdown Summary
|
||||
if: always()
|
||||
run: |
|
||||
node scripts/mocha2md.js 'Package Tests' reports/package-tests.json >> $GITHUB_STEP_SUMMARY
|
||||
echo '# Package Types' >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
cat reports/package-types.txt >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
shell: bash
|
||||
36
.github/actions/setup/action.yml
vendored
36
.github/actions/setup/action.yml
vendored
@@ -1,36 +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 (boolean)'
|
||||
type: boolean
|
||||
# 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: false
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Install Node JS ${{ inputs.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
if: ${{ inputs.cypress == 'false' }}
|
||||
env:
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
run: |
|
||||
echo "skipping cypress binary"
|
||||
npm ci
|
||||
shell: bash
|
||||
|
||||
- name: Install dependencies (including cypress binary)
|
||||
if: ${{ inputs.cypress == 'true' }}
|
||||
run: |
|
||||
echo "installing cypress binary"
|
||||
npm ci
|
||||
shell: bash
|
||||
6
.github/workflows/auto-close.yml
vendored
6
.github/workflows/auto-close.yml
vendored
@@ -1,13 +1,11 @@
|
||||
name: Auto close
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
on: pull_request_target
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
auto-close:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor == 'dependabot[bot]'
|
||||
steps:
|
||||
|
||||
2
.github/workflows/build-docker-image.yml
vendored
2
.github/workflows/build-docker-image.yml
vendored
@@ -3,7 +3,7 @@ on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build-docker-image:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
2
.github/workflows/deploy-docs.yml
vendored
2
.github/workflows/deploy-docs.yml
vendored
@@ -8,7 +8,7 @@ permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
deploy-docs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
2
.github/workflows/draft-release.yml
vendored
2
.github/workflows/draft-release.yml
vendored
@@ -10,7 +10,7 @@ permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
draft-release:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
name: 'Dependency Review'
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
enforce-dependency-review:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v3
|
||||
uses: actions/dependency-review-action@v2
|
||||
|
||||
2
.github/workflows/publish-docker-next.yml
vendored
2
.github/workflows/publish-docker-next.yml
vendored
@@ -5,7 +5,7 @@ on:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
publish-docker-next:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
52
.github/workflows/test-e2e.yml
vendored
52
.github/workflows/test-e2e.yml
vendored
@@ -1,52 +0,0 @@
|
||||
name: E2E
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
- 'dependabot/**'
|
||||
|
||||
jobs:
|
||||
test-e2e:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Cache Cypress binary
|
||||
id: cache-cypress
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-cypress
|
||||
with:
|
||||
path: ~/.cache/Cypress
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: 16
|
||||
cypress: true
|
||||
|
||||
- name: Frontend build
|
||||
run: GATSBY_BASE_URL=http://localhost:8080 npm run build
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
run: npm run e2e-on-build
|
||||
|
||||
- name: Archive videos
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: videos
|
||||
path: cypress/videos
|
||||
|
||||
- name: Archive screenshots
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: screenshots
|
||||
path: cypress/screenshots
|
||||
48
.github/workflows/test-integration-17.yml
vendored
48
.github/workflows/test-integration-17.yml
vendored
@@ -1,48 +0,0 @@
|
||||
name: Integration@node 17
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
- 'dependabot/**'
|
||||
|
||||
jobs:
|
||||
test-integration-17:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PAT_EXISTS: ${{ secrets.GH_PAT != '' }}
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: 17
|
||||
env:
|
||||
NPM_CONFIG_ENGINE_STRICT: 'false'
|
||||
|
||||
- name: Integration Tests (with PAT)
|
||||
if: ${{ env.PAT_EXISTS == 'true' }}
|
||||
uses: ./.github/actions/integration-tests
|
||||
with:
|
||||
github-token: '${{ secrets.GH_PAT }}'
|
||||
|
||||
- name: Integration Tests (with workflow token)
|
||||
if: ${{ env.PAT_EXISTS == 'false' }}
|
||||
uses: ./.github/actions/integration-tests
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
46
.github/workflows/test-integration.yml
vendored
46
.github/workflows/test-integration.yml
vendored
@@ -1,46 +0,0 @@
|
||||
name: Integration
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
- 'dependabot/**'
|
||||
|
||||
jobs:
|
||||
test-integration:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PAT_EXISTS: ${{ secrets.GH_PAT != '' }}
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Integration Tests (with PAT)
|
||||
if: ${{ env.PAT_EXISTS == 'true' }}
|
||||
uses: ./.github/actions/integration-tests
|
||||
with:
|
||||
github-token: '${{ secrets.GH_PAT }}'
|
||||
|
||||
- name: Integration Tests (with workflow token)
|
||||
if: ${{ env.PAT_EXISTS == 'false' }}
|
||||
uses: ./.github/actions/integration-tests
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
28
.github/workflows/test-lint.yml
vendored
28
.github/workflows/test-lint.yml
vendored
@@ -1,28 +0,0 @@
|
||||
name: Lint
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
- 'dependabot/**'
|
||||
|
||||
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
|
||||
25
.github/workflows/test-main-17.yml
vendored
25
.github/workflows/test-main-17.yml
vendored
@@ -1,25 +0,0 @@
|
||||
name: Main@node 17
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
- 'dependabot/**'
|
||||
|
||||
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
|
||||
28
.github/workflows/test-main.yml
vendored
28
.github/workflows/test-main.yml
vendored
@@ -1,28 +0,0 @@
|
||||
name: Main
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
- 'dependabot/**'
|
||||
|
||||
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
|
||||
46
.github/workflows/test-package-cli.yml
vendored
46
.github/workflows/test-package-cli.yml
vendored
@@ -1,46 +0,0 @@
|
||||
name: Package CLI
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
- 'dependabot/**'
|
||||
|
||||
# Smoke test (render a badge with the CLI) with only the package
|
||||
# dependencies installed.
|
||||
|
||||
jobs:
|
||||
test-package-cli:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- node: '14'
|
||||
engine-strict: 'false'
|
||||
- node: '16'
|
||||
engine-strict: 'false'
|
||||
- node: '18'
|
||||
engine-strict: 'true'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Node JS ${{ inputs.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install dependencies
|
||||
env:
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
NPM_CONFIG_ENGINE_STRICT: ${{ matrix.engine-strict }}
|
||||
run: |
|
||||
cd badge-maker
|
||||
npm install
|
||||
npm link
|
||||
|
||||
- name: Render a badge with the CLI
|
||||
run: |
|
||||
cd badge-maker
|
||||
badge cactus grown :green @flat
|
||||
34
.github/workflows/test-package-lib.yml
vendored
34
.github/workflows/test-package-lib.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: Package Library
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
- 'dependabot/**'
|
||||
|
||||
jobs:
|
||||
test-package-lib:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- node: '14'
|
||||
engine-strict: 'false'
|
||||
- node: '16'
|
||||
engine-strict: 'true'
|
||||
- node: '18'
|
||||
engine-strict: 'false'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
env:
|
||||
NPM_CONFIG_ENGINE_STRICT: ${{ matrix.engine-strict }}
|
||||
|
||||
- name: Package tests
|
||||
uses: ./.github/actions/package-tests
|
||||
78
CHANGELOG.md
78
CHANGELOG.md
@@ -4,84 +4,6 @@ Note: this changelog is for the shields.io server. The changelog for the badge-m
|
||||
|
||||
---
|
||||
|
||||
## server-2022-12-01
|
||||
|
||||
- fix: support logoColor to shield icons. [#8263](https://github.com/badges/shields/issues/8263)
|
||||
- handle missing properties array in [VisualStudioMarketplaceVersion] [#8603](https://github.com/badges/shields/issues/8603)
|
||||
- deprecate [wercker] service [#8642](https://github.com/badges/shields/issues/8642)
|
||||
- Add [Coincap] Cryptocurrency badges [#8623](https://github.com/badges/shields/issues/8623)
|
||||
- Add [modrinth] version [#8604](https://github.com/badges/shields/issues/8604)
|
||||
- [factorio-mod-portal] services [#8625](https://github.com/badges/shields/issues/8625)
|
||||
- [Coveralls] for GitLab [#8584](https://github.com/badges/shields/issues/8584), [#8644](https://github.com/badges/shields/issues/8644)
|
||||
- Remove 'suggest badges' feature [#8311](https://github.com/badges/shields/issues/8311)
|
||||
- Add [modrinth] followers [#8601](https://github.com/badges/shields/issues/8601)
|
||||
- Update the [modrinth] API to v2 [#8600](https://github.com/badges/shields/issues/8600)
|
||||
- tidy up [GitHubGist] routes [#8510](https://github.com/badges/shields/issues/8510)
|
||||
- fix [flathub] version error handling [#8500](https://github.com/badges/shields/issues/8500)
|
||||
- Dependency updates
|
||||
|
||||
## server-2022-11-01
|
||||
|
||||
- [Ansible] Add collection badge [#8578](https://github.com/badges/shields/issues/8578)
|
||||
- [VisualStudioMarketplace] Add support to prerelease extensions version (Issue #8207) [#8561](https://github.com/badges/shields/issues/8561)
|
||||
- feat: add [GitlabLastCommit] service [#8508](https://github.com/badges/shields/issues/8508)
|
||||
- fix [swagger] service tests (allow 0 items in array) [#8564](https://github.com/badges/shields/issues/8564)
|
||||
- fix codecov badge for non-default branch [#8565](https://github.com/badges/shields/issues/8565)
|
||||
- Add [GitHubLastCommit] by committer badge [#8537](https://github.com/badges/shields/issues/8537)
|
||||
- [GitHubReleaseDate] - published_at field [#8543](https://github.com/badges/shields/issues/8543)
|
||||
- Fix [Testspace] with new "untested" value in case_counts array [#8544](https://github.com/badges/shields/issues/8544)
|
||||
- fix: Support WAITING status for GitHub deployments [#8521](https://github.com/badges/shields/issues/8521)
|
||||
- [Whatpulse] badge for a user and for a team [#8466](https://github.com/badges/shields/issues/8466)
|
||||
- deprecate [pkgreview] service [#8499](https://github.com/badges/shields/issues/8499)
|
||||
- Dependency updates
|
||||
|
||||
## server-2022-10-08
|
||||
|
||||
- deprecate [criterion] service [#8501](https://github.com/badges/shields/issues/8501)
|
||||
- fix formatRelativeDate error handling; run [date] [#8497](https://github.com/badges/shields/issues/8497)
|
||||
- allow/validate bitbucket_username / bitbucket_password in private config schema [#8472](https://github.com/badges/shields/issues/8472)
|
||||
- fix [pub] points badge test and example [#8498](https://github.com/badges/shields/issues/8498)
|
||||
- feat: add [GitlabLanguageCount] service [#8377](https://github.com/badges/shields/issues/8377)
|
||||
- [GitHubGistStars] add GitHub Gist Stars [#8471](https://github.com/badges/shields/issues/8471)
|
||||
- fix display/search of CII badge examples [#8473](https://github.com/badges/shields/issues/8473)
|
||||
- feat: add 2022 support to GitHub Hacktoberfest [#8468](https://github.com/badges/shields/issues/8468)
|
||||
- fix [GitLabCoverage] subgroup bug [#8401](https://github.com/badges/shields/issues/8401)
|
||||
- implement ruby gems-specific version sort/color functions [#8434](https://github.com/badges/shields/issues/8434)
|
||||
- Add `rc` to pre-release identifiers [#8435](https://github.com/badges/shields/issues/8435)
|
||||
- add [GitHub] Number of commits between branches/tags/commits [#8394](https://github.com/badges/shields/issues/8394)
|
||||
- add [Packagist] dependency version [#8371](https://github.com/badges/shields/issues/8371)
|
||||
- fix Docker build status invalid response data bug [#8392](https://github.com/badges/shields/issues/8392)
|
||||
- Dependency updates
|
||||
|
||||
## 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)
|
||||
|
||||
@@ -134,7 +134,7 @@ Prettier before a commit by default.
|
||||
When adding or changing a service [please write tests][service-tests], and ensure the [title of your Pull Requests follows the required conventions](#running-service-tests-in-pull-requests) to ensure your tests are executed.
|
||||
When changing other code, please add unit tests.
|
||||
|
||||
To run the integration tests, you must have Redis installed and in your PATH.
|
||||
To run the integration tests, you must have redis installed and in your PATH.
|
||||
Use `brew install redis`, `yum install redis`, etc. The test runner will
|
||||
start the server automatically.
|
||||
|
||||
|
||||
10
app.json
10
app.json
@@ -35,16 +35,6 @@
|
||||
"WEBLATE_API_KEY": {
|
||||
"description": "Configure the API key to be used for the Weblate service.",
|
||||
"required": false
|
||||
},
|
||||
"METRICS_INFLUX_ENABLED": {
|
||||
"description": "Disable influx metrics",
|
||||
"value": "false",
|
||||
"required": false
|
||||
},
|
||||
"REQUIRE_CLOUDFLARE": {
|
||||
"description": "Allow direct traffic",
|
||||
"value": "false",
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"formation": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 4.0.0 [WIP]
|
||||
|
||||
- Drop compatibility with Node < 14
|
||||
- Drop compatibility with Node 10
|
||||
|
||||
## 3.3.1
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"badge": "lib/badge-cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14",
|
||||
"node": ">= 12",
|
||||
"npm": ">= 6"
|
||||
},
|
||||
"collective": {
|
||||
|
||||
18
core/badge-urls/make-badge-url.d.ts
vendored
18
core/badge-urls/make-badge-url.d.ts
vendored
@@ -14,6 +14,24 @@ export function badgeUrlFromPath({
|
||||
longCache?: boolean
|
||||
}): string
|
||||
|
||||
export function badgeUrlFromPattern({
|
||||
baseUrl,
|
||||
pattern,
|
||||
namedParams,
|
||||
queryParams,
|
||||
style,
|
||||
format,
|
||||
longCache,
|
||||
}: {
|
||||
baseUrl?: string
|
||||
pattern: string
|
||||
namedParams: { [k: string]: string }
|
||||
queryParams: { [k: string]: string | number | boolean }
|
||||
style?: string
|
||||
format?: string
|
||||
longCache?: boolean
|
||||
}): string
|
||||
|
||||
export function encodeField(s: string): string
|
||||
|
||||
export function staticBadgeUrl({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Avoid "Attempted import error: 'URL' is not exported from 'url'" in frontend.
|
||||
import url from 'url'
|
||||
import queryString from 'query-string'
|
||||
import { compile } from 'path-to-regexp'
|
||||
|
||||
function badgeUrlFromPath({
|
||||
baseUrl = '',
|
||||
@@ -22,6 +23,33 @@ function badgeUrlFromPath({
|
||||
return `${baseUrl}${path}${outExt}${suffix}`
|
||||
}
|
||||
|
||||
function badgeUrlFromPattern({
|
||||
baseUrl = '',
|
||||
pattern,
|
||||
namedParams,
|
||||
queryParams,
|
||||
style,
|
||||
format = '',
|
||||
longCache = false,
|
||||
}) {
|
||||
const toPath = compile(pattern, {
|
||||
strict: true,
|
||||
sensitive: true,
|
||||
encode: encodeURIComponent,
|
||||
})
|
||||
|
||||
const path = toPath(namedParams)
|
||||
|
||||
return badgeUrlFromPath({
|
||||
baseUrl,
|
||||
path,
|
||||
queryParams,
|
||||
style,
|
||||
format,
|
||||
longCache,
|
||||
})
|
||||
}
|
||||
|
||||
function encodeField(s) {
|
||||
return encodeURIComponent(s.replace(/-/g, '--').replace(/_/g, '__'))
|
||||
}
|
||||
@@ -126,6 +154,7 @@ function rasterRedirectUrl({ rasterUrl }, badgeUrl) {
|
||||
|
||||
export {
|
||||
badgeUrlFromPath,
|
||||
badgeUrlFromPattern,
|
||||
encodeField,
|
||||
staticBadgeUrl,
|
||||
queryStringStaticBadgeUrl,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { test, given } from 'sazerac'
|
||||
import {
|
||||
badgeUrlFromPath,
|
||||
badgeUrlFromPattern,
|
||||
encodeField,
|
||||
staticBadgeUrl,
|
||||
queryStringStaticBadgeUrl,
|
||||
@@ -19,6 +20,18 @@ describe('Badge URL generation functions', function () {
|
||||
)
|
||||
})
|
||||
|
||||
test(badgeUrlFromPattern, () => {
|
||||
given({
|
||||
baseUrl: 'http://example.com',
|
||||
pattern: '/npm/v/:packageName',
|
||||
namedParams: { packageName: 'gh-badges' },
|
||||
style: 'flat-square',
|
||||
longCache: true,
|
||||
}).expect(
|
||||
'http://example.com/npm/v/gh-badges?cacheSeconds=2592000&style=flat-square'
|
||||
)
|
||||
})
|
||||
|
||||
test(encodeField, () => {
|
||||
given('foo').expect('foo')
|
||||
given('').expect('')
|
||||
|
||||
@@ -16,15 +16,16 @@ import toArray from './to-array.js'
|
||||
//
|
||||
// Logos are resolved in this manner:
|
||||
//
|
||||
// 1. When `?logo=` contains a named logo or the name of one of the Shields
|
||||
// logos or contains base64-encoded SVG, that logo is used. When a
|
||||
// `&logoColor=` is specified, that color is used (except for the
|
||||
// base64-encoded logos). Otherwise the default color is used. If the color
|
||||
// is specified for a multicolor Shield logo, the named logo will be used and
|
||||
// colored. The appearance of the logo can be customized using `logoWidth`,
|
||||
// and in the case of the popout badge, `logoPosition`. When `?logo=` is
|
||||
// specified, any logo-related parameters specified dynamically by the
|
||||
// service, or by default in the service, are ignored.
|
||||
// 1. When `?logo=` contains the name of one of the Shields logos, or contains
|
||||
// base64-encoded SVG, that logo is used. In the case of a named logo, when
|
||||
// a `&logoColor=` is specified, that color is used. Otherwise the default
|
||||
// color is used. `logoColor` will not be applied to a custom
|
||||
// (base64-encoded) logo; if a custom color is desired the logo should be
|
||||
// recolored prior to making the request. The appearance of the logo can be
|
||||
// customized using `logoWidth`, and in the case of the popout badge,
|
||||
// `logoPosition`. When `?logo=` is specified, any logo-related parameters
|
||||
// specified dynamically by the service, or by default in the service, are
|
||||
// ignored.
|
||||
// 2. The second precedence is the dynamic logo returned by a service. This is
|
||||
// used only by the Endpoint badge. The `logoColor` can be overridden by the
|
||||
// query string.
|
||||
|
||||
@@ -153,18 +153,10 @@ describe('coalesceBadge', function () {
|
||||
).and.not.to.be.empty
|
||||
})
|
||||
|
||||
it('applies the named monochrome logo with color', function () {
|
||||
expect(
|
||||
coalesceBadge({}, { namedLogo: 'dependabot', logoColor: 'blue' }, {})
|
||||
.logo
|
||||
).to.equal(getShieldsIcon({ name: 'dependabot', color: 'blue' })).and.not
|
||||
.to.be.empty
|
||||
})
|
||||
|
||||
it('applies the named multicolored logo with color', function () {
|
||||
it('applies the named logo with color', function () {
|
||||
expect(
|
||||
coalesceBadge({}, { namedLogo: 'npm', logoColor: 'blue' }, {}).logo
|
||||
).to.equal(getSimpleIcon({ name: 'npm', color: 'blue' })).and.not.to.be
|
||||
).to.equal(getShieldsIcon({ name: 'npm', color: 'blue' })).and.not.to.be
|
||||
.empty
|
||||
})
|
||||
|
||||
@@ -174,25 +166,15 @@ describe('coalesceBadge', function () {
|
||||
).to.equal(getShieldsIcon({ name: 'npm' })).and.not.be.empty
|
||||
})
|
||||
|
||||
it('overrides the monochrome logo with a color', function () {
|
||||
expect(
|
||||
coalesceBadge(
|
||||
{ logo: 'dependabot', logoColor: 'blue' },
|
||||
{ namedLogo: 'appveyor' },
|
||||
{}
|
||||
).logo
|
||||
).to.equal(getShieldsIcon({ name: 'dependabot', color: 'blue' })).and.not
|
||||
.be.empty
|
||||
})
|
||||
|
||||
it('overrides multicolored logo with a color', function () {
|
||||
it('overrides the logo with a color', function () {
|
||||
expect(
|
||||
coalesceBadge(
|
||||
{ logo: 'npm', logoColor: 'blue' },
|
||||
{ namedLogo: 'appveyor' },
|
||||
{}
|
||||
).logo
|
||||
).to.equal(getSimpleIcon({ name: 'npm', color: 'blue' })).and.not.be.empty
|
||||
).to.equal(getShieldsIcon({ name: 'npm', color: 'blue' })).and.not.be
|
||||
.empty
|
||||
})
|
||||
|
||||
it("when the logo is overridden, it ignores the service's logo color, position, and width", function () {
|
||||
@@ -210,25 +192,15 @@ describe('coalesceBadge', function () {
|
||||
).to.equal(getShieldsIcon({ name: 'npm' })).and.not.be.empty
|
||||
})
|
||||
|
||||
it("overrides the service monochome logo's color", function () {
|
||||
expect(
|
||||
coalesceBadge(
|
||||
{ logoColor: 'blue' },
|
||||
{ namedLogo: 'dependabot', logoColor: 'red' },
|
||||
{}
|
||||
).logo
|
||||
).to.equal(getShieldsIcon({ name: 'dependabot', color: 'blue' })).and.not
|
||||
.be.empty
|
||||
})
|
||||
|
||||
it("overrides the service multicolored logo's color", function () {
|
||||
it("overrides the service logo's color", function () {
|
||||
expect(
|
||||
coalesceBadge(
|
||||
{ logoColor: 'blue' },
|
||||
{ namedLogo: 'npm', logoColor: 'red' },
|
||||
{}
|
||||
).logo
|
||||
).to.equal(getSimpleIcon({ name: 'npm', color: 'blue' })).and.not.be.empty
|
||||
).to.equal(getShieldsIcon({ name: 'npm', color: 'blue' })).and.not.be
|
||||
.empty
|
||||
})
|
||||
|
||||
// https://github.com/badges/shields/issues/2998
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import originalJoi from 'joi'
|
||||
import makeBadge from '../../badge-maker/lib/make-badge.js'
|
||||
import GithubConstellation from '../../services/github/github-constellation.js'
|
||||
import LibrariesIoConstellation from '../../services/librariesio/librariesio-constellation.js'
|
||||
import { setRoutes } from '../../services/suggest.js'
|
||||
import { loadServiceClasses } from '../base-service/loader.js'
|
||||
import { makeSend } from '../base-service/legacy-result-sender.js'
|
||||
import { handleRequest } from '../base-service/legacy-request-handler.js'
|
||||
@@ -112,9 +113,6 @@ const publicConfigSchema = Joi.object({
|
||||
redirectUrl: optionalUrl,
|
||||
rasterUrl: optionalUrl,
|
||||
cors: {
|
||||
// This doesn't actually do anything
|
||||
// TODO: maybe remove in future?
|
||||
// https://github.com/badges/shields/pull/8311#discussion_r945337530
|
||||
allowedOrigin: Joi.array().items(optionalUrl).required(),
|
||||
},
|
||||
services: Joi.object({
|
||||
@@ -171,8 +169,6 @@ const privateConfigSchema = Joi.object({
|
||||
jenkins_pass: Joi.string(),
|
||||
jira_user: Joi.string(),
|
||||
jira_pass: Joi.string(),
|
||||
bitbucket_username: Joi.string(),
|
||||
bitbucket_password: Joi.string(),
|
||||
bitbucket_server_username: Joi.string(),
|
||||
bitbucket_server_password: Joi.string(),
|
||||
librariesio_tokens: Joi.arrayFromString().items(Joi.string()),
|
||||
@@ -490,6 +486,7 @@ class Server {
|
||||
const {
|
||||
bind: { port, address: hostname },
|
||||
ssl: { isSecure: secure, cert, key },
|
||||
cors: { allowedOrigin },
|
||||
requireCloudflare,
|
||||
} = this.config.public
|
||||
|
||||
@@ -522,6 +519,9 @@ class Server {
|
||||
}
|
||||
}
|
||||
|
||||
const { apiProvider: githubApiProvider } = this.githubConstellation
|
||||
setRoutes(allowedOrigin, githubApiProvider, camp)
|
||||
|
||||
// https://github.com/badges/shields/issues/3273
|
||||
camp.handle((req, res, next) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*')
|
||||
|
||||
@@ -60,14 +60,12 @@ describe('The server', function () {
|
||||
})
|
||||
|
||||
it('should serve badges with custom maxAge', async function () {
|
||||
const { headers } = await got(`${baseUrl}badge/foo-bar-blue`)
|
||||
expect(headers['cache-control']).to.equal('max-age=86400, s-maxage=86400')
|
||||
const { headers } = await got(`${baseUrl}npm/l/express`)
|
||||
expect(headers['cache-control']).to.equal('max-age=3600, s-maxage=3600')
|
||||
})
|
||||
|
||||
it('should return cors header for the request', async function () {
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}badge/foo-bar-blue.svg`
|
||||
)
|
||||
const { statusCode, headers } = await got(`${baseUrl}npm/v/express.svg`)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(headers['access-control-allow-origin']).to.equal('*')
|
||||
})
|
||||
@@ -86,15 +84,12 @@ describe('The server', function () {
|
||||
})
|
||||
|
||||
it('should redirect modern PNG badges as configured', async function () {
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}badge/foo-bar-blue.png`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
)
|
||||
const { statusCode, headers } = await got(`${baseUrl}npm/v/express.png`, {
|
||||
followRedirect: false,
|
||||
})
|
||||
expect(statusCode).to.equal(301)
|
||||
expect(headers.location).to.equal(
|
||||
'http://raster.example.test/badge/foo-bar-blue.png'
|
||||
'http://raster.example.test/npm/v/express.png'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -202,12 +197,9 @@ describe('The server', function () {
|
||||
})
|
||||
|
||||
it('should return the 410 badge for obsolete formats', async function () {
|
||||
const { statusCode, body } = await got(
|
||||
`${baseUrl}badge/foo-bar-blue.jpg`,
|
||||
{
|
||||
throwHttpErrors: false,
|
||||
}
|
||||
)
|
||||
const { statusCode, body } = await got(`${baseUrl}npm/v/express.jpg`, {
|
||||
throwHttpErrors: false,
|
||||
})
|
||||
// TODO It would be nice if this were 404 or 410.
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body)
|
||||
@@ -215,6 +207,12 @@ describe('The server', function () {
|
||||
.and.to.include('410')
|
||||
.and.to.include('jpg no longer available')
|
||||
})
|
||||
|
||||
it('should return cors header for the request', async function () {
|
||||
const { statusCode, headers } = await got(`${baseUrl}npm/v/express.svg`)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(headers['access-control-allow-origin']).to.equal('*')
|
||||
})
|
||||
})
|
||||
|
||||
context('`requireCloudflare` is enabled', function () {
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
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"]'
|
||||
const SEARCH_INPUT = 'input[placeholder="search / project URL"]'
|
||||
|
||||
function expectBadgeExample(title, previewUrl, pattern) {
|
||||
cy.contains('tr', `${title}:`).find('code').should('have.text', pattern)
|
||||
@@ -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',
|
||||
@@ -36,22 +27,42 @@ describe('Main page', function () {
|
||||
)
|
||||
})
|
||||
|
||||
it('Customizate badges', function () {
|
||||
visitAndWait('/')
|
||||
it('Suggest badges', function () {
|
||||
const badgeUrl = `${backendUrl}/github/issues/badges/shields`
|
||||
cy.visit('/')
|
||||
|
||||
cy.get(SEARCH_INPUT).type('issues')
|
||||
cy.get(SEARCH_INPUT).type('https://github.com/badges/shields')
|
||||
cy.contains('Suggest badges').click()
|
||||
|
||||
cy.contains('/github/issues/:user/:repo').click()
|
||||
expectBadgeExample('GitHub issues', badgeUrl, badgeUrl)
|
||||
})
|
||||
|
||||
it('Customization form is filled with suggested badge details', function () {
|
||||
const badgeUrl = `${backendUrl}/github/issues/badges/shields`
|
||||
cy.visit('/')
|
||||
cy.get(SEARCH_INPUT).type('https://github.com/badges/shields')
|
||||
cy.contains('Suggest badges').click()
|
||||
|
||||
cy.contains(badgeUrl).click()
|
||||
|
||||
cy.get('input[name="user"]').should('have.value', 'badges')
|
||||
cy.get('input[name="repo"]').should('have.value', 'shields')
|
||||
})
|
||||
|
||||
it('Customizate suggested badge', function () {
|
||||
const badgeUrl = `${backendUrl}/github/issues/badges/shields`
|
||||
cy.visit('/')
|
||||
cy.get(SEARCH_INPUT).type('https://github.com/badges/shields')
|
||||
cy.contains('Suggest badges').click()
|
||||
cy.contains(badgeUrl).click()
|
||||
|
||||
cy.get('input[name="user"]').type('badges')
|
||||
cy.get('input[name="repo"]').type('shields')
|
||||
cy.get('table input[name="color"]').type('orange')
|
||||
|
||||
cy.get(`img[src='${backendUrl}/github/issues/badges/shields?color=orange']`)
|
||||
})
|
||||
|
||||
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 => {
|
||||
|
||||
@@ -114,7 +114,7 @@ if (allFiles.length > 100) {
|
||||
if (diff.includes('authHelper') && !secretsDocs.modified) {
|
||||
warn(
|
||||
[
|
||||
':books: Remember to ensure any changes to `config.private` ',
|
||||
`:books: Remember to ensure any changes to \`config.private\` `,
|
||||
`in \`${file}\` are reflected in the [server secrets documentation]`,
|
||||
'(https://github.com/badges/shields/blob/master/doc/server-secrets.md)',
|
||||
].join('')
|
||||
|
||||
@@ -20,6 +20,8 @@ The Shields codebase is divided into several parts:
|
||||
1. `*.js` in the root of [`services`][services]
|
||||
7. The services themselves (about 80% of the code)
|
||||
1. `*.js` in the folders of [`services`][services]
|
||||
8. The badge suggestion endpoint (Note: it's tested as if it’s a service.)
|
||||
1. [`lib/suggest.js`][suggest]
|
||||
|
||||
[frontend]: https://github.com/badges/shields/tree/master/frontend
|
||||
[badge-maker]: https://github.com/badges/shields/tree/master/badge-maker
|
||||
@@ -27,6 +29,7 @@ The Shields codebase is divided into several parts:
|
||||
[server]: https://github.com/badges/shields/tree/master/core/server
|
||||
[token-pooling]: https://github.com/badges/shields/tree/master/core/token-pooling
|
||||
[services]: https://github.com/badges/shields/tree/master/services
|
||||
[suggest]: https://github.com/badges/shields/tree/master/lib/suggest.js
|
||||
|
||||
The tests are also divided into several parts:
|
||||
|
||||
@@ -55,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.
|
||||
|
||||
@@ -92,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`.
|
||||
|
||||
@@ -153,6 +153,15 @@ Then copy the contents of the `build/` folder to your static hosting / CDN.
|
||||
|
||||
There are also a couple settings you should configure on the server.
|
||||
|
||||
If you want to use server suggestions, you should also set `ALLOWED_ORIGIN`:
|
||||
|
||||
```sh
|
||||
ALLOWED_ORIGIN=http://my-custom-shields.s3.amazonaws.com,https://my-custom-shields.s3.amazonaws.com
|
||||
```
|
||||
|
||||
This should be a comma-separated list of allowed origin headers. They should
|
||||
not have paths or trailing slashes.
|
||||
|
||||
To help out users, you can make the Shields server redirect the server root.
|
||||
Set the `REDIRECT_URI` environment variable:
|
||||
|
||||
|
||||
75
doc/service-integrations.md
Normal file
75
doc/service-integrations.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Integration with upstream services
|
||||
|
||||
## Overview
|
||||
|
||||
In a nutshell, the Shields Badge Server handles the responsibilities of accepting requests for badges, and then serving those badges back to users.
|
||||
|
||||
A grossly oversimplified visualization would probably look something like this:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor User
|
||||
participant B as Badge Server
|
||||
participant P as Data Provider
|
||||
User->>+B: I'd like a badge for my project please!
|
||||
B->>+P: Get data for project
|
||||
P-->>-B: Data
|
||||
B->>B: Make badge with data
|
||||
B-->>-User: Here's your badge!
|
||||
```
|
||||
|
||||
Shields is not a system of record (we're not the package registry, pipeline tool, etc.) so when Shields receives a request for a badge, the badge server will first have to reach out to the system of record in order to get the data points it needs to create your badge.
|
||||
|
||||
For example, if you ask Shields for a build status badge for your CircleCI pipeline, then Shields has to reach out to CircleCI to figure out what the status of your pipeline is (CircleCI would be the "Data Provider" actor in the above diagram). Similarly if you want a badge that shows the count of downloads of your npm package, Shields has to reach out to the npm.js registry.
|
||||
|
||||
That covers the gist, but the actual story is a bit more involved and complicated than that of course. There's a number of other components along the way, ranging from the browser on your local machine to extra services and actors we have deployed as part of the Shields.io runtime ecosystem which help ensure we can provide a stable and reliable deployment of the badge server-as-a-service. Additionally, badges rendered in GitHub have some additional factors at play that impose some additional constraints, detailed in the next section.
|
||||
|
||||
### GitHub Badge Rendering
|
||||
|
||||
A common usage pattern for badges is to embed them in your project's README files so relevant information is conveyed to the project's users. This means badges are often utilized and rendered in source control management platforms, like GitHub.
|
||||
|
||||
[GitHub utilizes a proxy service, called camo][camo], for handling the images that you see when you browse project pages on github.com, and this is utilized for badges too (both svg and png formats). GitHub does this for a number of reasons, including to anonymize requests and protect your privacy. However, this also requires the upstream images (including badges) to be returned quickly in order for those images to show up on your screen, with a rough ceiling of 3-4 seconds. If the upstream image provider is too slow to respond, then camo will timeout and the image won't be displayed.
|
||||
|
||||
This imposes constraints on Shields, as we need to ensure that the badge server completes the entire request/response workflow and returns the final badge within a few seconds.
|
||||
|
||||
[camo]: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-anonymized-urls
|
||||
|
||||
## Interaction patterns
|
||||
|
||||
Because of the time limits around the full badge flow discussed above, the badge server is somewhat constrained in terms of patterns it can employ to fetch data from the upstream provider. When we incorporate new badges we typically need to ensure when we receive a badge request and need to get data to serve that badge request, that we can get that data by making a single call to the upstream provider to get the data points needed for the badge.
|
||||
|
||||
The nature of the call Shields makes to upstream data providers is obviously dependent upon the nature of the upstream endpoints. The overwhelming majority of the time this occurs via a GET request, though there are a few occasions where the upstream endpoint requires a POST in order for us to retrieve data, and a few others where we issue a HEAD request because the data points we need for the badge actually reside in the response headers.
|
||||
|
||||
There are a couple other exceptions, but as a general pattern we strive to integrate with services via a stateless, single call manner.
|
||||
|
||||
### Authentication
|
||||
|
||||
Shields typically integrates with upstream data providers anonymously, largely because the data targets we need are anonymously available (open source packages, repositories, pipelines, etc.)
|
||||
|
||||
The badge server can be configured to make authenticated requests for certain supported services. This exists so that users who are [self-hosting] their own instance of the badge server can get badges for their private content, and also as part of agreements we've made with certain upstream data providers for the main Shields.io deployment.
|
||||
|
||||
[self-hosting]: ./self-hosting.md
|
||||
|
||||
### Rate Limits
|
||||
|
||||
Many upstream data providers employ common techniques to protect the availability and integrity of their service, and one common technique is [rate limiting].
|
||||
|
||||
Typically, when clients/consumers of rate limited endpoints will employ client-side techniques to avoid running afoul of those limits and potentially getting blocked or having their requests throttled. Unfortunately, those techniques aren't really viable for the Shields.io environment due to the workflow and constraints discussed above.
|
||||
|
||||
As such, we instead try to ensure that Shields.io never makes more calls to an upstream provider than their rate limits allow.
|
||||
|
||||
In cases where Shields.io may run close to or exceed those limits, we typically consider:
|
||||
|
||||
- increasing the cache periods we set (to reduce the number of badge requests we receive)
|
||||
- collaborating with the vendor or maintainers of the upstream data provider to explore options for an increased rate limit for Shields.io
|
||||
- decline to provide badges for that upstream data provider
|
||||
|
||||
[rate limiting]: https://en.wikipedia.org/wiki/Rate_limiting
|
||||
|
||||
### Denial of Service
|
||||
|
||||
coming soon...
|
||||
|
||||
### Considerations for new upstream integrations
|
||||
|
||||
coming soon...
|
||||
@@ -67,7 +67,7 @@ t.create('Build status')
|
||||
- All badges on shields can be requested in a number of formats. As well as calling https://img.shields.io/wercker/build/wercker/go-wercker-api.svg to generate  we can also call https://img.shields.io/wercker/build/wercker/go-wercker-api.json to request the same content as JSON. When writing service tests, we request the badge in JSON format so it is easier to make assertions about the content.
|
||||
- We don't need to explicitly call `/wercker/build/wercker/go-wercker-api.json` here, only `/build/wercker/go-wercker-api.json`. When we create a tester object with `createServiceTester()` the URL base defined in our service class (in this case `/wercker`) is used as the base URL for any requests made by the tester object.
|
||||
3. `expectBadge()` is a helper function which accepts either a string literal, a [RegExp][] or a [Joi][] schema for the different fields.
|
||||
Joi is a validation library that is 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"`.
|
||||
|
||||
@@ -2,11 +2,13 @@ import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import {
|
||||
badgeUrlFromPath,
|
||||
badgeUrlFromPattern,
|
||||
staticBadgeUrl,
|
||||
} from '../../core/badge-urls/make-badge-url'
|
||||
import { removeRegexpFromPattern } from '../lib/pattern-helpers'
|
||||
import {
|
||||
Example as ExampleData,
|
||||
Suggestion,
|
||||
RenderableExample,
|
||||
} from '../lib/service-definitions'
|
||||
import { Badge } from './common'
|
||||
@@ -34,34 +36,49 @@ function Example({
|
||||
baseUrl,
|
||||
onClick,
|
||||
exampleData,
|
||||
isBadgeSuggestion,
|
||||
}: {
|
||||
baseUrl?: string
|
||||
onClick: (example: RenderableExample) => void
|
||||
onClick: (example: RenderableExample, isSuggestion: boolean) => void
|
||||
exampleData: RenderableExample
|
||||
isBadgeSuggestion: boolean
|
||||
}): JSX.Element {
|
||||
const handleClick = React.useCallback(
|
||||
function (): void {
|
||||
onClick(exampleData)
|
||||
onClick(exampleData, isBadgeSuggestion)
|
||||
},
|
||||
[exampleData, onClick]
|
||||
[exampleData, isBadgeSuggestion, onClick]
|
||||
)
|
||||
|
||||
const {
|
||||
example: { pattern, queryParams },
|
||||
preview: { label, message, color, style, namedLogo },
|
||||
} = exampleData as ExampleData
|
||||
const previewUrl = staticBadgeUrl({
|
||||
baseUrl,
|
||||
label: label || '',
|
||||
message,
|
||||
color,
|
||||
style,
|
||||
namedLogo,
|
||||
})
|
||||
const exampleUrl = badgeUrlFromPath({
|
||||
path: removeRegexpFromPattern(pattern),
|
||||
queryParams,
|
||||
})
|
||||
let exampleUrl, previewUrl
|
||||
if (isBadgeSuggestion) {
|
||||
const {
|
||||
example: { pattern, namedParams, queryParams },
|
||||
} = exampleData as Suggestion
|
||||
exampleUrl = previewUrl = badgeUrlFromPattern({
|
||||
baseUrl,
|
||||
pattern,
|
||||
namedParams,
|
||||
queryParams,
|
||||
})
|
||||
} else {
|
||||
const {
|
||||
example: { pattern, queryParams },
|
||||
preview: { label, message, color, style, namedLogo },
|
||||
} = exampleData as ExampleData
|
||||
previewUrl = staticBadgeUrl({
|
||||
baseUrl,
|
||||
label: label || '',
|
||||
message,
|
||||
color,
|
||||
style,
|
||||
namedLogo,
|
||||
})
|
||||
exampleUrl = badgeUrlFromPath({
|
||||
path: removeRegexpFromPattern(pattern),
|
||||
queryParams,
|
||||
})
|
||||
}
|
||||
|
||||
const { title } = exampleData
|
||||
return (
|
||||
@@ -84,12 +101,14 @@ function Example({
|
||||
|
||||
export function BadgeExamples({
|
||||
examples,
|
||||
areBadgeSuggestions,
|
||||
baseUrl,
|
||||
onClick,
|
||||
}: {
|
||||
examples: RenderableExample[]
|
||||
areBadgeSuggestions: boolean
|
||||
baseUrl?: string
|
||||
onClick: (exampleData: RenderableExample) => void
|
||||
onClick: (exampleData: RenderableExample, isSuggestion: boolean) => void
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<ExampleTable>
|
||||
@@ -98,6 +117,7 @@ export function BadgeExamples({
|
||||
<Example
|
||||
baseUrl={baseUrl}
|
||||
exampleData={exampleData}
|
||||
isBadgeSuggestion={areBadgeSuggestions}
|
||||
key={`${exampleData.title} ${exampleData.example.pattern}`}
|
||||
onClick={onClick}
|
||||
/>
|
||||
|
||||
@@ -18,6 +18,8 @@ export default function Customizer({
|
||||
exampleNamedParams,
|
||||
exampleQueryParams,
|
||||
initialStyle,
|
||||
isPrefilled,
|
||||
link = '',
|
||||
}: {
|
||||
baseUrl: string
|
||||
title: string
|
||||
@@ -25,6 +27,8 @@ export default function Customizer({
|
||||
exampleNamedParams: { [k: string]: string }
|
||||
exampleQueryParams: { [k: string]: string }
|
||||
initialStyle?: string
|
||||
isPrefilled: boolean
|
||||
link?: string
|
||||
}): JSX.Element {
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/35572
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/28884#issuecomment-471341041
|
||||
@@ -71,6 +75,7 @@ export default function Customizer({
|
||||
const builtBadgeUrl = generateBuiltBadgeUrl()
|
||||
const markup = generateMarkup({
|
||||
badgeUrl: builtBadgeUrl,
|
||||
link,
|
||||
title,
|
||||
markupFormat,
|
||||
})
|
||||
@@ -88,7 +93,7 @@ export default function Customizer({
|
||||
indicatorRef.current.trigger()
|
||||
}
|
||||
},
|
||||
[generateBuiltBadgeUrl, title, setMessage, setMarkup]
|
||||
[generateBuiltBadgeUrl, link, title, setMessage, setMarkup]
|
||||
)
|
||||
|
||||
function renderMarkupAndLivePreview(): JSX.Element {
|
||||
@@ -142,6 +147,7 @@ export default function Customizer({
|
||||
<form action="">
|
||||
<PathBuilder
|
||||
exampleParams={exampleNamedParams}
|
||||
isPrefilled={isPrefilled}
|
||||
onChange={handlePathChange}
|
||||
pattern={pattern}
|
||||
/>
|
||||
|
||||
@@ -112,6 +112,7 @@ export default function PathBuilder({
|
||||
pattern,
|
||||
exampleParams,
|
||||
onChange,
|
||||
isPrefilled,
|
||||
}: {
|
||||
pattern: string
|
||||
exampleParams: { [k: string]: string }
|
||||
@@ -122,19 +123,22 @@ export default function PathBuilder({
|
||||
path: string
|
||||
isComplete: boolean
|
||||
}) => void
|
||||
isPrefilled: boolean
|
||||
}): JSX.Element {
|
||||
const [tokens] = useState(() => parse(pattern))
|
||||
const [namedParams, setNamedParams] = useState(() =>
|
||||
// `pathToRegexp.parse()` returns a mixed array of strings for literals
|
||||
// and objects for parameters. Filter out the literals and work with the
|
||||
// objects.
|
||||
tokens
|
||||
.filter(t => typeof t !== 'string')
|
||||
.map(t => t as Key)
|
||||
.reduce((accum, { name }) => {
|
||||
accum[name] = ''
|
||||
return accum
|
||||
}, {} as { [k: string]: string })
|
||||
isPrefilled
|
||||
? exampleParams
|
||||
: // `pathToRegexp.parse()` returns a mixed array of strings for literals
|
||||
// and objects for parameters. Filter out the literals and work with the
|
||||
// objects.
|
||||
tokens
|
||||
.filter(t => typeof t !== 'string')
|
||||
.map(t => t as Key)
|
||||
.reduce((accum, { name }) => {
|
||||
accum[name] = ''
|
||||
return accum
|
||||
}, {} as { [k: string]: string })
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -191,11 +195,11 @@ export default function PathBuilder({
|
||||
onChange={handleTokenChange}
|
||||
value={value}
|
||||
>
|
||||
<option key="empty" value="">
|
||||
<option disabled={isPrefilled} key="empty" value="">
|
||||
{' '}
|
||||
</option>
|
||||
{options.map(option => (
|
||||
<option key={option} value={option}>
|
||||
<option disabled={isPrefilled} key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
@@ -204,6 +208,7 @@ export default function PathBuilder({
|
||||
} else {
|
||||
return (
|
||||
<NamedParamInput
|
||||
disabled={isPrefilled}
|
||||
name={name}
|
||||
onChange={handleTokenChange}
|
||||
type="text"
|
||||
@@ -234,9 +239,11 @@ export default function PathBuilder({
|
||||
{optional ? <BuilderLabel>(optional)</BuilderLabel> : null}
|
||||
</NamedParamLabelContainer>
|
||||
{renderNamedParamInput(token)}
|
||||
<NamedParamCaption>
|
||||
{namedParamIndex === 0 ? `e.g. ${exampleValue}` : exampleValue}
|
||||
</NamedParamCaption>
|
||||
{!isPrefilled && (
|
||||
<NamedParamCaption>
|
||||
{namedParamIndex === 0 ? `e.g. ${exampleValue}` : exampleValue}
|
||||
</NamedParamCaption>
|
||||
)}
|
||||
</PathBuilderColumn>
|
||||
</React.Fragment>
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ import ServiceDefinitionSetHelper from '../lib/service-definitions/service-defin
|
||||
import { getBaseUrl } from '../constants'
|
||||
import Meta from './meta'
|
||||
import Header from './header'
|
||||
import Search from './search'
|
||||
import SuggestionAndSearch from './suggestion-and-search'
|
||||
import DonateBox from './donate'
|
||||
import { MarkupModal } from './markup-modal'
|
||||
import Usage from './usage'
|
||||
@@ -49,6 +49,8 @@ export default function Main({
|
||||
[k: string]: ServiceDefinition[]
|
||||
}>()
|
||||
const [selectedExample, setSelectedExample] = useState<RenderableExample>()
|
||||
const [selectedExampleIsSuggestion, setSelectedExampleIsSuggestion] =
|
||||
useState(false)
|
||||
const searchTimeout = useRef(0)
|
||||
const baseUrl = getBaseUrl()
|
||||
|
||||
@@ -90,6 +92,14 @@ export default function Main({
|
||||
[setSearchIsInProgress, performSearch]
|
||||
)
|
||||
|
||||
const exampleClicked = React.useCallback(
|
||||
function (example: RenderableExample, isSuggestion: boolean): void {
|
||||
setSelectedExample(example)
|
||||
setSelectedExampleIsSuggestion(isSuggestion)
|
||||
},
|
||||
[setSelectedExample, setSelectedExampleIsSuggestion]
|
||||
)
|
||||
|
||||
const dismissMarkupModal = React.useCallback(
|
||||
function (): void {
|
||||
setSelectedExample(undefined)
|
||||
@@ -113,6 +123,7 @@ export default function Main({
|
||||
<div>
|
||||
<CategoryHeading category={category} />
|
||||
<BadgeExamples
|
||||
areBadgeSuggestions={false}
|
||||
baseUrl={baseUrl}
|
||||
examples={flattened}
|
||||
onClick={setSelectedExample}
|
||||
@@ -171,10 +182,15 @@ export default function Main({
|
||||
<MarkupModal
|
||||
baseUrl={baseUrl}
|
||||
example={selectedExample}
|
||||
isBadgeSuggestion={selectedExampleIsSuggestion}
|
||||
onRequestClose={dismissMarkupModal}
|
||||
/>
|
||||
<section>
|
||||
<Search queryChanged={searchQueryChanged} />
|
||||
<SuggestionAndSearch
|
||||
baseUrl={baseUrl}
|
||||
onBadgeClick={exampleClicked}
|
||||
queryChanged={searchQueryChanged}
|
||||
/>
|
||||
<DonateBox />
|
||||
</section>
|
||||
{renderMain()}
|
||||
|
||||
@@ -11,10 +11,12 @@ const ContentContainer = styled(BaseFont)`
|
||||
|
||||
export function MarkupModal({
|
||||
example,
|
||||
isBadgeSuggestion,
|
||||
baseUrl,
|
||||
onRequestClose,
|
||||
}: {
|
||||
example: RenderableExample | undefined
|
||||
isBadgeSuggestion: boolean
|
||||
baseUrl: string
|
||||
onRequestClose: () => void
|
||||
}): JSX.Element {
|
||||
@@ -27,7 +29,11 @@ export function MarkupModal({
|
||||
>
|
||||
{example !== undefined && (
|
||||
<ContentContainer>
|
||||
<MarkupModalContent baseUrl={baseUrl} example={example} />
|
||||
<MarkupModalContent
|
||||
baseUrl={baseUrl}
|
||||
example={example}
|
||||
isBadgeSuggestion={isBadgeSuggestion}
|
||||
/>
|
||||
</ContentContainer>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { Example, RenderableExample } from '../../lib/service-definitions'
|
||||
import {
|
||||
Example,
|
||||
Suggestion,
|
||||
RenderableExample,
|
||||
} from '../../lib/service-definitions'
|
||||
import { H3 } from '../common'
|
||||
import Customizer from '../customizer/customizer'
|
||||
|
||||
@@ -12,12 +16,20 @@ const Documentation = styled.div`
|
||||
|
||||
export function MarkupModalContent({
|
||||
example,
|
||||
isBadgeSuggestion,
|
||||
baseUrl,
|
||||
}: {
|
||||
example: RenderableExample
|
||||
isBadgeSuggestion: boolean
|
||||
baseUrl: string
|
||||
}): JSX.Element {
|
||||
const { documentation } = example as Example
|
||||
let documentation: { __html: string } | undefined
|
||||
let link: string | undefined
|
||||
if (isBadgeSuggestion) {
|
||||
;({ link } = example as Suggestion)
|
||||
} else {
|
||||
;({ documentation } = example as Example)
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
@@ -36,6 +48,8 @@ export function MarkupModalContent({
|
||||
exampleNamedParams={namedParams}
|
||||
exampleQueryParams={queryParams}
|
||||
initialStyle={initialStyle}
|
||||
isPrefilled={isBadgeSuggestion}
|
||||
link={link}
|
||||
pattern={pattern}
|
||||
title={title}
|
||||
/>
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import React, { useRef, ChangeEvent } from 'react'
|
||||
import debounce from 'lodash.debounce'
|
||||
import { BlockInput } from './common'
|
||||
|
||||
export default function Search({
|
||||
queryChanged,
|
||||
}: {
|
||||
queryChanged: (query: string) => void
|
||||
}): JSX.Element {
|
||||
const queryChangedDebounced = useRef(
|
||||
debounce(queryChanged, 50, { leading: true })
|
||||
)
|
||||
|
||||
const onQueryChanged = React.useCallback(
|
||||
function ({
|
||||
target: { value: query },
|
||||
}: ChangeEvent<HTMLInputElement>): void {
|
||||
queryChangedDebounced.current(query)
|
||||
},
|
||||
[queryChangedDebounced]
|
||||
)
|
||||
|
||||
// TODO: Warning: A future version of React will block javascript: URLs as a security precaution
|
||||
// how else to do this?
|
||||
return (
|
||||
<section>
|
||||
<form action="javascript:void 0" autoComplete="off">
|
||||
<BlockInput
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
onChange={onQueryChanged}
|
||||
placeholder="search"
|
||||
/>
|
||||
</form>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
133
frontend/components/suggestion-and-search.tsx
Normal file
133
frontend/components/suggestion-and-search.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import React, { useRef, useState, ChangeEvent } from 'react'
|
||||
import fetchPonyfill from 'fetch-ponyfill'
|
||||
import debounce from 'lodash.debounce'
|
||||
import { RenderableExample } from '../lib/service-definitions'
|
||||
import { BadgeExamples } from './badge-examples'
|
||||
import { BlockInput } from './common'
|
||||
|
||||
interface SuggestionItem {
|
||||
title: string
|
||||
link: string
|
||||
example: {
|
||||
pattern: string
|
||||
namedParams: { [k: string]: string }
|
||||
queryParams?: { [k: string]: string }
|
||||
}
|
||||
preview:
|
||||
| {
|
||||
style?: string
|
||||
}
|
||||
| undefined
|
||||
}
|
||||
|
||||
interface SuggestionResponse {
|
||||
suggestions: SuggestionItem[]
|
||||
}
|
||||
|
||||
export default function SuggestionAndSearch({
|
||||
queryChanged,
|
||||
onBadgeClick,
|
||||
baseUrl,
|
||||
}: {
|
||||
queryChanged: (query: string) => void
|
||||
onBadgeClick: (example: RenderableExample, isSuggestion: boolean) => void
|
||||
baseUrl: string
|
||||
}): JSX.Element {
|
||||
const queryChangedDebounced = useRef(
|
||||
debounce(queryChanged, 50, { leading: true })
|
||||
)
|
||||
const [isUrl, setIsUrl] = useState(false)
|
||||
const [inProgress, setInProgress] = useState(false)
|
||||
const [projectUrl, setProjectUrl] = useState<string>()
|
||||
const [suggestions, setSuggestions] = useState<SuggestionItem[]>([])
|
||||
|
||||
const onQueryChanged = React.useCallback(
|
||||
function ({
|
||||
target: { value: query },
|
||||
}: ChangeEvent<HTMLInputElement>): void {
|
||||
const isUrl = query.startsWith('https://') || query.startsWith('http://')
|
||||
setIsUrl(isUrl)
|
||||
setProjectUrl(isUrl ? query : undefined)
|
||||
|
||||
queryChangedDebounced.current(query)
|
||||
},
|
||||
[setIsUrl, setProjectUrl, queryChangedDebounced]
|
||||
)
|
||||
|
||||
const getSuggestions = React.useCallback(
|
||||
async function (): Promise<void> {
|
||||
if (!projectUrl) {
|
||||
setSuggestions([])
|
||||
return
|
||||
}
|
||||
|
||||
setInProgress(true)
|
||||
|
||||
const fetch = window.fetch || fetchPonyfill
|
||||
const res = await fetch(
|
||||
`${baseUrl}/$suggest/v1?url=${encodeURIComponent(projectUrl)}`
|
||||
)
|
||||
let suggestions = [] as SuggestionItem[]
|
||||
try {
|
||||
const json = (await res.json()) as SuggestionResponse
|
||||
// This doesn't validate the response. The default value here prevents
|
||||
// a crash if the server returns {"err":"Disallowed"}.
|
||||
suggestions = json.suggestions || []
|
||||
} catch (e) {
|
||||
suggestions = []
|
||||
}
|
||||
|
||||
setInProgress(false)
|
||||
setSuggestions(suggestions)
|
||||
},
|
||||
[setSuggestions, setInProgress, baseUrl, projectUrl]
|
||||
)
|
||||
|
||||
function renderSuggestions(): JSX.Element | null {
|
||||
if (suggestions.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const transformed = suggestions.map(
|
||||
({ title, link, example, preview }) => ({
|
||||
title,
|
||||
link,
|
||||
example: {
|
||||
...example,
|
||||
queryParams: example.queryParams || {},
|
||||
},
|
||||
preview: preview || {},
|
||||
isBadgeSuggestion: true,
|
||||
})
|
||||
)
|
||||
|
||||
return (
|
||||
<BadgeExamples
|
||||
areBadgeSuggestions
|
||||
baseUrl={baseUrl}
|
||||
examples={transformed}
|
||||
onClick={onBadgeClick}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Warning: A future version of React will block javascript: URLs as a security precaution
|
||||
// how else to do this?
|
||||
return (
|
||||
<section>
|
||||
<form action="javascript:void 0" autoComplete="off">
|
||||
<BlockInput
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
onChange={onQueryChanged}
|
||||
placeholder="search / project URL"
|
||||
/>
|
||||
<br />
|
||||
<button disabled={inProgress} hidden={!isUrl} onClick={getSuggestions}>
|
||||
Suggest badges
|
||||
</button>
|
||||
</form>
|
||||
{renderSuggestions()}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -363,9 +363,8 @@ export default function Usage({ baseUrl }: { baseUrl: string }): JSX.Element {
|
||||
documentation={
|
||||
<span>
|
||||
Set the color of the logo (hex, rgb, rgba, hsl, hsla and css
|
||||
named colors supported). Supported for named logos and Shields
|
||||
logos but not for custom logos. For multicolor Shields logos,
|
||||
the corresponding named logo will be used and colored.
|
||||
named colors supported). Supported for named logos but not for
|
||||
custom logos.
|
||||
</span>
|
||||
}
|
||||
key="logoColor"
|
||||
|
||||
@@ -18,29 +18,47 @@ test(bareLink, () => {
|
||||
})
|
||||
|
||||
test(html, () => {
|
||||
given('https://img.shields.io/badge', 'Example').expect(
|
||||
'<img alt="Example" src="https://img.shields.io/badge">'
|
||||
given(
|
||||
'https://img.shields.io/badge',
|
||||
'https://example.com/example',
|
||||
'Example'
|
||||
).expect(
|
||||
'<a href="https://example.com/example"><img alt="Example" src="https://img.shields.io/badge"></a>'
|
||||
)
|
||||
given('https://img.shields.io/badge', undefined).expect(
|
||||
given('https://img.shields.io/badge', undefined, undefined).expect(
|
||||
'<img src="https://img.shields.io/badge">'
|
||||
)
|
||||
})
|
||||
|
||||
test(markdown, () => {
|
||||
given('https://img.shields.io/badge', 'Example').expect(
|
||||
given('https://img.shields.io/badge', undefined, 'Example').expect(
|
||||
''
|
||||
)
|
||||
given('https://img.shields.io/badge', undefined).expect(
|
||||
given(
|
||||
'https://img.shields.io/badge',
|
||||
'https://example.com/example',
|
||||
'Example'
|
||||
).expect(
|
||||
'[](https://example.com/example)'
|
||||
)
|
||||
given('https://img.shields.io/badge', undefined, undefined).expect(
|
||||
''
|
||||
)
|
||||
})
|
||||
|
||||
test(reStructuredText, () => {
|
||||
given('https://img.shields.io/badge', undefined).expect(
|
||||
given('https://img.shields.io/badge', undefined, undefined).expect(
|
||||
'.. image:: https://img.shields.io/badge'
|
||||
)
|
||||
given('https://img.shields.io/badge', 'Example').expect(
|
||||
'.. image:: https://img.shields.io/badge\n :alt: Example'
|
||||
given('https://img.shields.io/badge', undefined, 'Example').expect(
|
||||
'.. 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 :alt: Example :target: https://example.com/example'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -52,21 +70,33 @@ test(renderAsciiDocAttributes, () => {
|
||||
})
|
||||
|
||||
test(asciiDoc, () => {
|
||||
given('https://img.shields.io/badge', undefined).expect(
|
||||
given('https://img.shields.io/badge', undefined, undefined).expect(
|
||||
'image:https://img.shields.io/badge[]'
|
||||
)
|
||||
given('https://img.shields.io/badge', 'Example').expect(
|
||||
given('https://img.shields.io/badge', undefined, 'Example').expect(
|
||||
'image:https://img.shields.io/badge[Example]'
|
||||
)
|
||||
given('https://img.shields.io/badge', 'Example, with comma').expect(
|
||||
'image:https://img.shields.io/badge["Example, with comma"]'
|
||||
given(
|
||||
'https://img.shields.io/badge',
|
||||
undefined,
|
||||
'Example, with comma'
|
||||
).expect('image:https://img.shields.io/badge["Example, with comma"]')
|
||||
given(
|
||||
'https://img.shields.io/badge',
|
||||
'https://example.com/example',
|
||||
'Example'
|
||||
).expect(
|
||||
'image:https://img.shields.io/badge["Example",link="https://example.com/example"]'
|
||||
)
|
||||
})
|
||||
|
||||
test(generateMarkup, () => {
|
||||
given({
|
||||
badgeUrl: 'https://img.shields.io/badge',
|
||||
link: 'https://example.com/example',
|
||||
title: 'Example',
|
||||
markupFormat: 'markdown',
|
||||
}).expect('')
|
||||
}).expect(
|
||||
'[](https://example.com/example)'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -2,20 +2,41 @@ export function bareLink(badgeUrl: string, link?: string, title = ''): string {
|
||||
return badgeUrl
|
||||
}
|
||||
|
||||
export function html(badgeUrl: string, title?: string): string {
|
||||
export function html(badgeUrl: string, link?: string, title?: string): string {
|
||||
// To be more robust, this should escape the title.
|
||||
const alt = title ? ` alt="${title}"` : ''
|
||||
return `<img${alt} src="${badgeUrl}">`
|
||||
const img = `<img${alt} src="${badgeUrl}">`
|
||||
if (link) {
|
||||
return `<a href="${link}">${img}</a>`
|
||||
} else {
|
||||
return img
|
||||
}
|
||||
}
|
||||
|
||||
export function markdown(badgeUrl: string, title?: string): string {
|
||||
return ``
|
||||
export function markdown(
|
||||
badgeUrl: string,
|
||||
link?: string,
|
||||
title?: string
|
||||
): string {
|
||||
const withoutLink = ``
|
||||
if (link) {
|
||||
return `[${withoutLink}](${link})`
|
||||
} else {
|
||||
return withoutLink
|
||||
}
|
||||
}
|
||||
|
||||
export function reStructuredText(badgeUrl: string, title?: string): string {
|
||||
export function reStructuredText(
|
||||
badgeUrl: string,
|
||||
link?: string,
|
||||
title?: string
|
||||
): string {
|
||||
let result = `.. image:: ${badgeUrl}`
|
||||
if (title) {
|
||||
result += `\n :alt: ${title}`
|
||||
result += ` :alt: ${title}`
|
||||
}
|
||||
if (link) {
|
||||
result += ` :target: ${link}`
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -70,9 +91,13 @@ export function renderAsciiDocAttributes(
|
||||
}
|
||||
}
|
||||
|
||||
export function asciiDoc(badgeUrl: string, title?: string): string {
|
||||
export function asciiDoc(
|
||||
badgeUrl: string,
|
||||
link?: string,
|
||||
title?: string
|
||||
): string {
|
||||
const positional = title ? [title] : []
|
||||
const named = {} as { [k: string]: string }
|
||||
const named = link ? { link } : ({} as { [k: string]: string })
|
||||
const attrs = renderAsciiDocAttributes(positional, named)
|
||||
return `image:${badgeUrl}${attrs}`
|
||||
}
|
||||
@@ -81,10 +106,12 @@ export type MarkupFormat = 'markdown' | 'rst' | 'asciidoc' | 'link' | 'html'
|
||||
|
||||
export function generateMarkup({
|
||||
badgeUrl,
|
||||
link,
|
||||
title,
|
||||
markupFormat,
|
||||
}: {
|
||||
badgeUrl: string
|
||||
link?: string
|
||||
title?: string
|
||||
markupFormat: MarkupFormat
|
||||
}): string {
|
||||
@@ -95,5 +122,5 @@ export function generateMarkup({
|
||||
link: bareLink,
|
||||
html,
|
||||
}[markupFormat]
|
||||
return generatorFn(badgeUrl, title)
|
||||
return generatorFn(badgeUrl, link, title)
|
||||
}
|
||||
|
||||
@@ -64,4 +64,13 @@ export function getDefinitionsForCategory(
|
||||
return byCategory[category] || []
|
||||
}
|
||||
|
||||
export type RenderableExample = Example
|
||||
export interface Suggestion {
|
||||
title: string
|
||||
link: string
|
||||
example: ExampleSignature
|
||||
preview: {
|
||||
style?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type RenderableExample = Example | Suggestion
|
||||
|
||||
@@ -210,9 +210,7 @@ export default function EndpointPage(): JSX.Element {
|
||||
<dt>logoColor</dt>
|
||||
<dd>
|
||||
Default: none. Same meaning as the query string. Can be overridden by
|
||||
the query string. Only works for named logos and Shields logos. If you
|
||||
override the color of a multicolor Shield logo, the corresponding
|
||||
named logo will be used and colored.
|
||||
the query string. Only works for named logos.
|
||||
</dd>
|
||||
<dt>logoWidth</dt>
|
||||
<dd>
|
||||
@@ -246,6 +244,7 @@ export default function EndpointPage(): JSX.Element {
|
||||
exampleQueryParams={{
|
||||
url: 'https://shields.redsparr0w.com/2473/monday',
|
||||
}}
|
||||
isPrefilled={false}
|
||||
pattern="/endpoint"
|
||||
title="Custom badge"
|
||||
/>
|
||||
|
||||
@@ -18,8 +18,8 @@ function loadSimpleIcons() {
|
||||
|
||||
icon.base64 = {
|
||||
default: svg2base64(icon.svg.replace('<svg', `<svg fill="#${hex}"`)),
|
||||
light: svg2base64(icon.svg.replace('<svg', '<svg fill="whitesmoke"')),
|
||||
dark: svg2base64(icon.svg.replace('<svg', '<svg fill="#333"')),
|
||||
light: svg2base64(icon.svg.replace('<svg', `<svg fill="whitesmoke"`)),
|
||||
dark: svg2base64(icon.svg.replace('<svg', `<svg fill="#333"`)),
|
||||
}
|
||||
|
||||
// There are a few instances where multiple icons have the same title
|
||||
|
||||
10
lib/logos.js
10
lib/logos.js
@@ -66,12 +66,8 @@ function getShieldsIcon({ name, color }) {
|
||||
|
||||
const { svg, base64, isMonochrome } = logos[name]
|
||||
const svgColor = toSvgColor(color)
|
||||
if (svgColor) {
|
||||
if (isMonochrome) {
|
||||
return svg2base64(svg.replace(/fill="(.+?)"/g, `fill="${svgColor}"`))
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
if (svgColor && isMonochrome) {
|
||||
return svg2base64(svg.replace(/fill="(.+?)"/g, `fill="${svgColor}"`))
|
||||
} else {
|
||||
return base64
|
||||
}
|
||||
@@ -89,7 +85,7 @@ function getSimpleIconStyle({ icon, style }) {
|
||||
}
|
||||
|
||||
function getSimpleIcon({ name, color, style }) {
|
||||
const key = name === 'travis' ? 'travis-ci' : name.replace(/ /g, '-')
|
||||
const key = name.replace(/ /g, '-')
|
||||
|
||||
if (!(key in simpleIcons)) {
|
||||
return undefined
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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 }
|
||||
|
||||
21224
package-lock.json
generated
21224
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
110
package.json
110
package.json
@@ -21,48 +21,47 @@
|
||||
"url": "https://github.com/badges/shields"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/lato": "^4.5.10",
|
||||
"@fontsource/lekton": "^4.5.11",
|
||||
"@fontsource/lato": "^4.5.8",
|
||||
"@fontsource/lekton": "^4.5.9",
|
||||
"@renovate/pep440": "^1.0.0",
|
||||
"@renovatebot/ruby-semver": "^1.1.7",
|
||||
"@sentry/node": "^7.21.1",
|
||||
"@sentry/node": "^7.5.1",
|
||||
"@shields_io/camp": "^18.1.1",
|
||||
"badge-maker": "file:badge-maker",
|
||||
"bytes": "^3.1.2",
|
||||
"camelcase": "^7.0.0",
|
||||
"chalk": "^5.1.2",
|
||||
"chalk": "^5.0.1",
|
||||
"check-node-version": "^4.2.1",
|
||||
"cloudflare-middleware": "^1.0.4",
|
||||
"config": "^3.3.8",
|
||||
"config": "^3.3.7",
|
||||
"cross-env": "^7.0.3",
|
||||
"dayjs": "^1.11.6",
|
||||
"decamelize": "^3.2.0",
|
||||
"emojic": "^1.1.17",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"fast-xml-parser": "^4.0.12",
|
||||
"fast-xml-parser": "^4.0.8",
|
||||
"glob": "^8.0.3",
|
||||
"global-agent": "^3.0.0",
|
||||
"got": "^12.5.3",
|
||||
"got": "^12.1.0",
|
||||
"graphql": "^15.6.1",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"ioredis": "5.2.4",
|
||||
"joi": "17.7.0",
|
||||
"ioredis": "5.1.0",
|
||||
"joi": "17.6.0",
|
||||
"joi-extension-semver": "5.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonpath": "~1.1.1",
|
||||
"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.8",
|
||||
"simple-icons": "7.20.0",
|
||||
"semver": "~7.3.7",
|
||||
"simple-icons": "7.4.0",
|
||||
"webextension-store-meta": "^1.0.5",
|
||||
"xmldom": "~0.6.0",
|
||||
"xpath": "~0.0.32"
|
||||
@@ -142,36 +141,35 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.2",
|
||||
"@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.4",
|
||||
"@types/chai": "^4.3.1",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/lodash.groupby": "^4.6.7",
|
||||
"@types/mocha": "^10.0.0",
|
||||
"@types/mocha": "^9.1.1",
|
||||
"@types/node": "^16.7.10",
|
||||
"@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.44.0",
|
||||
"@typescript-eslint/parser": "^5.42.0",
|
||||
"@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.7",
|
||||
"chai": "^4.3.6",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"chai-datetime": "^1.8.0",
|
||||
"chai-string": "^1.4.0",
|
||||
"child-process-promise": "^2.2.1",
|
||||
"clipboard-copy": "^4.0.1",
|
||||
"concurrently": "^7.6.0",
|
||||
"cypress": "^11.2.0",
|
||||
"cypress-wait-for-stable-dom": "^0.1.0",
|
||||
"danger": "^11.2.0",
|
||||
"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",
|
||||
@@ -182,68 +180,68 @@
|
||||
"eslint-plugin-chai-friendly": "^0.7.2",
|
||||
"eslint-plugin-cypress": "^2.12.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsdoc": "^39.6.4",
|
||||
"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.11",
|
||||
"eslint-plugin-react": "^7.30.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-sort-class-members": "^1.15.2",
|
||||
"eslint-plugin-sort-class-members": "^1.14.1",
|
||||
"fetch-ponyfill": "^7.1.0",
|
||||
"form-data": "^4.0.0",
|
||||
"gatsby": "4.23.1",
|
||||
"gatsby-plugin-catch-links": "^4.19.0",
|
||||
"gatsby-plugin-page-creator": "^4.24.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.24.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": "^4.0.0",
|
||||
"lint-staged": "^13.0.4",
|
||||
"jsdoc": "^3.6.10",
|
||||
"lint-staged": "^13.0.3",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.difference": "^4.5.0",
|
||||
"minimist": "^1.2.7",
|
||||
"mocha": "^10.1.0",
|
||||
"minimist": "^1.2.6",
|
||||
"mocha": "^9.2.2",
|
||||
"mocha-env-reporter": "^4.0.0",
|
||||
"mocha-junit-reporter": "^2.2.0",
|
||||
"mocha-junit-reporter": "^2.0.2",
|
||||
"mocha-yaml-loader": "^1.0.3",
|
||||
"nock": "13.2.9",
|
||||
"node-mocks-http": "^1.12.1",
|
||||
"nodemon": "^2.0.20",
|
||||
"nock": "13.2.8",
|
||||
"node-mocks-http": "^1.11.0",
|
||||
"nodemon": "^2.0.19",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"open-cli": "^7.1.0",
|
||||
"portfinder": "^1.0.32",
|
||||
"prettier": "2.8.0",
|
||||
"open-cli": "^7.0.1",
|
||||
"portfinder": "^1.0.28",
|
||||
"prettier": "2.7.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-error-overlay": "^6.0.11",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-modal": "^3.16.1",
|
||||
"react-modal": "^3.15.1",
|
||||
"react-pose": "^4.0.10",
|
||||
"react-select": "^4.3.1",
|
||||
"read-all-stdin-sync": "^1.0.5",
|
||||
"redis-server": "^1.2.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"sazerac": "^2.0.0",
|
||||
"simple-git-hooks": "^2.8.1",
|
||||
"sinon": "^14.0.2",
|
||||
"simple-git-hooks": "^2.8.0",
|
||||
"sinon": "^14.0.0",
|
||||
"sinon-chai": "^3.7.0",
|
||||
"snap-shot-it": "^7.9.6",
|
||||
"start-server-and-test": "1.14.0",
|
||||
"styled-components": "^5.3.6",
|
||||
"styled-components": "^5.3.5",
|
||||
"ts-mocha": "^10.0.0",
|
||||
"tsd": "^0.24.1",
|
||||
"typescript": "^4.9.3",
|
||||
"tsd": "^0.22.0",
|
||||
"typescript": "^4.7.4",
|
||||
"url": "^0.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.13.0",
|
||||
"node": ">=16.13.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"type": "module",
|
||||
|
||||
@@ -29,7 +29,7 @@ async function captureTimings(warmupIterations) {
|
||||
function logResults({ times, iterations, warmupIterations }) {
|
||||
if (isNaN(iterations)) {
|
||||
console.log(
|
||||
'No timings captured. Have you included console.time statements in the badge creation code path?'
|
||||
`No timings captured. Have you included console.time statements in the badge creation code path?`
|
||||
)
|
||||
} else {
|
||||
const timedIterations = iterations - warmupIterations
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
34
scripts/run_package_tests.sh
Executable file
34
scripts/run_package_tests.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
|
||||
# https://discuss.circleci.com/t/switch-nodejs-version-on-machine-executor-solved/26675/3
|
||||
|
||||
# Start off less strict to work around various nvm errors.
|
||||
set -e
|
||||
export NVM_DIR="/opt/circleci/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh"
|
||||
nvm install $NODE_VERSION
|
||||
nvm use $NODE_VERSION
|
||||
|
||||
# Stricter.
|
||||
set -euo pipefail
|
||||
node --version
|
||||
|
||||
# Install the shields.io dependencies.
|
||||
npm ci
|
||||
|
||||
# Run the package tests.
|
||||
npm run test:package
|
||||
npm run check-types:package
|
||||
|
||||
# Delete the full shields.io dependency tree
|
||||
rm -rf node_modules/
|
||||
|
||||
|
||||
# Run a smoke test (render a badge with the CLI) with only the package
|
||||
# dependencies installed.
|
||||
cd badge-maker
|
||||
|
||||
npm install # install only the package dependencies for this test
|
||||
npm link
|
||||
badge cactus grown :green @flat
|
||||
rm package-lock.json && rm -rf node_modules/ # clean up package dependencies
|
||||
@@ -1,46 +0,0 @@
|
||||
import Joi from 'joi'
|
||||
import { BaseJsonService } from '../index.js'
|
||||
|
||||
const ansibleCollectionSchema = Joi.object({
|
||||
name: Joi.string().required(),
|
||||
namespace: Joi.object({
|
||||
name: Joi.string().required(),
|
||||
}),
|
||||
}).required()
|
||||
|
||||
class AnsibleGalaxyCollectionName extends BaseJsonService {
|
||||
static category = 'other'
|
||||
static route = { base: 'ansible/collection', pattern: ':collectionId' }
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'Ansible Collection',
|
||||
namedParams: { collectionId: '278' },
|
||||
staticPreview: this.render({
|
||||
name: 'community.general',
|
||||
}),
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = { label: 'collection' }
|
||||
|
||||
static render({ name }) {
|
||||
return { message: name, color: 'blue' }
|
||||
}
|
||||
|
||||
async fetch({ collectionId }) {
|
||||
const url = `https://galaxy.ansible.com/api/v2/collections/${collectionId}/`
|
||||
return this._requestJson({
|
||||
url,
|
||||
schema: ansibleCollectionSchema,
|
||||
})
|
||||
}
|
||||
|
||||
async handle({ collectionId }) {
|
||||
const json = await this.fetch({ collectionId })
|
||||
const name = `${json.namespace.name}.${json.name}`
|
||||
return this.constructor.render({ name })
|
||||
}
|
||||
}
|
||||
|
||||
export { AnsibleGalaxyCollectionName }
|
||||
@@ -1,14 +0,0 @@
|
||||
import { ServiceTester } from '../tester.js'
|
||||
export const t = new ServiceTester({
|
||||
id: 'AnsibleCollection',
|
||||
title: 'AnsibleCollection',
|
||||
pathPrefix: '/ansible/collection',
|
||||
})
|
||||
|
||||
t.create('collection name (valid)')
|
||||
.get('/278.json')
|
||||
.expectBadge({ label: 'collection', message: 'community.general' })
|
||||
|
||||
t.create('collection name (not found)')
|
||||
.get('/000.json')
|
||||
.expectBadge({ label: 'collection', message: 'not found' })
|
||||
@@ -8,7 +8,7 @@ import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('unknown build definition')
|
||||
.get('/swellaby/opensource/99999999.json')
|
||||
.get(`/swellaby/opensource/99999999.json`)
|
||||
.expectBadge({ label: 'tests', message: 'build pipeline not found' })
|
||||
|
||||
t.create('404 latest build error response')
|
||||
@@ -51,7 +51,7 @@ t.create('no test result summary response')
|
||||
})
|
||||
|
||||
t.create('no build response')
|
||||
.get('/swellaby/opensource/174.json')
|
||||
.get(`/swellaby/opensource/174.json`)
|
||||
.expectBadge({ label: 'tests', message: 'build pipeline not found' })
|
||||
|
||||
t.create('no tests in test result summary response')
|
||||
|
||||
@@ -27,7 +27,7 @@ function pullRequestClassGenerator(raw) {
|
||||
static category = 'issue-tracking'
|
||||
static route = {
|
||||
base: `bitbucket/${routePrefix}`,
|
||||
pattern: ':user/:repo',
|
||||
pattern: `:user/:repo`,
|
||||
queryParamSchema,
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ t.create('pr-raw (not found)')
|
||||
|
||||
t.create('pr-raw (private repo)')
|
||||
.get('/pr-raw/chris48s/example-private-repo.json')
|
||||
.expectBadge({ label: 'pull requests', message: 'not found' })
|
||||
.expectBadge({ label: 'pull requests', message: 'private repo' })
|
||||
|
||||
t.create('pr (valid)').get('/pr/atlassian/python-bitbucket.json').expectBadge({
|
||||
label: 'pull requests',
|
||||
@@ -33,7 +33,7 @@ t.create('pr (not found)')
|
||||
|
||||
t.create('pr (private repo)')
|
||||
.get('/pr/chris48s/example-private-repo.json')
|
||||
.expectBadge({ label: 'pull requests', message: 'not found' })
|
||||
.expectBadge({ label: 'pull requests', message: 'private repo' })
|
||||
|
||||
t.create('pr (server)')
|
||||
.get('/pr/project/repo.json?server=https://bitbucket.mydomain.net')
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -60,7 +60,7 @@ const allStatuses = greenStatuses
|
||||
* Joi schema for validating Build Status.
|
||||
* Checks if the build status is present in the list of allowed build status.
|
||||
*
|
||||
* @type {Joi}
|
||||
* @type {object}
|
||||
*/
|
||||
const isBuildStatus = Joi.equal(...allStatuses)
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export default class CIIBestPracticesService extends BaseJsonService {
|
||||
pattern: ':metric(level|percentage|summary)/:projectId',
|
||||
}
|
||||
|
||||
static examples = [
|
||||
static exampless = [
|
||||
{
|
||||
title: 'CII Best Practices Level',
|
||||
pattern: 'level/:projectId',
|
||||
|
||||
@@ -3,32 +3,32 @@ import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('level known project')
|
||||
.get('/level/1.json')
|
||||
.get(`/level/1.json`)
|
||||
.expectBadge({
|
||||
label: 'cii',
|
||||
message: withRegex(/in progress|passing|silver|gold/),
|
||||
})
|
||||
|
||||
t.create('percentage known project')
|
||||
.get('/percentage/29.json')
|
||||
.get(`/percentage/29.json`)
|
||||
.expectBadge({
|
||||
label: 'cii',
|
||||
message: withRegex(/([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-9][0-9]|300)%/),
|
||||
})
|
||||
|
||||
t.create('summary known project')
|
||||
.get('/summary/33.json')
|
||||
.get(`/summary/33.json`)
|
||||
.expectBadge({
|
||||
label: 'cii',
|
||||
message: withRegex(/(in progress [0-9]|[1-9][0-9]%)|passing|silver|gold/),
|
||||
})
|
||||
|
||||
t.create('unknown project')
|
||||
.get('/level/abc.json')
|
||||
.get(`/level/abc.json`)
|
||||
.expectBadge({ label: 'cii', message: 'project not found' })
|
||||
|
||||
t.create('level: gold project')
|
||||
.get('/level/1.json')
|
||||
.get(`/level/1.json`)
|
||||
.intercept(nock =>
|
||||
nock('https://bestpractices.coreinfrastructure.org/projects')
|
||||
.get('/1/badge.json')
|
||||
@@ -43,7 +43,7 @@ t.create('level: gold project')
|
||||
})
|
||||
|
||||
t.create('level: silver project')
|
||||
.get('/level/34.json')
|
||||
.get(`/level/34.json`)
|
||||
.intercept(nock =>
|
||||
nock('https://bestpractices.coreinfrastructure.org/projects')
|
||||
.get('/34/badge.json')
|
||||
@@ -58,7 +58,7 @@ t.create('level: silver project')
|
||||
})
|
||||
|
||||
t.create('level: passing project')
|
||||
.get('/level/29.json')
|
||||
.get(`/level/29.json`)
|
||||
.intercept(nock =>
|
||||
nock('https://bestpractices.coreinfrastructure.org/projects')
|
||||
.get('/29/badge.json')
|
||||
@@ -73,7 +73,7 @@ t.create('level: passing project')
|
||||
})
|
||||
|
||||
t.create('level: in progress project')
|
||||
.get('/level/33.json')
|
||||
.get(`/level/33.json`)
|
||||
.intercept(nock =>
|
||||
nock('https://bestpractices.coreinfrastructure.org/projects')
|
||||
.get('/33/badge.json')
|
||||
@@ -88,7 +88,7 @@ t.create('level: in progress project')
|
||||
})
|
||||
|
||||
t.create('percentage: gold project')
|
||||
.get('/percentage/1.json')
|
||||
.get(`/percentage/1.json`)
|
||||
.intercept(nock =>
|
||||
nock('https://bestpractices.coreinfrastructure.org/projects')
|
||||
.get('/1/badge.json')
|
||||
@@ -103,7 +103,7 @@ t.create('percentage: gold project')
|
||||
})
|
||||
|
||||
t.create('percentage: silver project')
|
||||
.get('/percentage/34.json')
|
||||
.get(`/percentage/34.json`)
|
||||
.intercept(nock =>
|
||||
nock('https://bestpractices.coreinfrastructure.org/projects')
|
||||
.get('/34/badge.json')
|
||||
@@ -118,7 +118,7 @@ t.create('percentage: silver project')
|
||||
})
|
||||
|
||||
t.create('percentage: passing project')
|
||||
.get('/percentage/29.json')
|
||||
.get(`/percentage/29.json`)
|
||||
.intercept(nock =>
|
||||
nock('https://bestpractices.coreinfrastructure.org/projects')
|
||||
.get('/29/badge.json')
|
||||
@@ -133,7 +133,7 @@ t.create('percentage: passing project')
|
||||
})
|
||||
|
||||
t.create('percentage: in progress project')
|
||||
.get('/percentage/33.json')
|
||||
.get(`/percentage/33.json`)
|
||||
.intercept(nock =>
|
||||
nock('https://bestpractices.coreinfrastructure.org/projects')
|
||||
.get('/33/badge.json')
|
||||
@@ -148,7 +148,7 @@ t.create('percentage: in progress project')
|
||||
})
|
||||
|
||||
t.create('summary: gold project')
|
||||
.get('/summary/1.json')
|
||||
.get(`/summary/1.json`)
|
||||
.intercept(nock =>
|
||||
nock('https://bestpractices.coreinfrastructure.org/projects')
|
||||
.get('/1/badge.json')
|
||||
@@ -163,7 +163,7 @@ t.create('summary: gold project')
|
||||
})
|
||||
|
||||
t.create('summary: silver project')
|
||||
.get('/summary/34.json')
|
||||
.get(`/summary/34.json`)
|
||||
.intercept(nock =>
|
||||
nock('https://bestpractices.coreinfrastructure.org/projects')
|
||||
.get('/34/badge.json')
|
||||
@@ -178,7 +178,7 @@ t.create('summary: silver project')
|
||||
})
|
||||
|
||||
t.create('summary: passing project')
|
||||
.get('/summary/29.json')
|
||||
.get(`/summary/29.json`)
|
||||
.intercept(nock =>
|
||||
nock('https://bestpractices.coreinfrastructure.org/projects')
|
||||
.get('/29/badge.json')
|
||||
@@ -193,7 +193,7 @@ t.create('summary: passing project')
|
||||
})
|
||||
|
||||
t.create('summary: in progress project')
|
||||
.get('/summary/33.json')
|
||||
.get(`/summary/33.json`)
|
||||
.intercept(nock =>
|
||||
nock('https://bestpractices.coreinfrastructure.org/projects')
|
||||
.get('/33/badge.json')
|
||||
|
||||
@@ -107,7 +107,7 @@ export default class Codecov extends BaseSvgScrapingService {
|
||||
async legacyFetch({ vcsName, user, repo, branch, token }) {
|
||||
// Codecov Docs: https://docs.codecov.io/reference#section-get-a-single-repository
|
||||
const url = `https://codecov.io/api/${vcsName}/${user}/${repo}${
|
||||
branch ? `/branch/${branch}` : ''
|
||||
branch ? `/branches/${branch}` : ''
|
||||
}`
|
||||
const { buffer } = await this._request({
|
||||
url,
|
||||
|
||||
@@ -60,7 +60,7 @@ t.create('handles unauthorized private repository')
|
||||
.intercept(nock =>
|
||||
nock('https://codecov.io')
|
||||
.get('/github/codecov/private-example-python/graph/badge.svg')
|
||||
.reply(200, '<g><text x="105.5" y="14">unknown</text></g>', {
|
||||
.reply(200, `<g><text x="105.5" y="14">unknown</text></g>`, {
|
||||
'Content-Type': 'image/svg+xml',
|
||||
})
|
||||
)
|
||||
@@ -110,7 +110,7 @@ t.create('gets coverage for private repository')
|
||||
.get(
|
||||
'/gh/codecov/private-example-python/graph/badge.svg?token=a1b2c3d4e5'
|
||||
)
|
||||
.reply(200, '<g><text x="105.5" y="14">100%</text></g>', {
|
||||
.reply(200, `<g><text x="105.5" y="14">100%</text></g>`, {
|
||||
'Content-Type': 'image/svg+xml',
|
||||
})
|
||||
)
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { BaseJsonService } from '../index.js'
|
||||
|
||||
export default class BaseCoincapService extends BaseJsonService {
|
||||
static category = 'other'
|
||||
|
||||
static defaultBadgeData = { label: 'coincap' }
|
||||
|
||||
// Doc this API. From https://docs.coincap.io/
|
||||
// example: https://api.coincap.io/v2/assets/bitcoin
|
||||
|
||||
async fetch({ assetId, schema }) {
|
||||
return this._requestJson({
|
||||
schema,
|
||||
url: `https://api.coincap.io/v2/assets/${assetId}`,
|
||||
errorMessages: {
|
||||
404: 'asset not found',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export { BaseCoincapService }
|
||||
@@ -1,44 +0,0 @@
|
||||
import Joi from 'joi'
|
||||
import { floorCount } from '../color-formatters.js'
|
||||
import BaseCoincapService from './coincap-base.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
data: Joi.object({
|
||||
changePercent24Hr: Joi.string()
|
||||
.pattern(/[0-9]*\.[0-9]+/i)
|
||||
.required(),
|
||||
name: Joi.string().required(),
|
||||
}).required(),
|
||||
}).required()
|
||||
|
||||
export default class CoincapChangePercent24HrUsd extends BaseCoincapService {
|
||||
static route = { base: 'coincap/change-percent-24hr', pattern: ':assetId' }
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'Coincap (Change Percent 24Hr)',
|
||||
namedParams: { assetId: 'bitcoin' },
|
||||
staticPreview: this.render({
|
||||
asset: { name: 'bitcoin', changePercent24Hr: '2.0670573674501840"' },
|
||||
}),
|
||||
keywords: ['bitcoin', 'crypto', 'cryptocurrency'],
|
||||
},
|
||||
]
|
||||
|
||||
static percentFormat(changePercent24Hr) {
|
||||
return `${parseInt(changePercent24Hr).toFixed(2)}%`
|
||||
}
|
||||
|
||||
static render({ asset }) {
|
||||
return {
|
||||
label: `${asset.name}`.toLowerCase(),
|
||||
message: this.percentFormat(asset.changePercent24Hr),
|
||||
color: floorCount(asset.changePercent24Hr),
|
||||
}
|
||||
}
|
||||
|
||||
async handle({ assetId }) {
|
||||
const { data: asset } = await this.fetch({ assetId, schema })
|
||||
return this.constructor.render({ asset })
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { isPercentage } from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('request for existing asset with positive')
|
||||
.get('/bitcoin.json')
|
||||
.intercept(nock =>
|
||||
nock('https://api.coincap.io')
|
||||
.get('/v2/assets/bitcoin')
|
||||
.reply(200, {
|
||||
data: { changePercent24Hr: '1.4767080598737783', name: 'Bitcoin' },
|
||||
})
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'bitcoin',
|
||||
message: '1.00%',
|
||||
color: 'brightgreen',
|
||||
})
|
||||
|
||||
t.create('request for existing asset with negative')
|
||||
.get('/bitcoin.json')
|
||||
.intercept(nock =>
|
||||
nock('https://api.coincap.io')
|
||||
.get('/v2/assets/bitcoin')
|
||||
.reply(200, {
|
||||
data: { changePercent24Hr: '-1.4767080598737783', name: 'Bitcoin' },
|
||||
})
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'bitcoin',
|
||||
message: '-1.00%',
|
||||
color: 'red',
|
||||
})
|
||||
|
||||
t.create('change percent 24hr').get('/bitcoin.json').expectBadge({
|
||||
label: 'bitcoin',
|
||||
message: isPercentage,
|
||||
})
|
||||
|
||||
t.create('asset not found').get('/not-a-valid-asset.json').expectBadge({
|
||||
label: 'coincap',
|
||||
message: 'asset not found',
|
||||
})
|
||||
@@ -1,45 +0,0 @@
|
||||
import Joi from 'joi'
|
||||
import BaseCoincapService from './coincap-base.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
data: Joi.object({
|
||||
priceUsd: Joi.string()
|
||||
.pattern(/[0-9]*\.[0-9]+/i)
|
||||
.required(),
|
||||
name: Joi.string().required(),
|
||||
}).required(),
|
||||
}).required()
|
||||
|
||||
export default class CoincapPriceUsd extends BaseCoincapService {
|
||||
static route = { base: 'coincap/price-usd', pattern: ':assetId' }
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'Coincap (Price USD)',
|
||||
namedParams: { assetId: 'bitcoin' },
|
||||
staticPreview: this.render({
|
||||
asset: { name: 'bitcoin', priceUsd: '19116.0479117336250772' },
|
||||
}),
|
||||
keywords: ['bitcoin', 'crypto', 'cryptocurrency'],
|
||||
},
|
||||
]
|
||||
|
||||
static priceFormat(price) {
|
||||
return `$${parseFloat(price)
|
||||
.toFixed(2)
|
||||
.replace(/\d(?=(\d{3})+\.)/g, '$&,')}`
|
||||
}
|
||||
|
||||
static render({ asset }) {
|
||||
return {
|
||||
label: `${asset.name}`.toLowerCase(),
|
||||
message: this.priceFormat(asset.priceUsd),
|
||||
color: 'blue',
|
||||
}
|
||||
}
|
||||
|
||||
async handle({ assetId }) {
|
||||
const { data: asset } = await this.fetch({ assetId, schema })
|
||||
return this.constructor.render({ asset })
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { test, given } from 'sazerac'
|
||||
import CoincapPriceUsd from './coincap-priceusd.service.js'
|
||||
|
||||
describe('PriceUsd Format', function () {
|
||||
test(CoincapPriceUsd.priceFormat, () => {
|
||||
given('3').expect('$3.00')
|
||||
given('33').expect('$33.00')
|
||||
given('332').expect('$332.00')
|
||||
given('3324').expect('$3,324.00')
|
||||
given('332432').expect('$332,432.00')
|
||||
given('332432.2').expect('$332,432.20')
|
||||
given('332432.25').expect('$332,432.25')
|
||||
given('332432432').expect('$332,432,432.00')
|
||||
given('332432432.3432432').expect('$332,432,432.34')
|
||||
})
|
||||
})
|
||||
@@ -1,29 +0,0 @@
|
||||
import { isCurrency } from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('request for existing asset')
|
||||
.get('/bitcoin.json')
|
||||
.intercept(nock =>
|
||||
nock('https://api.coincap.io')
|
||||
.get('/v2/assets/bitcoin')
|
||||
.reply(200, {
|
||||
data: { priceUsd: '16417.7176754790740415', name: 'Bitcoin' },
|
||||
})
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'bitcoin',
|
||||
message: '$16,417.72',
|
||||
color: 'blue',
|
||||
})
|
||||
|
||||
t.create('price usd').get('/bitcoin.json').expectBadge({
|
||||
label: 'bitcoin',
|
||||
message: isCurrency,
|
||||
color: 'blue',
|
||||
})
|
||||
|
||||
t.create('asset not found').get('/not-a-valid-asset.json').expectBadge({
|
||||
label: 'coincap',
|
||||
message: 'asset not found',
|
||||
})
|
||||
@@ -1,37 +0,0 @@
|
||||
import Joi from 'joi'
|
||||
import BaseCoincapService from './coincap-base.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
data: Joi.object({
|
||||
rank: Joi.string()
|
||||
.pattern(/^[0-9]+$/)
|
||||
.required(),
|
||||
name: Joi.string().required(),
|
||||
}).required(),
|
||||
}).required()
|
||||
|
||||
export default class CoincapRank extends BaseCoincapService {
|
||||
static route = { base: 'coincap/rank', pattern: ':assetId' }
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'Coincap (Rank)',
|
||||
namedParams: { assetId: 'bitcoin' },
|
||||
staticPreview: this.render({ asset: { name: 'bitcoin', rank: '1' } }),
|
||||
keywords: ['bitcoin', 'crypto', 'cryptocurrency'],
|
||||
},
|
||||
]
|
||||
|
||||
static render({ asset }) {
|
||||
return {
|
||||
label: `${asset.name}`.toLowerCase(),
|
||||
message: asset.rank,
|
||||
color: 'blue',
|
||||
}
|
||||
}
|
||||
|
||||
async handle({ assetId }) {
|
||||
const { data: asset } = await this.fetch({ assetId, schema })
|
||||
return this.constructor.render({ asset })
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import Joi from 'joi'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('request for existing asset')
|
||||
.get('/bitcoin.json')
|
||||
.intercept(nock =>
|
||||
nock('https://api.coincap.io')
|
||||
.get('/v2/assets/bitcoin')
|
||||
.reply(200, { data: { rank: '1', name: 'Bitcoin' } })
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'bitcoin',
|
||||
message: '1',
|
||||
color: 'blue',
|
||||
})
|
||||
|
||||
t.create('rank')
|
||||
.get('/bitcoin.json')
|
||||
.expectBadge({
|
||||
label: 'bitcoin',
|
||||
message: Joi.number().integer().min(1).required(),
|
||||
color: 'blue',
|
||||
})
|
||||
|
||||
t.create('asset not found').get('/not-a-valid-asset.json').expectBadge({
|
||||
label: 'coincap',
|
||||
message: 'asset not found',
|
||||
})
|
||||
@@ -5,7 +5,7 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import dayjs from 'dayjs'
|
||||
import moment from 'moment'
|
||||
import pep440 from '@renovate/pep440'
|
||||
|
||||
/**
|
||||
@@ -23,7 +23,7 @@ function version(version) {
|
||||
if (first === 'v') {
|
||||
first = version[1]
|
||||
}
|
||||
if (first === '0' || /alpha|beta|snapshot|dev|pre|rc/i.test(version)) {
|
||||
if (first === '0' || /alpha|beta|snapshot|dev|pre/i.test(version)) {
|
||||
return 'orange'
|
||||
} else {
|
||||
return 'blue'
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,6 @@ describe('Color formatters', function () {
|
||||
given('6.0-SNAPSHOT'),
|
||||
given('1.0.1-dev'),
|
||||
given('2.1.6-prerelease'),
|
||||
given('2.1.6-RC1'),
|
||||
]).expect('orange')
|
||||
|
||||
expect(() => version(null)).to.throw(
|
||||
|
||||
@@ -2,49 +2,14 @@ import { redirector } from '../index.js'
|
||||
|
||||
export default [
|
||||
redirector({
|
||||
name: 'CoverallsGitHubRedirectWithBranch',
|
||||
name: 'CoverallsGitHubRedirect',
|
||||
category: 'coverage',
|
||||
route: {
|
||||
base: 'coveralls',
|
||||
pattern: ':user((?!github|bitbucket).*)/:repo/:branch+',
|
||||
pattern: ':user((?!github|bitbucket).*)/:repo/:branch*',
|
||||
},
|
||||
transformPath: ({ user, repo }) =>
|
||||
`/coverallsCoverage/github/${user}/${repo}`,
|
||||
transformQueryParams: ({ branch }) => ({ branch }),
|
||||
dateAdded: new Date('2022-11-10'),
|
||||
}),
|
||||
redirector({
|
||||
name: 'CoverallsGitHubRedirectWithoutBranch',
|
||||
category: 'coverage',
|
||||
route: {
|
||||
base: 'coveralls',
|
||||
pattern: ':user((?!github|bitbucket).*)/:repo',
|
||||
},
|
||||
transformPath: ({ user, repo }) =>
|
||||
`/coverallsCoverage/github/${user}/${repo}`,
|
||||
transformPath: ({ user, repo, branch }) =>
|
||||
`/coveralls/github/${user}/${repo}${branch ? `/${branch}` : ''}`,
|
||||
dateAdded: new Date('2021-02-23'),
|
||||
}),
|
||||
redirector({
|
||||
name: 'CoverallsPreGitlabRedirectWithBranch',
|
||||
category: 'coverage',
|
||||
route: {
|
||||
base: 'coveralls',
|
||||
pattern: ':vcsType(github|bitbucket)/:user/:repo/:branch+',
|
||||
},
|
||||
transformPath: ({ vcsType, user, repo }) =>
|
||||
`/coverallsCoverage/${vcsType}/${user}/${repo}`,
|
||||
transformQueryParams: ({ branch }) => ({ branch }),
|
||||
dateAdded: new Date('2022-11-10'),
|
||||
}),
|
||||
redirector({
|
||||
name: 'CoverallsPreGitlabRedirectWithoutBranch',
|
||||
category: 'coverage',
|
||||
route: {
|
||||
base: 'coveralls',
|
||||
pattern: ':vcsType(github|bitbucket)/:user/:repo',
|
||||
},
|
||||
transformPath: ({ vcsType, user, repo }) =>
|
||||
`/coverallsCoverage/${vcsType}/${user}/${repo}`,
|
||||
dateAdded: new Date('2022-11-20'),
|
||||
}),
|
||||
]
|
||||
|
||||
@@ -1,41 +1,11 @@
|
||||
import { ServiceTester } from '../tester.js'
|
||||
|
||||
export const t = new ServiceTester({
|
||||
id: 'CoverallsTestsRedirector',
|
||||
title: 'CoverallsTestsRedirector',
|
||||
id: 'CoverallsGitHubRedirect',
|
||||
title: 'Coveralls GitHub Redirector',
|
||||
pathPrefix: '/coveralls',
|
||||
})
|
||||
|
||||
t.create('Coveralls VCS type missing')
|
||||
.get('/lemurheavy/coveralls-ruby.svg')
|
||||
.expectRedirect('/coverallsCoverage/github/lemurheavy/coveralls-ruby.svg')
|
||||
|
||||
t.create('Coveralls VCS type missing + specified branch')
|
||||
.get('/jekyll/jekyll/master.svg')
|
||||
.expectRedirect('/coverallsCoverage/github/jekyll/jekyll.svg?branch=master')
|
||||
|
||||
t.create(
|
||||
'Redirect from before branch was a query param - github, with specified branch'
|
||||
)
|
||||
.get('/github/jekyll/jekyll/master.svg')
|
||||
.expectRedirect('/coverallsCoverage/github/jekyll/jekyll.svg?branch=master')
|
||||
|
||||
t.create(
|
||||
'Redirect from before branch was a query param - github, without specified branch'
|
||||
)
|
||||
.get('/github/badges/shields')
|
||||
.expectRedirect('/coverallsCoverage/github/badges/shields.svg')
|
||||
|
||||
t.create(
|
||||
'Redirect from before branch was a query param - bitbucket, with specified branch'
|
||||
)
|
||||
.get('/bitbucket/pyKLIP/pyklip/master.svg')
|
||||
.expectRedirect(
|
||||
'/coverallsCoverage/bitbucket/pyKLIP/pyklip.svg?branch=master'
|
||||
)
|
||||
|
||||
t.create(
|
||||
'Redirect from before branch was a query param - bitbucket, without specified branch'
|
||||
)
|
||||
.get('/bitbucket/pyKLIP/pyklip.svg')
|
||||
.expectRedirect('/coverallsCoverage/bitbucket/pyKLIP/pyklip.svg')
|
||||
.expectRedirect('/coveralls/github/lemurheavy/coveralls-ruby.svg')
|
||||
|
||||
@@ -6,22 +6,18 @@ const schema = Joi.object({
|
||||
covered_percent: Joi.number().min(0).max(100).required(),
|
||||
}).required()
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
branch: Joi.string(),
|
||||
}).required()
|
||||
|
||||
export default class Coveralls extends BaseJsonService {
|
||||
static category = 'coverage'
|
||||
static route = {
|
||||
base: 'coverallsCoverage',
|
||||
pattern: ':vcsType(github|bitbucket|gitlab)/:user/:repo+',
|
||||
queryParamSchema,
|
||||
base: 'coveralls',
|
||||
pattern: ':vcsType(github|bitbucket)/:user/:repo/:branch*',
|
||||
}
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'Coveralls',
|
||||
namedParams: { vcsType: 'github', user: 'jekyll', repo: 'jekyll' },
|
||||
pattern: ':vcsType(github|bitbucket)/:user/:repo',
|
||||
staticPreview: this.render({ coverage: 86 }),
|
||||
},
|
||||
{
|
||||
@@ -30,8 +26,9 @@ export default class Coveralls extends BaseJsonService {
|
||||
vcsType: 'bitbucket',
|
||||
user: 'pyKLIP',
|
||||
repo: 'pyklip',
|
||||
branch: 'master',
|
||||
},
|
||||
queryParams: { branch: 'master' },
|
||||
pattern: ':vcsType(github|bitbucket)/:user/:repo/:branch',
|
||||
staticPreview: this.render({ coverage: 96 }),
|
||||
},
|
||||
]
|
||||
@@ -72,7 +69,7 @@ export default class Coveralls extends BaseJsonService {
|
||||
})
|
||||
}
|
||||
|
||||
async handle({ vcsType, user, repo }, { branch }) {
|
||||
async handle({ vcsType, user, repo, branch }) {
|
||||
const json = await this.fetch({ vcsType, user, repo, branch })
|
||||
return this.constructor.render({ coverage: json.covered_percent })
|
||||
}
|
||||
|
||||
@@ -11,21 +11,9 @@ t.create('nonexistent project')
|
||||
.expectBadge({ label: 'coverage', message: 'repository not found' })
|
||||
|
||||
t.create('github branch coverage')
|
||||
.get('/github/lemurheavy/coveralls-ruby.json?branch=master')
|
||||
.get('/github/lemurheavy/coveralls-ruby/master.json')
|
||||
.expectBadge({ label: 'coverage', message: isIntegerPercentage })
|
||||
|
||||
t.create('bitbucket coverage')
|
||||
.get('/bitbucket/pyKLIP/pyklip.json')
|
||||
.expectBadge({ label: 'coverage', message: isIntegerPercentage })
|
||||
|
||||
t.create('bitbucket branch coverage')
|
||||
.get('/bitbucket/pyKLIP/pyklip.json?branch=master')
|
||||
.expectBadge({ label: 'coverage', message: isIntegerPercentage })
|
||||
|
||||
t.create('gitlab coverage')
|
||||
.get('/gitlab/selcouth/wsrouter.json')
|
||||
.expectBadge({ label: 'coverage', message: isIntegerPercentage })
|
||||
|
||||
t.create('gitlab branch coverage')
|
||||
.get('/gitlab/selcouth/wsrouter.json?branch=master')
|
||||
.expectBadge({ label: 'coverage', message: isIntegerPercentage })
|
||||
|
||||
@@ -1,11 +1,66 @@
|
||||
import { deprecatedService } from '../index.js'
|
||||
import Joi from 'joi'
|
||||
import { BaseJsonService } from '../index.js'
|
||||
import {
|
||||
IMPROVED_STATUS,
|
||||
NOT_FOUND_STATUS,
|
||||
REGRESSED_STATUS,
|
||||
NO_CHANGE_STATUS,
|
||||
} from './constants.js'
|
||||
|
||||
export default deprecatedService({
|
||||
category: 'analysis',
|
||||
route: {
|
||||
base: 'criterion',
|
||||
pattern: ':various*',
|
||||
},
|
||||
label: 'criterion',
|
||||
dateAdded: new Date('2022-10-07'),
|
||||
})
|
||||
const schema = Joi.string()
|
||||
.allow(IMPROVED_STATUS, REGRESSED_STATUS, NO_CHANGE_STATUS)
|
||||
.required()
|
||||
|
||||
/**
|
||||
* Criterion Badge Service
|
||||
*
|
||||
* Support and Contact:
|
||||
* - https://github.com/chmoder/api.criterion.dev
|
||||
*
|
||||
* API Documentation:
|
||||
* - https://app.swaggerhub.com/apis-docs/chmoder/Criterion.dev
|
||||
*/
|
||||
export default class Criterion extends BaseJsonService {
|
||||
static category = 'analysis'
|
||||
static route = { base: 'criterion', pattern: ':user/:repo' }
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'Criterion',
|
||||
namedParams: {
|
||||
user: 'chmoder',
|
||||
repo: 'data_vault',
|
||||
},
|
||||
staticPreview: this.render({ status: IMPROVED_STATUS }),
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = { label: 'criterion' }
|
||||
|
||||
static render({ status }) {
|
||||
let statusColor = 'lightgrey'
|
||||
|
||||
if (status === IMPROVED_STATUS) {
|
||||
statusColor = 'brightgreen'
|
||||
} else if (status === NO_CHANGE_STATUS) {
|
||||
statusColor = 'green'
|
||||
} else if (statusColor === REGRESSED_STATUS) {
|
||||
statusColor = 'red'
|
||||
}
|
||||
|
||||
return {
|
||||
message: `${status}`,
|
||||
color: statusColor,
|
||||
}
|
||||
}
|
||||
|
||||
async handle({ user, repo }) {
|
||||
const status = await this._requestJson({
|
||||
url: `https://api.criterion.dev/v1/${user}/${repo}/status`,
|
||||
errorMessages: { 404: NOT_FOUND_STATUS },
|
||||
schema,
|
||||
})
|
||||
|
||||
return this.constructor.render({ status })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
import { ServiceTester } from '../tester.js'
|
||||
import Joi from 'joi'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
import {
|
||||
IMPROVED_STATUS,
|
||||
REGRESSED_STATUS,
|
||||
NO_CHANGE_STATUS,
|
||||
NOT_FOUND_STATUS,
|
||||
} from './constants.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
export const t = new ServiceTester({
|
||||
id: 'criterion',
|
||||
title: 'Criterion',
|
||||
pathPrefix: '/criterion',
|
||||
})
|
||||
const isStatus = Joi.string()
|
||||
.allow(IMPROVED_STATUS, REGRESSED_STATUS, NOT_FOUND_STATUS, NO_CHANGE_STATUS)
|
||||
.required()
|
||||
|
||||
t.create('Criterion')
|
||||
t.create('Criterion (valid repo)')
|
||||
.get('/chmoder/credit_card.json')
|
||||
.expectBadge({ label: 'criterion', message: 'no longer available' })
|
||||
.expectBadge({ label: 'criterion', message: isStatus })
|
||||
|
||||
t.create('Criterion (not found)')
|
||||
.get('/chmoder/not-a-repo.json')
|
||||
.expectBadge({ label: 'criterion', message: NOT_FOUND_STATUS })
|
||||
|
||||
@@ -14,12 +14,12 @@ describe('Discord', function () {
|
||||
},
|
||||
}
|
||||
|
||||
const scope = nock('https://discord.com', {
|
||||
const scope = nock(`https://discord.com`, {
|
||||
// This ensures that the expected credential is actually being sent with the HTTP request.
|
||||
// Without this the request wouldn't match and the test would fail.
|
||||
reqheaders: { Authorization: 'Bot password' },
|
||||
reqheaders: { Authorization: `Bot password` },
|
||||
})
|
||||
.get('/api/v6/guilds/12345/widget.json')
|
||||
.get(`/api/v6/guilds/12345/widget.json`)
|
||||
.reply(200, {
|
||||
presence_count: 125,
|
||||
})
|
||||
|
||||
@@ -72,7 +72,7 @@ class DiscourseStatus extends DiscourseBase {
|
||||
static route = this.buildRoute('status')
|
||||
static examples = [
|
||||
{
|
||||
title: 'Discourse status',
|
||||
title: `Discourse status`,
|
||||
namedParams: {},
|
||||
queryParams: {
|
||||
server: 'https://meta.discourse.org',
|
||||
|
||||
@@ -28,12 +28,6 @@ export default class DockerCloudAutomatedBuild extends BaseJsonService {
|
||||
|
||||
async handle({ user, repo }) {
|
||||
const data = await fetchBuild(this, { user, repo })
|
||||
|
||||
if (data.objects.length === 0) {
|
||||
return this.constructor.render({
|
||||
buildSettings: [],
|
||||
})
|
||||
}
|
||||
return this.constructor.render({
|
||||
buildSettings: data.objects[0].build_settings,
|
||||
})
|
||||
|
||||
@@ -5,29 +5,13 @@ export const t = await createServiceTester()
|
||||
|
||||
const isAutomatedBuildStatus = Joi.string().valid('automated', 'manual')
|
||||
|
||||
t.create('docker cloud automated build (valid user)')
|
||||
.get('/pavics/magpie.json')
|
||||
t.create('docker cloud automated build (valid, user)')
|
||||
.get('/jrottenberg/ffmpeg.json')
|
||||
.expectBadge({
|
||||
label: 'docker build',
|
||||
message: isAutomatedBuildStatus,
|
||||
})
|
||||
|
||||
t.create('docker cloud automated build status (invalid, nonexisting user)')
|
||||
.get('/pavicsssss/magpie.json')
|
||||
.expectBadge({
|
||||
label: 'docker build',
|
||||
message: 'manual',
|
||||
})
|
||||
|
||||
t.create(
|
||||
"docker cloud build status (valid user, but the 'objects' array from the response is empty)"
|
||||
)
|
||||
.get('/pavics/weaver.json')
|
||||
.expectBadge({
|
||||
label: 'docker build',
|
||||
message: 'manual',
|
||||
})
|
||||
|
||||
t.create('docker cloud automated build (not found)')
|
||||
.get('/badges/not-a-real-repo.json')
|
||||
.intercept(nock =>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseJsonService, NotFound } from '../index.js'
|
||||
import { BaseJsonService } from '../index.js'
|
||||
import { dockerBlue, buildDockerUrl } from './docker-helpers.js'
|
||||
import { fetchBuild } from './docker-cloud-common-fetch.js'
|
||||
|
||||
@@ -31,12 +31,6 @@ export default class DockerCloudBuild extends BaseJsonService {
|
||||
|
||||
async handle({ user, repo }) {
|
||||
const data = await fetchBuild(this, { user, repo })
|
||||
|
||||
if (data.objects.length === 0) {
|
||||
throw new NotFound({
|
||||
prettyMessage: 'automated builds not set up',
|
||||
})
|
||||
}
|
||||
return this.constructor.render({ state: data.objects[0].state })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,29 +3,13 @@ import { createServiceTester } from '../tester.js'
|
||||
import { dockerBlue } from './docker-helpers.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('docker cloud build status (valid user)')
|
||||
.get('/pavics/magpie.json')
|
||||
t.create('docker cloud build status (valid, user)')
|
||||
.get('/jrottenberg/ffmpeg.json')
|
||||
.expectBadge({
|
||||
label: 'docker build',
|
||||
message: isBuildStatus,
|
||||
})
|
||||
|
||||
t.create('docker cloud build status (invalid, nonexisting user)')
|
||||
.get('/pavicsssss/magpie.json')
|
||||
.expectBadge({
|
||||
label: 'docker build',
|
||||
message: 'automated builds not set up',
|
||||
})
|
||||
|
||||
t.create(
|
||||
"docker cloud build status (valid user, but the 'objects' array from the response is empty)"
|
||||
)
|
||||
.get('/pavics/weaver.json')
|
||||
.expectBadge({
|
||||
label: 'docker build',
|
||||
message: 'automated builds not set up',
|
||||
})
|
||||
|
||||
t.create('docker cloud build status (not found)')
|
||||
.get('/badges/not-a-real-repo.json')
|
||||
.intercept(nock =>
|
||||
|
||||
@@ -6,7 +6,7 @@ const cloudBuildSchema = Joi.object({
|
||||
Joi.object({
|
||||
state: Joi.string(),
|
||||
build_settings: Joi.array(),
|
||||
})
|
||||
}).required()
|
||||
)
|
||||
.required(),
|
||||
}).required()
|
||||
@@ -14,7 +14,7 @@ const cloudBuildSchema = Joi.object({
|
||||
async function fetchBuild(serviceInstance, { user, repo }) {
|
||||
return serviceInstance._requestJson({
|
||||
schema: cloudBuildSchema,
|
||||
url: 'https://cloud.docker.com/api/build/v1/source',
|
||||
url: `https://cloud.docker.com/api/build/v1/source`,
|
||||
options: { searchParams: { image: `${user}/${repo}` } },
|
||||
errorMessages: { 404: 'repo not found' },
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user