Compare commits

..

1 Commits

Author SHA1 Message Date
Caleb Cartwright
7da38f8354 docs: start collating info on integration patterns 2022-07-14 21:11:16 -05:00
328 changed files with 15756 additions and 17153 deletions

View File

@@ -1,3 +1,369 @@
version: 2
# Do nothing
# TODO: disable Circle
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
- run:
name: Install dependencies
command: |
npm ci
environment:
CYPRESS_INSTALL_BINARY: 0
- run:
name: Identify services tagged in the PR title
command: npm run test:services:pr:prepare
- run:
name: Run tests for tagged services
environment:
mocha_reporter: mocha-junit-reporter
MOCHA_FILE: junit/services/results.xml
command: RETRY_COUNT=3 npm run test:services:pr:run
- 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
steps:
- checkout
- run:
name: Install dependencies
command: npm ci
environment:
CYPRESS_INSTALL_BINARY: 0
- run:
name: Danger
when: always
environment:
# https://github.com/gatsbyjs/gatsby/pull/11555
NODE_ENV: test
command: npm run danger ci
frontend:
docker:
- image: cimg/node:16.15
steps:
- checkout
- run:
name: Install dependencies
command: |
npm ci
environment:
CYPRESS_INSTALL_BINARY: 0
- run:
name: Prepare frontend tests
command: npm run defs && npm run features
- run:
name: Check types
command: npm run check-types:frontend
- run:
name: Frontend unit tests
environment:
mocha_reporter: mocha-junit-reporter
MOCHA_FILE: junit/frontend/results.xml
when: always
command: npm run test:frontend
- store_test_results:
path: junit
- run:
name: Frontend build completes successfully
when: always
command: npm run build
package:
machine:
image: 'ubuntu-2004:202111-02'
<<: *package_steps
services:
docker:
- image: cimg/node:16.15
<<: *services_steps
services@node-17:
docker:
- image: cimg/node:17.9
<<: *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:
ignore:
- master
- gh-pages
- services@node-17:
filters:
branches:
ignore:
- master
- gh-pages
- danger:
filters:
branches:
ignore:
- 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
# - services:
# requires:
# - npm-install
# filters:
# branches:
# ignore: master
# - services@node-latest:
# requires:
# - npm-install
# filters:
# branches:
# ignore: master
# - danger:
# requires:
# - npm-install
# filters:
# branches:
# ignore: /dependabot\/.*/

View File

@@ -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

View File

@@ -0,0 +1,37 @@
---
name: 💡 Badge Request
about: Ideas for new badges
labels: 'service-badge'
---
:clipboard: **Description**
<!--
A clear and concise description of the new badge.
- Which service is this badge for e.g: GitHub, Travis CI
- What sort of information should this badge show?
Provide an example in plain text e.g: "version | v1.01" or as a static badge
(static badge generator can be found at https://shields.io)
-->
:link: **Data**
<!--
Where can we get the data from?
- Is there a public API?
- Does the API requires an API key?
- Link to the API documentation.
-->
:microphone: **Motivation**
<!--
Please explain why this feature should be implemented and how it would be used.
- What is the specific use case?
-->
<!-- Love Shields? Please consider donating $10 to sustain our activities:
👉 https://opencollective.com/shields -->

View File

@@ -1,62 +0,0 @@
name: '💡 Badge Request'
description: Ideas for new badges
labels: ['service-badge']
body:
- type: markdown
attributes:
value: >
## Ideas for new badges
This issue template is for suggesting new badges which
**fetch and display data from an upstream service**.
If your suggestion is for a static badge
(which shows the same information every time it is requested), it is
[already possible to make these](https://github.com/badges/shields/blob/master/doc/static-badges.md).
We don't add specific routes for badges which only show static information.
- type: textarea
id: description
attributes:
label: '📋 Description'
description: |
A clear and concise description of the new badge.
- Which service is this badge for e.g: GitHub, Travis CI
- What sort of information should this badge show?
Provide an example in plain text e.g: "version | v1.01" or as a static badge
(static badge generator can be found at https://shields.io/#your-badge )
validations:
required: true
- type: textarea
id: data
attributes:
label: '🔗 Data'
description: |
Where can we get the data from?
Please consider and cover details like:
- Is there a public API?
- Does the API require authentication or an API key?
If so, please review our documentation on [Badges Requiring Authentication](https://github.com/badges/shields/blob/master/doc/authentication.md)
- Link to the API documentation.
validations:
required: true
- type: textarea
id: motivation
attributes:
label: '🎤 Motivation'
description: |
Please explain why this feature should be implemented and how it would be used.
- What is the specific use case?
validations:
required: true
- type: markdown
attributes:
value: |
## :heart: Love Shields?
Please consider donating $10 to sustain our activities: [https://opencollective.com/shields](https://opencollective.com/shields)

View File

@@ -8,5 +8,5 @@ inputs:
description: 'The GITHUB_TOKEN secret'
required: true
runs:
using: 'node16'
using: 'node12'
main: 'index.js'

View File

@@ -27,7 +27,7 @@ async function run() {
state: 'closed',
})
core.debug('Done.')
core.debug(`Done.`)
}
}
} catch (error) {

View File

@@ -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",

View File

@@ -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"
}
}

View File

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

View File

@@ -1,31 +0,0 @@
name: 'Frontend tests'
description: 'Run frontend tests and check types'
runs:
using: 'composite'
steps:
- name: Prepare frontend tests
if: always()
run: npm run defs && npm run features
shell: bash
- name: Tests
if: always()
run: npm run test:frontend -- --reporter json --reporter-option 'output=reports/frontend-tests.json'
shell: bash
- name: Type Checks
if: always()
run: |
set -o pipefail
npm run check-types:frontend 2>&1 | tee reports/frontend-types.txt
shell: bash
- name: Write Markdown Summary
if: always()
run: |
node scripts/mocha2md.js 'Frontend Tests' reports/frontend-tests.json >> $GITHUB_STEP_SUMMARY
echo '# Frontend Types' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
cat reports/frontend-types.txt >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
shell: bash

View File

@@ -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

View File

@@ -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

View File

@@ -1,86 +0,0 @@
name: 'Service tests'
description: 'Run tests for selected services'
inputs:
github-token:
description: 'The GITHUB_TOKEN secret'
required: true
librariesio-tokens:
description: 'The SERVICETESTS_LIBRARIESIO_TOKENS secret'
required: false
default: ''
obs-user:
description: 'The SERVICETESTS_OBS_USER secret'
required: false
default: ''
obs-pass:
description: 'The SERVICETESTS_OBS_PASS secret'
required: false
default: ''
sl-insight-user-uuid:
description: 'The SERVICETESTS_SL_INSIGHT_USER_UUID secret'
required: false
default: ''
sl-insight-api-token:
description: 'The SERVICETESTS_SL_INSIGHT_API_TOKEN secret'
required: false
default: ''
twitch-client-id:
description: 'The SERVICETESTS_TWITCH_CLIENT_ID secret'
required: false
default: ''
twitch-client-secret:
description: 'The SERVICETESTS_TWITCH_CLIENT_SECRET secret'
required: false
default: ''
wheelmap-token:
description: 'The SERVICETESTS_WHEELMAP_TOKEN secret'
required: false
default: ''
youtube-api-key:
description: 'The SERVICETESTS_YOUTUBE_API_KEY secret'
required: false
default: ''
runs:
using: 'composite'
steps:
- name: Derive list of service tests to run
# Note: In this step we are using an intermediate env var instead of
# passing github.event.pull_request.title as an argument
# to prevent a shell injection attack. Further reading:
# https://securitylab.github.com/research/github-actions-untrusted-input/#exploitability-and-impact
# https://securitylab.github.com/research/github-actions-untrusted-input/#remediation
if: always()
env:
TITLE: ${{ github.event.pull_request.title }}
run: npm run test:services:pr:prepare "$TITLE"
shell: bash
- name: Run service tests
if: always()
run: npm run test:services:pr:run -- --reporter json --reporter-option 'output=reports/service-tests.json'
shell: bash
env:
RETRY_COUNT: 3
GH_TOKEN: '${{ inputs.github-token }}'
LIBRARIESIO_TOKENS: '${{ inputs.librariesio-tokens }}'
OBS_USER: '${{ inputs.obs-user }}'
OBS_PASS: '${{ inputs.obs-pass }}'
SL_INSIGHT_USER_UUID: '${{ inputs.sl-insight-user-uuid }}'
SL_INSIGHT_API_TOKEN: '${{ inputs.sl-insight-api-token }}'
TWITCH_CLIENT_ID: '${{ inputs.twitch-client-id }}'
TWITCH_CLIENT_SECRET: '${{ inputs.twitch-client-secret }}'
WHEELMAP_TOKEN: '${{ inputs.wheelmap-token }}'
YOUTUBE_API_KEY: '${{ inputs.youtube-api-key }}'
- name: Write Markdown Summary
if: always()
run: |
if test -f 'reports/service-tests.json'; then
echo '# Services' >> $GITHUB_STEP_SUMMARY
sed -e 's/^/- /' pull-request-services.log >> $GITHUB_STEP_SUMMARY
node scripts/mocha2md.js Report reports/service-tests.json >> $GITHUB_STEP_SUMMARY
else
echo 'No services found. Nothing to do.' >> $GITHUB_STEP_SUMMARY
fi
shell: bash

View File

@@ -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

View File

@@ -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:

View File

@@ -3,7 +3,7 @@ on:
pull_request:
jobs:
build-docker-image:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout

View File

@@ -1,29 +0,0 @@
name: Danger
on:
pull_request_target:
types: [opened, edited, reopened, synchronize]
permissions:
checks: write
pull-requests: write
statuses: write
jobs:
danger:
runs-on: ubuntu-latest
if: github.actor != 'dependabot[bot]'
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup
uses: ./.github/actions/setup
with:
node-version: 16
- name: Danger
run: npm run danger ci
env:
# https://github.com/gatsbyjs/gatsby/pull/11555
NODE_ENV: test
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'

View File

@@ -8,7 +8,7 @@ permissions:
contents: write
jobs:
deploy-docs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout

View File

@@ -10,7 +10,7 @@ permissions:
contents: write
jobs:
draft-release:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout

View File

@@ -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

View File

@@ -5,7 +5,7 @@ on:
- master
jobs:
publish-docker-next:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout

View File

@@ -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

View File

@@ -1,26 +0,0 @@
name: Frontend
on:
pull_request:
types: [opened, edited, reopened, synchronize]
push:
branches-ignore:
- 'gh-pages'
- 'dependabot/**'
jobs:
test-frontend:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup
uses: ./.github/actions/setup
with:
node-version: 16
- name: Frontend tests
uses: ./.github/actions/frontend-tests
- name: Frontend build
run: npm run build

View File

@@ -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 }}'

View File

@@ -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 }}'

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,40 +0,0 @@
name: Services@node 17
on:
pull_request:
types: [opened, edited, reopened, synchronize]
jobs:
test-services-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: Service tests (triggered from local branch)
if: github.event.pull_request.head.repo.full_name == github.repository
uses: ./.github/actions/service-tests
with:
github-token: '${{ secrets.GH_PAT }}'
librariesio-tokens: '${{ secrets.SERVICETESTS_LIBRARIESIO_TOKENS }}'
obs-user: '${{ secrets.SERVICETESTS_OBS_USER }}'
obs-pass: '${{ secrets.SERVICETESTS_OBS_PASS }}'
sl-insight-user-uuid: '${{ secrets.SERVICETESTS_SL_INSIGHT_USER_UUID }}'
sl-insight-api-token: '${{ secrets.SERVICETESTS_SL_INSIGHT_API_TOKEN }}'
twitch-client-id: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_ID }}'
twitch-client-secret: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_SECRET }}'
wheelmap-token: '${{ secrets.SERVICETESTS_WHEELMAP_TOKEN }}'
youtube-api-key: '${{ secrets.SERVICETESTS_YOUTUBE_API_KEY }}'
- name: Service tests (triggered from fork)
if: github.event.pull_request.head.repo.full_name != github.repository
uses: ./.github/actions/service-tests
with:
github-token: '${{ secrets.GITHUB_TOKEN }}'

View File

@@ -1,38 +0,0 @@
name: Services
on:
pull_request:
types: [opened, edited, reopened, synchronize]
jobs:
test-services:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup
uses: ./.github/actions/setup
with:
node-version: 16
- name: Service tests (triggered from local branch)
if: github.event.pull_request.head.repo.full_name == github.repository
uses: ./.github/actions/service-tests
with:
github-token: '${{ secrets.GH_PAT }}'
librariesio-tokens: '${{ secrets.SERVICETESTS_LIBRARIESIO_TOKENS }}'
obs-user: '${{ secrets.SERVICETESTS_OBS_USER }}'
obs-pass: '${{ secrets.SERVICETESTS_OBS_PASS }}'
sl-insight-user-uuid: '${{ secrets.SERVICETESTS_SL_INSIGHT_USER_UUID }}'
sl-insight-api-token: '${{ secrets.SERVICETESTS_SL_INSIGHT_API_TOKEN }}'
twitch-client-id: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_ID }}'
twitch-client-secret: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_SECRET }}'
wheelmap-token: '${{ secrets.SERVICETESTS_WHEELMAP_TOKEN }}'
youtube-api-key: '${{ secrets.SERVICETESTS_YOUTUBE_API_KEY }}'
- name: Service tests (triggered from fork)
if: github.event.pull_request.head.repo.full_name != github.repository
uses: ./.github/actions/service-tests
with:
github-token: '${{ secrets.GITHUB_TOKEN }}'

View File

@@ -1,33 +0,0 @@
name: Update GitHub API Version
on:
schedule:
- cron: '0 7 * * 6'
# At 07:00 on Saturday
workflow_dispatch:
permissions:
pull-requests: write
contents: write
jobs:
update-github-api:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup
uses: ./.github/actions/setup
with:
node-version: 16
- name: Check for new GitHub API version
run: node scripts/update-github-api.js
- name: Create Pull Request if config has changed
uses: peter-evans/create-pull-request@v4
with:
token: '${{ secrets.GITHUB_TOKEN }}'
commit-message: Update GitHub API Version
title: Update [GitHub] API Version
branch-suffix: random

View File

@@ -4,99 +4,6 @@ Note: this changelog is for the shields.io server. The changelog for the badge-m
---
## server-2023-01-01
- Breaking change: Routes for GitHub workflows badge have changed. See https://github.com/badges/shields/issues/8671 for more details
- Behaviour change: In this release we fixed a long standing bug. GitHub badges were previously not reading the base URL from the `config.service.baseUri`.
This release fixes that bug, bringing the code into line with the documented behaviour. This should not cause a behaviour change for most users,
but users who had previously set a value in `config.service.baseUri` which was previously ignored could see this now have an effect.
Users who configure their instance using env vars rather than yaml should see no change.
- Send `X-GitHub-Api-Version` when calling [GitHub] v3 API [#8669](https://github.com/badges/shields/issues/8669)
- add [VpmVersion] badge [#8755](https://github.com/badges/shields/issues/8755)
- Add [modrinth] game versions [#8673](https://github.com/badges/shields/issues/8673)
- fix debug logging of undefined query params [#8540](https://github.com/badges/shields/issues/8540), [#8757](https://github.com/badges/shields/issues/8757)
- fall back to classifiers if [pypi] license text is really long [#8690](https://github.com/badges/shields/issues/8690)
- allow passing key to [stackexchange] [#8539](https://github.com/badges/shields/issues/8539)
- Dependency updates
## 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)

View File

@@ -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.

View File

@@ -19,6 +19,9 @@
<a href="https://coveralls.io/github/badges/shields">
<img src="https://img.shields.io/coveralls/github/badges/shields"
alt="coverage"></a>
<a href="https://lgtm.com/projects/g/badges/shields/alerts/">
<img src="https://img.shields.io/lgtm/alerts/g/badges/shields"
alt="Total alerts"/></a>
<a href="https://discord.gg/HjJCwm5">
<img src="https://img.shields.io/discord/308323056592486420?logo=discord"
alt="chat on Discord"></a>

View File

@@ -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": {

View File

@@ -2,7 +2,7 @@
## 4.0.0 [WIP]
- Drop compatibility with Node < 14
- Drop compatibility with Node 10
## 3.3.1

View File

@@ -26,7 +26,7 @@
"badge": "lib/badge-cli.js"
},
"engines": {
"node": ">= 14",
"node": ">= 12",
"npm": ">= 6"
},
"collective": {

View File

@@ -98,7 +98,6 @@ private:
sl_insight_userUuid: 'SL_INSIGHT_USER_UUID'
sl_insight_apiToken: 'SL_INSIGHT_API_TOKEN'
sonarqube_token: 'SONARQUBE_TOKEN'
stackapps_api_key: 'STACKAPPS_API_KEY'
teamcity_user: 'TEAMCITY_USER'
teamcity_pass: 'TEAMCITY_PASS'
twitch_client_id: 'TWITCH_CLIENT_ID'

View File

@@ -1,6 +1,7 @@
public:
bind:
address: '::'
metrics:
prometheus:
enabled: false
@@ -11,26 +12,33 @@ public:
intervalSeconds: 15
ssl:
isSecure: false
cors:
allowedOrigin: []
services:
github:
baseUri: 'https://api.github.com'
baseUri: 'https://api.github.com/'
debug:
enabled: false
intervalSeconds: 200
restApiVersion: '2022-11-28'
obs:
authorizedOrigins: 'https://api.opensuse.org'
weblate:
authorizedOrigins: 'https://hosted.weblate.org'
trace: false
cacheHeaders:
defaultCacheLengthSeconds: 120
handleInternalErrors: true
fetchLimit: '10MB'
userAgentBase: 'shields (self-hosted)'
requestTimeoutSeconds: 120
requestTimeoutMaxAgeSeconds: 30
requireCloudflare: false
private: {}

View File

@@ -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({

View File

@@ -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,

View File

@@ -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('')

View File

@@ -221,14 +221,8 @@ class BaseService {
const logTrace = (...args) => trace.logTrace('fetch', ...args)
let logUrl = url
const logOptions = Object.assign({}, options)
if ('searchParams' in options && options.searchParams != null) {
const params = new URLSearchParams(
Object.fromEntries(
Object.entries(options.searchParams).filter(
([k, v]) => v !== undefined
)
)
)
if ('searchParams' in options) {
const params = new URLSearchParams(options.searchParams)
logUrl = `${url}?${params.toString()}`
delete logOptions.searchParams
}

View File

@@ -440,21 +440,14 @@ describe('BaseService', function () {
)
const url = 'some-url'
const options = {
headers: { Cookie: 'some-cookie' },
searchParams: { param1: 'foobar', param2: undefined },
}
const options = { headers: { Cookie: 'some-cookie' } }
await serviceInstance._request({ url, options })
expect(trace.logTrace).to.be.calledWithMatch(
'fetch',
sinon.match.string,
'Request',
`${url}?param1=foobar\n${JSON.stringify(
{ headers: options.headers },
null,
2
)}`
`${url}\n${JSON.stringify(options, null, 2)}`
)
expect(trace.logTrace).to.be.calledWithMatch(
'fetch',

View File

@@ -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.

View File

@@ -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

View File

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

View File

@@ -14,16 +14,14 @@ let resourceCache = Object.create(null)
/**
* Make a HTTP request using an in-memory cache
*
* @async
* @param {object} attrs - Refer to individual attrs
* @param {string} attrs.url - URL to request
* @param {number} attrs.ttl - Number of milliseconds to keep cached value for
* @param {boolean} [attrs.json=true] - True if we expect to parse the response as JSON
* @param {Function} [attrs.scraper=buffer => buffer] - Function to extract value from the response
* @param {object} [attrs.options={}] - Options to pass to got
* @param {Function} [attrs.requestFetcher=fetch] - Custom fetch function
* @throws {InvalidResponse} - Error if unable to parse response
* @returns {Promise<*>} Promise that resolves to parsed response
* @param {object} attrs Refer to individual attrs
* @param {string} attrs.url URL to request
* @param {number} attrs.ttl Number of milliseconds to keep cached value for
* @param {boolean} [attrs.json=true] True if we expect to parse the response as JSON
* @param {Function} [attrs.scraper=buffer => buffer] Function to extract value from the response
* @param {object} [attrs.options={}] Options to pass to got
* @param {Function} [attrs.requestFetcher=fetch] Custom fetch function
* @returns {*} Parsed response
*/
async function getCachedResource({
url,

View File

@@ -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({
@@ -126,7 +124,6 @@ const publicConfigSchema = Joi.object({
enabled: Joi.boolean().required(),
intervalSeconds: Joi.number().integer().min(1).required(),
},
restApiVersion: Joi.date().raw().required(),
},
gitlab: defaultService,
jira: defaultService,
@@ -172,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()),
@@ -187,7 +182,6 @@ const privateConfigSchema = Joi.object({
sl_insight_userUuid: Joi.string(),
sl_insight_apiToken: Joi.string(),
sonarqube_token: Joi.string(),
stackapps_api_key: Joi.string(),
teamcity_user: Joi.string(),
teamcity_pass: Joi.string(),
twitch_client_id: Joi.string(),
@@ -492,6 +486,7 @@ class Server {
const {
bind: { port, address: hostname },
ssl: { isSecure: secure, cert, key },
cors: { allowedOrigin },
requireCloudflare,
} = this.config.public
@@ -524,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', '*')

View File

@@ -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 () {

View File

@@ -0,0 +1,102 @@
/**
* @module
*/
import { URL, format as urlFormat } from 'url'
function formatSlug(owner, repo, pullRequest) {
return `${owner}/${repo}#${pullRequest}`
}
function parseGithubPullRequestUrl(url, options = {}) {
const { verifyBaseUrl } = options
const parsed = new URL(url)
const components = parsed.pathname.substr(1).split('/')
if (components[2] !== 'pull' || components.length !== 4) {
throw Error(`Invalid GitHub pull request URL: ${url}`)
}
const [owner, repo, , pullRequest] = components
parsed.pathname = ''
const baseUrl = urlFormat(parsed, {
auth: false,
fragment: false,
search: false,
}).replace(/\/$/, '')
if (verifyBaseUrl && baseUrl !== verifyBaseUrl) {
throw Error(`Expected base URL to be ${verifyBaseUrl} but got ${baseUrl}`)
}
return {
baseUrl,
owner,
repo,
pullRequest: +pullRequest,
slug: formatSlug(owner, repo, pullRequest),
}
}
function parseGithubRepoSlug(slug) {
const components = slug.split('/')
if (components.length !== 2) {
throw Error(`Invalid GitHub repo slug: ${slug}`)
}
const [owner, repo] = components
return { owner, repo }
}
function _inferPullRequestFromTravisEnv(env) {
const { owner, repo } = parseGithubRepoSlug(env.TRAVIS_REPO_SLUG)
const pullRequest = +env.TRAVIS_PULL_REQUEST
return {
owner,
repo,
pullRequest,
slug: formatSlug(owner, repo, pullRequest),
}
}
function _inferPullRequestFromCircleEnv(env) {
return parseGithubPullRequestUrl(
env.CI_PULL_REQUEST || env.CIRCLE_PULL_REQUEST
)
}
/**
* When called inside a CI build, infer the details
* of a pull request from the environment variables.
*
* @param {object} [env=process.env] Environment variables
* @returns {module:core/service-test-runner/infer-pull-request~PullRequest}
* Pull Request
*/
function inferPullRequest(env = process.env) {
if (env.TRAVIS) {
return _inferPullRequestFromTravisEnv(env)
} else if (env.CIRCLECI) {
return _inferPullRequestFromCircleEnv(env)
} else if (env.CI) {
throw Error(
'Unsupported CI system. Unable to obtain pull request information from the environment.'
)
} else {
throw Error(
'Unable to obtain pull request information from the environment. Is this running in CI?'
)
}
}
/**
* Pull Request
*
* @typedef PullRequest
* @property {string} pr.baseUrl (returned for travis CI only)
* @property {string} owner
* @property {string} repo
* @property {string} pullRequest PR/issue number
* @property {string} slug owner/repo/#pullRequest
*/
export { parseGithubPullRequestUrl, parseGithubRepoSlug, inferPullRequest }

View File

@@ -0,0 +1,48 @@
import { test, given, forCases } from 'sazerac'
import {
parseGithubPullRequestUrl,
inferPullRequest,
} from './infer-pull-request.js'
describe('Pull request inference', function () {
test(parseGithubPullRequestUrl, () => {
forCases([
given('https://github.com/badges/shields/pull/1234'),
given('https://github.com/badges/shields/pull/1234', {
verifyBaseUrl: 'https://github.com',
}),
]).expect({
baseUrl: 'https://github.com',
owner: 'badges',
repo: 'shields',
pullRequest: 1234,
slug: 'badges/shields#1234',
})
given('https://github.com/badges/shields/pull/1234', {
verifyBaseUrl: 'https://example.com',
}).expectError(
'Expected base URL to be https://example.com but got https://github.com'
)
})
test(inferPullRequest, () => {
const expected = {
owner: 'badges',
repo: 'shields',
pullRequest: 1234,
slug: 'badges/shields#1234',
}
given({
CIRCLECI: '1',
CI_PULL_REQUEST: 'https://github.com/badges/shields/pull/1234',
}).expect(Object.assign({ baseUrl: 'https://github.com' }, expected))
given({
TRAVIS: '1',
TRAVIS_REPO_SLUG: 'badges/shields',
TRAVIS_PULL_REQUEST: '1234',
}).expect(expected)
})
})

View File

@@ -1,5 +1,5 @@
// Derive a list of service tests to run based on
// space-separated service names in the PR title.
// Infer the current PR from the Travis environment, and look for bracketed,
// space-separated service names in the pull request title.
//
// Output the list of services.
//
@@ -8,26 +8,54 @@
// Output:
// travis
// sonar
//
// Example:
//
// TRAVIS=1 TRAVIS_REPO_SLUG=badges/shields TRAVIS_PULL_REQUEST=1108 npm run test:services:pr:prepare
import got from 'got'
import { inferPullRequest } from './infer-pull-request.js'
import servicesForTitle from './services-for-title.js'
let title
async function getTitle(owner, repo, pullRequest) {
const {
body: { title },
} = await got(
`https://api.github.com/repos/${owner}/${repo}/pulls/${pullRequest}`,
{
headers: {
'User-Agent': 'badges/shields',
Authorization: `token ${process.env.GITHUB_TOKEN}`,
},
responseType: 'json',
}
)
return title
}
try {
if (process.argv.length < 3) {
throw new Error()
async function main() {
const { owner, repo, pullRequest, slug } = inferPullRequest()
console.error(`PR: ${slug}`)
const title = await getTitle(owner, repo, pullRequest)
console.error(`Title: ${title}\n`)
const services = servicesForTitle(title)
if (services.length === 0) {
console.error('No services found. Nothing to do.')
} else {
console.error(
`Services: (${services.length} found) ${services.join(', ')}\n`
)
console.log(services.join('\n'))
}
title = process.argv[2]
} catch (e) {
console.error('Error processing arguments')
process.exit(1)
}
console.error(`Title: ${title}\n`)
const services = servicesForTitle(title)
if (services.length === 0) {
console.error('No services found. Nothing to do.')
} else {
console.error(`Services: (${services.length} found) ${services.join(', ')}\n`)
console.log(services.join('\n'))
}
;(async () => {
try {
await main()
} catch (e) {
console.error(e)
process.exit(1)
}
})()

View File

@@ -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 => {

View File

@@ -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('')

View File

@@ -1,8 +0,0 @@
# Badges Requiring Authentication
There are two patterns for how shields.io can interact with APIs that require auth:
1. We can store one token at the service level which allows us to read public data for everyone's projects, or lift a rate limit. If you are looking for information on configuring credentials for a self-hosted instance see https://github.com/badges/shields/blob/master/doc/server-secrets.md
2. If every user needs to provide their own token, that has to be a token which can be passed to us as a query param in the badge URL. This means it must be possible to generate a key or token that can be exposed in a public github README public with no negative consequences. (i.e: that key or token only exposes public metrics).
If every user would need to supply their own token for some particular service and it is only possible to generate a key or token which allows access to sensitive data or allows write access to resources, we can't provide an integration for this service.

View File

@@ -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 its 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`.

View File

@@ -22,18 +22,6 @@ Any custom logo can be passed in a URL parameter by base64 encoding it. e.g:
![](https://img.shields.io/badge/play-station-blue.svg?logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEiIHdpZHRoPSI2MDAiIGhlaWdodD0iNjAwIj48cGF0aCBkPSJNMTI5IDExMWMtNTUgNC05MyA2Ni05MyA3OEwwIDM5OGMtMiA3MCAzNiA5MiA2OSA5MWgxYzc5IDAgODctNTcgMTMwLTEyOGgyMDFjNDMgNzEgNTAgMTI4IDEyOSAxMjhoMWMzMyAxIDcxLTIxIDY5LTkxbC0zNi0yMDljMC0xMi00MC03OC05OC03OGgtMTBjLTYzIDAtOTIgMzUtOTIgNDJIMjM2YzAtNy0yOS00Mi05Mi00MmgtMTV6IiBmaWxsPSIjZmZmIi8+PC9zdmc+) - https://img.shields.io/badge/play-station-blue.svg?logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEiIHdpZHRoPSI2MDAiIGhlaWdodD0iNjAwIj48cGF0aCBkPSJNMTI5IDExMWMtNTUgNC05MyA2Ni05MyA3OEwwIDM5OGMtMiA3MCAzNiA5MiA2OSA5MWgxYzc5IDAgODctNTcgMTMwLTEyOGgyMDFjNDMgNzEgNTAgMTI4IDEyOSAxMjhoMWMzMyAxIDcxLTIxIDY5LTkxbC0zNi0yMDljMC0xMi00MC03OC05OC03OGgtMTBjLTYzIDAtOTIgMzUtOTIgNDJIMjM2YzAtNy0yOS00Mi05Mi00MmgtMTV6IiBmaWxsPSIjZmZmIi8+PC9zdmc+
### logoColor parameter
The `logoColor` param can be used to set the color of the logo. Hex, rgb, rgba, hsl, hsla and css named colors can all be used. For SimpleIcons named logos (which are monochrome), the color will be applied to the SimpleIcons logo.
- ![](https://img.shields.io/badge/logo-javascript-blue?logo=javascript) - https://img.shields.io/badge/logo-javascript-blue?logo=javascript
- ![](https://img.shields.io/badge/logo-javascript-blue?logo=javascript&logoColor=f5f5f5) - https://img.shields.io/badge/logo-javascript-blue?logo=javascript&logoColor=f5f5f5
In the case where Shields hosts a custom multi-colored logo, if the `logoColor` param is passed, the corresponding SimpleIcons logo will be substituted and colored.
- ![](https://img.shields.io/badge/logo-gitlab-blue?logo=gitlab) - https://img.shields.io/badge/logo-gitlab-blue?logo=gitlab
- ![](https://img.shields.io/badge/logo-gitlab-blue?logo=gitlab&logoColor=white) - https://img.shields.io/badge/logo-gitlab-blue?logo=gitlab&logoColor=white
## Contributing Logos
Our preferred way to consume icons is via the SimpleIcons logo. As a first port of call, we encourage you to contribute logos to [the SimpleIcons project][simple-icons github]. Please review their [guidance](https://github.com/simple-icons/simple-icons/blob/develop/CONTRIBUTING.md) before contributing.

View File

@@ -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:

View File

@@ -244,17 +244,6 @@ Create an account, sign in and obtain a uuid and token from your
to give your self-hosted Shields installation access to a
private SonarQube instance or private project on a public instance.
### StackApps (for StackExchange and StackOverflow)
- `STACKAPPS_API_KEY`: (yml: `private.stackapps_api_key`)
Anonymous requests to the stackexchange API are limited to 300 calls per day.
To increase your quota to 10,000 calls per day, create an account at
[StackApps](https://stackapps.com/) and
[register an OAuth app](https://stackapps.com/apps/oauth/register). Having registered
an OAuth app, you'll be granted a key which can be used to increase your request quota.
It is not necessary to performa full OAuth Flow to gain an access token.
### TeamCity
- `TEAMCITY_ORIGINS` (yml: `public.services.teamcity.authorizedOrigins`)

View 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...

View File

@@ -67,7 +67,7 @@ t.create('Build status')
- All badges on shields can be requested in a number of formats. As well as calling https://img.shields.io/wercker/build/wercker/go-wercker-api.svg to generate ![](https://img.shields.io/wercker/build/wercker/go-wercker-api.svg) we can also call https://img.shields.io/wercker/build/wercker/go-wercker-api.json to request the same content as JSON. When writing service tests, we request the badge in JSON format so it is easier to make assertions about the content.
- We don't need to explicitly call `/wercker/build/wercker/go-wercker-api.json` here, only `/build/wercker/go-wercker-api.json`. When we create a tester object with `createServiceTester()` the URL base defined in our service class (in this case `/wercker`) is used as the base URL for any requests made by the tester object.
3. `expectBadge()` is a helper function which accepts either a string literal, a [RegExp][] or a [Joi][] schema for the different fields.
Joi is a validation library that is built into IcedFrisby which you can use to
Joi is a validation library that is build into IcedFrisby which you can use to
match based on a set of allowed strings, regexes, or specific values. You can
refer to their [API reference][joi api].
4. We expect `label` to be a string literal `"build"`.

View File

@@ -1,12 +0,0 @@
# Static Badges
It is possible to use shields.io to make a wide variety of badges displaying static text and/or logos. For example:
- ![any text you like](https://img.shields.io/badge/any%20text-you%20like-blue) - https://img.shields.io/badge/any%20text-you%20like-blue
- ![just the message](https://img.shields.io/badge/just%20the%20message-8A2BE2) - https://img.shields.io/badge/just%20the%20message-8A2BE2
- !['for the badge' style](https://img.shields.io/badge/%27for%20the%20badge%27%20style-20B2AA?style=for-the-badge) - https://img.shields.io/badge/%27for%20the%20badge%27%20style-20B2AA?style=for-the-badge
- ![with a logo](https://img.shields.io/badge/with%20a%20logo-grey?style=for-the-badge&logo=javascript) - https://img.shields.io/badge/with%20a%20logo-grey?style=for-the-badge&logo=javascript
Full documentation of styles and parameters: https://shields.io/#styles
More documentation on logos: https://github.com/badges/shields/blob/master/doc/logos.md

View File

@@ -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}
/>

View File

@@ -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}
/>

View File

@@ -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>
)

View File

@@ -6,7 +6,7 @@ import React, {
} from 'react'
import styled from 'styled-components'
import humanizeString from 'humanize-string'
import qs from 'query-string'
import { stringify as stringifyQueryString } from 'query-string'
import { advertisedStyles } from '../../lib/supported-features'
import { noAutocorrect, StyledInput } from '../common'
import {
@@ -94,7 +94,7 @@ function getQueryString({
}
})
const queryString = qs.stringify(outQuery)
const queryString = stringifyQueryString(outQuery)
return { queryString, isComplete }
}

View File

@@ -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()}

View File

@@ -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>

View File

@@ -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}
/>

View File

@@ -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>
)
}

View 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>
)
}

View File

@@ -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"

View File

@@ -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(
'![Example](https://img.shields.io/badge)'
)
given('https://img.shields.io/badge', undefined).expect(
given(
'https://img.shields.io/badge',
'https://example.com/example',
'Example'
).expect(
'[![Example](https://img.shields.io/badge)](https://example.com/example)'
)
given('https://img.shields.io/badge', undefined, undefined).expect(
'![](https://img.shields.io/badge)'
)
})
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('![Example](https://img.shields.io/badge)')
}).expect(
'[![Example](https://img.shields.io/badge)](https://example.com/example)'
)
})

View File

@@ -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 `![${title || ''}](${badgeUrl})`
export function markdown(
badgeUrl: string,
link?: string,
title?: string
): string {
const withoutLink = `![${title || ''}](${badgeUrl})`
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)
}

View File

@@ -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

View File

@@ -106,6 +106,9 @@ export default function SponsorsPage(): JSX.Element {
<li>
<a href="https://github.com/">GitHub</a>
</li>
<li>
<a href="https://lgtm.com/">LGTM</a>
</li>
<li>
<a href="https://uptimerobot.com/">Uptime Robot</a>
</li>

View File

@@ -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"
/>

View File

@@ -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

View File

@@ -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

View File

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

22323
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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.29.0",
"@sentry/node": "^7.5.1",
"@shields_io/camp": "^18.1.1",
"badge-maker": "file:badge-maker",
"bytes": "^3.1.2",
"camelcase": "^7.0.1",
"chalk": "^5.2.0",
"camelcase": "^7.0.0",
"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.7",
"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.1",
"prom-client": "^14.0.1",
"qs": "^6.11.0",
"query-string": "^8.1.0",
"semver": "~7.3.8",
"simple-icons": "8.2.0",
"query-string": "^7.1.1",
"semver": "~7.3.7",
"simple-icons": "7.4.0",
"webextension-store-meta": "^1.0.5",
"xmldom": "~0.6.0",
"xpath": "~0.0.32"
@@ -142,108 +141,107 @@
]
},
"devDependencies": {
"@babel/core": "^7.20.12",
"@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.1",
"@types/mocha": "^9.1.1",
"@types/node": "^16.7.10",
"@types/react-helmet": "^6.1.6",
"@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.48.0",
"@typescript-eslint/parser": "^5.46.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": "^12.3.0",
"cypress-wait-for-stable-dom": "^0.1.0",
"danger": "^11.2.1",
"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",
"eslint-config-prettier": "^8.6.0",
"eslint-config-prettier": "^8.5.0",
"eslint-config-standard": "^16.0.3",
"eslint-config-standard-jsx": "^10.0.0",
"eslint-config-standard-react": "^11.0.1",
"eslint-plugin-chai-friendly": "^0.7.2",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-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.16.0",
"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.25.0",
"gatsby-plugin-page-creator": "^4.25.0",
"gatsby-plugin-react-helmet": "^5.25.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.25.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.1.0",
"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.2.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.1",
"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": "^15.0.1",
"simple-git-hooks": "^2.8.0",
"sinon": "^14.0.0",
"sinon-chai": "^3.7.0",
"snap-shot-it": "^7.9.10",
"start-server-and-test": "1.15.2",
"styled-components": "^5.3.6",
"snap-shot-it": "^7.9.6",
"start-server-and-test": "1.14.0",
"styled-components": "^5.3.5",
"ts-mocha": "^10.0.0",
"tsd": "^0.25.0",
"typescript": "^4.9.4",
"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",

View File

@@ -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

View File

@@ -1,40 +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.pending > 0) {
process.stdout.write(`${data.stats.pending} pending\n\n`)
process.exit(2)
}
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
View 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

View File

@@ -1,19 +0,0 @@
import fs from 'fs/promises'
import got from 'got'
import yaml from 'js-yaml'
const resp = await got('https://api.github.com/versions').json()
const latestDate = resp.sort()[resp.length - 1]
const config = yaml.load(await fs.readFile('./config/default.yml', 'utf8'))
if (latestDate === config.public.services.github.restApiVersion) {
console.log("We're already using the latest version. No change needed.")
process.exit(0)
}
config.public.services.github.restApiVersion = latestDate
await fs.writeFile(
'./config/default.yml',
yaml.dump(config, { forceQuotes: true })
)

View File

@@ -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 }

View File

@@ -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' })

View File

@@ -1,33 +1,117 @@
import { deprecatedService } from '../index.js'
import Joi from 'joi'
import { renderLicenseBadge } from '../licenses.js'
import { renderVersionBadge } from '../version.js'
import { renderDownloadsBadge } from '../downloads.js'
import { nonNegativeInteger } from '../validators.js'
import { BaseJsonService, InvalidResponse } from '../index.js'
const APMDownloads = deprecatedService({
category: 'downloads',
route: {
base: 'apm/dm',
pattern: ':various*',
},
label: 'downloads',
dateAdded: new Date('2023-01-04'),
const keywords = ['atom']
const schema = Joi.object({
downloads: nonNegativeInteger,
releases: Joi.object({
latest: Joi.string().required(),
}),
metadata: Joi.object({
license: Joi.string().required(),
}),
})
const APMVersion = deprecatedService({
category: 'version',
route: {
base: 'apm/v',
pattern: ':various*',
},
label: 'apm',
dateAdded: new Date('2023-01-04'),
})
class BaseAPMService extends BaseJsonService {
static defaultBadgeData = { label: 'apm' }
const APMLicense = deprecatedService({
category: 'license',
route: {
base: 'apm/l',
pattern: ':various*',
},
label: 'license',
dateAdded: new Date('2023-01-04'),
})
async fetch({ packageName }) {
return this._requestJson({
schema,
url: `https://atom.io/api/packages/${packageName}`,
errorMessages: { 404: 'package not found' },
})
}
}
class APMDownloads extends BaseAPMService {
static category = 'downloads'
static route = { base: 'apm/dm', pattern: ':packageName' }
static examples = [
{
title: 'APM',
namedParams: { packageName: 'vim-mode' },
staticPreview: this.render({ downloads: '60043' }),
keywords,
},
]
static defaultBadgeData = { label: 'downloads' }
static render({ downloads }) {
return renderDownloadsBadge({ downloads, colorOverride: 'green' })
}
async handle({ packageName }) {
const json = await this.fetch({ packageName })
return this.constructor.render({ downloads: json.downloads })
}
}
class APMVersion extends BaseAPMService {
static category = 'version'
static route = { base: 'apm/v', pattern: ':packageName' }
static examples = [
{
title: 'APM',
namedParams: { packageName: 'vim-mode' },
staticPreview: this.render({ version: '0.6.0' }),
keywords,
},
]
static render({ version }) {
return renderVersionBadge({ version })
}
async handle({ packageName }) {
const json = await this.fetch({ packageName })
const version = json.releases.latest
if (!version)
throw new InvalidResponse({
underlyingError: new Error('version is invalid'),
})
return this.constructor.render({ version })
}
}
class APMLicense extends BaseAPMService {
static category = 'license'
static route = { base: 'apm/l', pattern: ':packageName' }
static examples = [
{
title: 'APM',
namedParams: { packageName: 'vim-mode' },
staticPreview: this.render({ license: 'MIT' }),
keywords,
},
]
static defaultBadgeData = { label: 'license' }
static render({ license }) {
return renderLicenseBadge({ license })
}
async handle({ packageName }) {
const json = await this.fetch({ packageName })
const license = json.metadata.license
if (!license)
throw new InvalidResponse({
underlyingError: new Error('licence is invalid'),
})
return this.constructor.render({ license })
}
}
export { APMDownloads, APMVersion, APMLicense }

View File

@@ -1,19 +1,57 @@
import { ServiceTester } from '../tester.js'
import { invalidJSON } from '../response-fixtures.js'
import { isMetric, isVPlusTripleDottedVersion } from '../test-validators.js'
export const t = new ServiceTester({
id: 'apm',
title: 'Atom Package Manager',
pathPrefix: '/apm',
})
t.create('Downloads')
.get('/dm/vim-mode.json')
.expectBadge({ label: 'downloads', message: 'no longer available' })
.expectBadge({ label: 'downloads', message: isMetric })
t.create('Version')
.get('/v/vim-mode.json')
.expectBadge({ label: 'apm', message: 'no longer available' })
.expectBadge({ label: 'apm', message: isVPlusTripleDottedVersion })
t.create('License')
.get('/l/vim-mode.json')
.expectBadge({ label: 'license', message: 'no longer available' })
.expectBadge({ label: 'license', message: 'MIT' })
t.create('Downloads | Package not found')
.get('/dm/notapackage.json')
.expectBadge({ label: 'downloads', message: 'package not found' })
t.create('Version | Package not found')
.get('/v/notapackage.json')
.expectBadge({ label: 'apm', message: 'package not found' })
t.create('License | Package not found')
.get('/l/notapackage.json')
.expectBadge({ label: 'license', message: 'package not found' })
t.create('Invalid version')
.get('/dm/vim-mode.json')
.intercept(nock =>
nock('https://atom.io')
.get('/api/packages/vim-mode')
.reply(200, '{"releases":{}}')
)
.expectBadge({ label: 'downloads', message: 'invalid response data' })
t.create('Invalid License')
.get('/l/vim-mode.json')
.intercept(nock =>
nock('https://atom.io')
.get('/api/packages/vim-mode')
.reply(200, '{"metadata":{}}')
)
.expectBadge({ label: 'license', message: 'invalid response data' })
t.create('Unexpected response')
.get('/dm/vim-mode.json')
.intercept(nock =>
nock('https://atom.io').get('/api/packages/vim-mode').reply(invalidJSON)
)
.expectBadge({ label: 'downloads', message: 'unparseable json response' })

View File

@@ -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')

View File

@@ -27,7 +27,7 @@ function pullRequestClassGenerator(raw) {
static category = 'issue-tracking'
static route = {
base: `bitbucket/${routePrefix}`,
pattern: ':user/:repo',
pattern: `:user/:repo`,
queryParamSchema,
}

View File

@@ -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')

View File

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

View File

@@ -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)

View File

@@ -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',

Some files were not shown because too many files have changed in this diff Show More