Compare commits
25 Commits
server-202
...
express
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc68b88a04 | ||
|
|
ca6ae88504 | ||
|
|
fc13e8e90a | ||
|
|
1761aab020 | ||
|
|
416ef3920a | ||
|
|
25ab4806c0 | ||
|
|
9d6b3e0985 | ||
|
|
18c76e392f | ||
|
|
78b886c010 | ||
|
|
b19ca203e8 | ||
|
|
00df3e1136 | ||
|
|
b4f7ec383e | ||
|
|
6d77534709 | ||
|
|
e8f59a2645 | ||
|
|
8cbc8cb926 | ||
|
|
be9d49083d | ||
|
|
aa046cb510 | ||
|
|
903bef2a4c | ||
|
|
15043dfc92 | ||
|
|
cf6b5b14c7 | ||
|
|
aeebfaa51f | ||
|
|
c3097aad0d | ||
|
|
6e6cec9b2b | ||
|
|
7c071e352e | ||
|
|
6d72fd68e8 |
@@ -1,5 +1,71 @@
|
||||
version: 2
|
||||
|
||||
main_steps: &main_steps
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: |
|
||||
npm install --dry-run
|
||||
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 install --dry-run
|
||||
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
|
||||
@@ -7,6 +73,7 @@ services_steps: &services_steps
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: |
|
||||
npm install --dry-run
|
||||
npm ci
|
||||
environment:
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
@@ -25,10 +92,90 @@ services_steps: &services_steps
|
||||
- store_test_results:
|
||||
path: junit
|
||||
|
||||
package_steps: &package_steps
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- run:
|
||||
name: Install node and npm
|
||||
command: |
|
||||
set +e
|
||||
export NVM_DIR="/opt/circleci/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||
nvm install v14
|
||||
nvm use v14
|
||||
npm install -g npm
|
||||
|
||||
# Run the package tests on each currently supported node version. See:
|
||||
# https://github.com/badges/shields/blob/master/badge-maker/README.md#node-version-support
|
||||
# https://nodejs.org/en/about/releases/
|
||||
|
||||
- run:
|
||||
environment:
|
||||
mocha_reporter: mocha-junit-reporter
|
||||
MOCHA_FILE: junit/badge-maker/v12/results.xml
|
||||
NODE_VERSION: v12
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
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
|
||||
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.14
|
||||
environment:
|
||||
NPM_CONFIG_ENGINE_STRICT: 'true'
|
||||
NPM_CONFIG_STRICT_PEER_DEPS: 'true'
|
||||
|
||||
<<: *main_steps
|
||||
|
||||
main@node-17:
|
||||
docker:
|
||||
- image: cimg/node:17.7
|
||||
|
||||
<<: *main_steps
|
||||
|
||||
integration:
|
||||
docker:
|
||||
- image: cimg/node:16.14
|
||||
environment:
|
||||
NPM_CONFIG_ENGINE_STRICT: 'true'
|
||||
NPM_CONFIG_STRICT_PEER_DEPS: 'true'
|
||||
- image: redis
|
||||
|
||||
<<: *integration_steps
|
||||
|
||||
integration@node-17:
|
||||
docker:
|
||||
- image: cimg/node:17.7
|
||||
- image: redis
|
||||
|
||||
<<: *integration_steps
|
||||
|
||||
danger:
|
||||
docker:
|
||||
- image: cimg/node:16.15
|
||||
- image: cimg/node:16.14
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
@@ -48,14 +195,17 @@ jobs:
|
||||
|
||||
frontend:
|
||||
docker:
|
||||
- image: cimg/node:16.15
|
||||
|
||||
- image: cimg/node:16.14
|
||||
environment:
|
||||
NPM_CONFIG_ENGINE_STRICT: 'true'
|
||||
NPM_CONFIG_STRICT_PEER_DEPS: 'true'
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: |
|
||||
npm install --dry-run
|
||||
npm ci
|
||||
environment:
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
@@ -84,24 +234,33 @@ jobs:
|
||||
when: always
|
||||
command: npm run build
|
||||
|
||||
package:
|
||||
machine:
|
||||
image: 'ubuntu-2004:202111-02'
|
||||
|
||||
<<: *package_steps
|
||||
|
||||
services:
|
||||
docker:
|
||||
- image: cimg/node:16.15
|
||||
- image: cimg/node:16.14
|
||||
environment:
|
||||
NPM_CONFIG_ENGINE_STRICT: 'true'
|
||||
NPM_CONFIG_STRICT_PEER_DEPS: 'true'
|
||||
|
||||
<<: *services_steps
|
||||
|
||||
services@node-17:
|
||||
docker:
|
||||
- image: cimg/node:17.9
|
||||
environment:
|
||||
NPM_CONFIG_ENGINE_STRICT: 'false'
|
||||
- image: cimg/node:17.7
|
||||
|
||||
<<: *services_steps
|
||||
|
||||
e2e:
|
||||
docker:
|
||||
- image: cypress/base:16.14.0
|
||||
|
||||
- image: cypress/base:16.13.0
|
||||
environment:
|
||||
NPM_CONFIG_ENGINE_STRICT: 'true'
|
||||
NPM_CONFIG_STRICT_PEER_DEPS: 'true'
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
@@ -113,6 +272,7 @@ jobs:
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: |
|
||||
npm install --dry-run
|
||||
npm ci
|
||||
|
||||
- run:
|
||||
@@ -147,10 +307,26 @@ workflows:
|
||||
|
||||
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:
|
||||
@@ -180,6 +356,12 @@ workflows:
|
||||
# filters:
|
||||
# branches:
|
||||
# ignore: gh-pages
|
||||
# - main:
|
||||
# requires:
|
||||
# - npm-install
|
||||
# - main@node-latest:
|
||||
# requires:
|
||||
# - npm-install
|
||||
# - frontend:
|
||||
# requires:
|
||||
# - npm-install
|
||||
|
||||
@@ -144,8 +144,6 @@ rules:
|
||||
func-style: ['error', 'declaration', { 'allowArrowFunctions': true }]
|
||||
new-cap: ['error', { 'capIsNew': true }]
|
||||
import/order: ['error', { 'newlines-between': 'never' }]
|
||||
quotes:
|
||||
['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }]
|
||||
|
||||
# Account for destructuring responses from upstream services,
|
||||
# many of which do not follow camelcase
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/3_Badge_request.md
vendored
2
.github/ISSUE_TEMPLATE/3_Badge_request.md
vendored
@@ -21,7 +21,7 @@ A clear and concise description of the new badge.
|
||||
Where can we get the data from?
|
||||
|
||||
- Is there a public API?
|
||||
- Does the API require an API key?
|
||||
- Does the API requires an API key?
|
||||
- Link to the API documentation.
|
||||
-->
|
||||
|
||||
|
||||
2
.github/actions/close-bot/index.js
vendored
2
.github/actions/close-bot/index.js
vendored
@@ -27,7 +27,7 @@ async function run() {
|
||||
state: 'closed',
|
||||
})
|
||||
|
||||
core.debug('Done.')
|
||||
core.debug(`Done.`)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
67
.github/actions/close-bot/package-lock.json
generated
vendored
67
.github/actions/close-bot/package-lock.json
generated
vendored
@@ -9,36 +9,35 @@
|
||||
"version": "0.0.0",
|
||||
"license": "CC0",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.0",
|
||||
"@actions/github": "^5.1.1"
|
||||
"@actions/core": "^1.6.0",
|
||||
"@actions/github": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"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.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.6.0.tgz",
|
||||
"integrity": "sha512-NB1UAZomZlCV/LmJqkLhNTqtKfFXJZAUPcfl/zqG7EfsQdeUJtaWO98SGbuQ3pydJ3fHl2CvI/51OKYlCYYcaw==",
|
||||
"dependencies": {
|
||||
"@actions/http-client": "^2.0.1",
|
||||
"uuid": "^8.3.2"
|
||||
"@actions/http-client": "^1.0.11"
|
||||
}
|
||||
},
|
||||
"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.1",
|
||||
"resolved": "https://registry.npmjs.org/@actions/github/-/github-5.0.1.tgz",
|
||||
"integrity": "sha512-JZGyPM9ektb8NVTTI/2gfJ9DL7Rk98tQ7OVyTlgTuaQroariRBsOnzjy0I2EarX4xUZpK88YyO503fhmjFdyAg==",
|
||||
"dependencies": {
|
||||
"@actions/http-client": "^2.0.1",
|
||||
"@actions/http-client": "^1.0.11",
|
||||
"@octokit/core": "^3.6.0",
|
||||
"@octokit/plugin-paginate-rest": "^2.17.0",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^5.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/http-client": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz",
|
||||
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==",
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
|
||||
"integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
|
||||
"dependencies": {
|
||||
"tunnel": "^0.0.6"
|
||||
"tunnel": "0.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-token": {
|
||||
@@ -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,31 +226,30 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz",
|
||||
"integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==",
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.6.0.tgz",
|
||||
"integrity": "sha512-NB1UAZomZlCV/LmJqkLhNTqtKfFXJZAUPcfl/zqG7EfsQdeUJtaWO98SGbuQ3pydJ3fHl2CvI/51OKYlCYYcaw==",
|
||||
"requires": {
|
||||
"@actions/http-client": "^2.0.1",
|
||||
"uuid": "^8.3.2"
|
||||
"@actions/http-client": "^1.0.11"
|
||||
}
|
||||
},
|
||||
"@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.1",
|
||||
"resolved": "https://registry.npmjs.org/@actions/github/-/github-5.0.1.tgz",
|
||||
"integrity": "sha512-JZGyPM9ektb8NVTTI/2gfJ9DL7Rk98tQ7OVyTlgTuaQroariRBsOnzjy0I2EarX4xUZpK88YyO503fhmjFdyAg==",
|
||||
"requires": {
|
||||
"@actions/http-client": "^2.0.1",
|
||||
"@actions/http-client": "^1.0.11",
|
||||
"@octokit/core": "^3.6.0",
|
||||
"@octokit/plugin-paginate-rest": "^2.17.0",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^5.13.0"
|
||||
}
|
||||
},
|
||||
"@actions/http-client": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz",
|
||||
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==",
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
|
||||
"integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
|
||||
"requires": {
|
||||
"tunnel": "^0.0.6"
|
||||
"tunnel": "0.0.6"
|
||||
}
|
||||
},
|
||||
"@octokit/auth-token": {
|
||||
@@ -403,11 +393,6 @@
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
|
||||
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
|
||||
},
|
||||
"uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
||||
},
|
||||
"webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
|
||||
4
.github/actions/close-bot/package.json
vendored
4
.github/actions/close-bot/package.json
vendored
@@ -10,7 +10,7 @@
|
||||
"author": "chris48s",
|
||||
"license": "CC0",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.0",
|
||||
"@actions/github": "^5.1.1"
|
||||
"@actions/core": "^1.6.0",
|
||||
"@actions/github": "^5.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
21
.github/actions/core-tests/action.yml
vendored
21
.github/actions/core-tests/action.yml
vendored
@@ -1,21 +0,0 @@
|
||||
name: 'Core tests'
|
||||
description: 'Run core and entrypoint tests'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Core tests
|
||||
if: always()
|
||||
run: npm run test:core -- --reporter json --reporter-option 'output=reports/core.json'
|
||||
shell: bash
|
||||
|
||||
- name: Entrypoint tests
|
||||
if: always()
|
||||
run: npm run test:entrypoint -- --reporter json --reporter-option 'output=reports/entrypoint.json'
|
||||
shell: bash
|
||||
|
||||
- name: Write Markdown Summary
|
||||
if: always()
|
||||
run: |
|
||||
node scripts/mocha2md.js Core reports/core.json >> $GITHUB_STEP_SUMMARY
|
||||
node scripts/mocha2md.js Entrypoint reports/entrypoint.json >> $GITHUB_STEP_SUMMARY
|
||||
shell: bash
|
||||
20
.github/actions/integration-tests/action.yml
vendored
20
.github/actions/integration-tests/action.yml
vendored
@@ -1,20 +0,0 @@
|
||||
name: 'Integration tests'
|
||||
description: 'Run integration tests'
|
||||
inputs:
|
||||
github-token:
|
||||
description: 'The GITHUB_TOKEN secret'
|
||||
required: true
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Integration Tests
|
||||
if: always()
|
||||
run: npm run test:integration -- --reporter json --reporter-option 'output=reports/integration-tests.json'
|
||||
env:
|
||||
GH_TOKEN: '${{ inputs.github-token }}'
|
||||
shell: bash
|
||||
|
||||
- name: Write Markdown Summary
|
||||
if: always()
|
||||
run: node scripts/mocha2md.js Integration reports/integration-tests.json >> $GITHUB_STEP_SUMMARY
|
||||
shell: bash
|
||||
26
.github/actions/package-tests/action.yml
vendored
26
.github/actions/package-tests/action.yml
vendored
@@ -1,26 +0,0 @@
|
||||
name: 'Package tests'
|
||||
description: 'Run package tests and check types'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Tests
|
||||
if: always()
|
||||
run: npm run test:package -- --reporter json --reporter-option 'output=reports/package-tests.json'
|
||||
shell: bash
|
||||
|
||||
- name: Type Checks
|
||||
if: always()
|
||||
run: |
|
||||
set -o pipefail
|
||||
npm run check-types:package 2>&1 | tee reports/package-types.txt
|
||||
shell: bash
|
||||
|
||||
- name: Write Markdown Summary
|
||||
if: always()
|
||||
run: |
|
||||
node scripts/mocha2md.js 'Package Tests' reports/package-tests.json >> $GITHUB_STEP_SUMMARY
|
||||
echo '# Package Types' >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
cat reports/package-types.txt >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
shell: bash
|
||||
25
.github/actions/setup/action.yml
vendored
25
.github/actions/setup/action.yml
vendored
@@ -1,25 +0,0 @@
|
||||
name: 'Set up project'
|
||||
description: 'Set up project'
|
||||
inputs:
|
||||
node-version:
|
||||
description: 'Version Spec of the version to use. Examples: 12.x, 10.15.1, >=10.15.0.'
|
||||
required: true
|
||||
cypress:
|
||||
description: 'Install Cypress binary: 0 or 1'
|
||||
# https://docs.cypress.io/guides/getting-started/installing-cypress.html#Skipping-installation
|
||||
# We don't need to install the Cypress binary in jobs that aren't actually running Cypress.
|
||||
required: false
|
||||
default: 0
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Install Node JS ${{ inputs.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
env:
|
||||
CYPRESS_INSTALL_BINARY: ${{ inputs.cypress }}
|
||||
run: npm ci
|
||||
shell: bash
|
||||
5
.github/dependabot.yml
vendored
5
.github/dependabot.yml
vendored
@@ -36,8 +36,3 @@ updates:
|
||||
day: friday
|
||||
time: '12:00'
|
||||
open-pull-requests-limit: 99
|
||||
- package-ecosystem: 'github-actions'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 99
|
||||
|
||||
8
.github/workflows/auto-close.yml
vendored
8
.github/workflows/auto-close.yml
vendored
@@ -1,18 +1,16 @@
|
||||
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:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install action dependencies
|
||||
run: cd .github/actions/close-bot && npm ci
|
||||
|
||||
8
.github/workflows/build-docker-image.yml
vendored
8
.github/workflows/build-docker-image.yml
vendored
@@ -3,20 +3,20 @@ on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build-docker-image:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Set Git Short SHA
|
||||
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
|
||||
11
.github/workflows/create-release.yml
vendored
11
.github/workflows/create-release.yml
vendored
@@ -4,9 +4,6 @@ on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
if: |
|
||||
@@ -23,7 +20,7 @@ jobs:
|
||||
run: echo "::set-output name=date::$(date --rfc-3339=date)"
|
||||
|
||||
- name: Checkout branch "master"
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: 'master'
|
||||
|
||||
@@ -34,16 +31,16 @@ jobs:
|
||||
tag: server-${{ steps.date.outputs.date }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push snapshot release to DockerHub
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
|
||||
17
.github/workflows/deploy-docs.yml
vendored
17
.github/workflows/deploy-docs.yml
vendored
@@ -3,16 +3,12 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
deploy-docs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v2.3.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -22,8 +18,9 @@ jobs:
|
||||
npm run build-docs
|
||||
|
||||
- name: Deploy
|
||||
uses: JamesIves/github-pages-deploy-action@v4
|
||||
uses: JamesIves/github-pages-deploy-action@3.7.1
|
||||
with:
|
||||
branch: gh-pages
|
||||
folder: api-docs
|
||||
clean: true
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BRANCH: gh-pages
|
||||
FOLDER: api-docs
|
||||
CLEAN: true
|
||||
|
||||
8
.github/workflows/draft-release.yml
vendored
8
.github/workflows/draft-release.yml
vendored
@@ -5,16 +5,12 @@ on:
|
||||
# At 01:00 on the first day of every month
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
draft-release:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Draft Release
|
||||
uses: ./.github/actions/draft-release
|
||||
|
||||
11
.github/workflows/enforce-dependency-review.yml
vendored
11
.github/workflows/enforce-dependency-review.yml
vendored
@@ -1,13 +1,14 @@
|
||||
name: 'Dependency Review'
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
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@v2
|
||||
uses: actions/dependency-review-action@v1
|
||||
|
||||
10
.github/workflows/publish-docker-next.yml
vendored
10
.github/workflows/publish-docker-next.yml
vendored
@@ -5,17 +5,17 @@ on:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
publish-docker-next:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
|
||||
48
.github/workflows/test-integration-17.yml
vendored
48
.github/workflows/test-integration-17.yml
vendored
@@ -1,48 +0,0 @@
|
||||
name: Integration@node 17
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
- 'dependabot/**'
|
||||
|
||||
jobs:
|
||||
test-integration-17:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PAT_EXISTS: ${{ secrets.GH_PAT != '' }}
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: 17
|
||||
env:
|
||||
NPM_CONFIG_ENGINE_STRICT: 'false'
|
||||
|
||||
- name: Integration Tests (with PAT)
|
||||
if: ${{ env.PAT_EXISTS == 'true' }}
|
||||
uses: ./.github/actions/integration-tests
|
||||
with:
|
||||
github-token: '${{ secrets.GH_PAT }}'
|
||||
|
||||
- name: Integration Tests (with workflow token)
|
||||
if: ${{ env.PAT_EXISTS == 'false' }}
|
||||
uses: ./.github/actions/integration-tests
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
46
.github/workflows/test-integration.yml
vendored
46
.github/workflows/test-integration.yml
vendored
@@ -1,46 +0,0 @@
|
||||
name: Integration
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
- 'dependabot/**'
|
||||
|
||||
jobs:
|
||||
test-integration:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PAT_EXISTS: ${{ secrets.GH_PAT != '' }}
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Integration Tests (with PAT)
|
||||
if: ${{ env.PAT_EXISTS == 'true' }}
|
||||
uses: ./.github/actions/integration-tests
|
||||
with:
|
||||
github-token: '${{ secrets.GH_PAT }}'
|
||||
|
||||
- name: Integration Tests (with workflow token)
|
||||
if: ${{ env.PAT_EXISTS == 'false' }}
|
||||
uses: ./.github/actions/integration-tests
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
28
.github/workflows/test-lint.yml
vendored
28
.github/workflows/test-lint.yml
vendored
@@ -1,28 +0,0 @@
|
||||
name: Lint
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
- 'dependabot/**'
|
||||
|
||||
jobs:
|
||||
test-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: ESLint
|
||||
if: always()
|
||||
run: npm run lint
|
||||
|
||||
- name: 'Prettier check (quick fix: `npm run prettier`)'
|
||||
if: always()
|
||||
run: npm run prettier:check
|
||||
25
.github/workflows/test-main-17.yml
vendored
25
.github/workflows/test-main-17.yml
vendored
@@ -1,25 +0,0 @@
|
||||
name: Main@node 17
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
- 'dependabot/**'
|
||||
|
||||
jobs:
|
||||
test-main-17:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: 17
|
||||
env:
|
||||
NPM_CONFIG_ENGINE_STRICT: 'false'
|
||||
|
||||
- name: Core tests
|
||||
uses: ./.github/actions/core-tests
|
||||
28
.github/workflows/test-main.yml
vendored
28
.github/workflows/test-main.yml
vendored
@@ -1,28 +0,0 @@
|
||||
name: Main
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
- 'dependabot/**'
|
||||
|
||||
jobs:
|
||||
test-main:
|
||||
strategy:
|
||||
matrix:
|
||||
os: ['ubuntu-latest', 'windows-latest']
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Core tests
|
||||
uses: ./.github/actions/core-tests
|
||||
46
.github/workflows/test-package-cli.yml
vendored
46
.github/workflows/test-package-cli.yml
vendored
@@ -1,46 +0,0 @@
|
||||
name: Package CLI
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
- 'dependabot/**'
|
||||
|
||||
# Smoke test (render a badge with the CLI) with only the package
|
||||
# dependencies installed.
|
||||
|
||||
jobs:
|
||||
test-package-cli:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- node: '14'
|
||||
engine-strict: 'false'
|
||||
- node: '16'
|
||||
engine-strict: 'false'
|
||||
- node: '18'
|
||||
engine-strict: 'true'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Node JS ${{ inputs.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install dependencies
|
||||
env:
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
NPM_CONFIG_ENGINE_STRICT: ${{ matrix.engine-strict }}
|
||||
run: |
|
||||
cd badge-maker
|
||||
npm install
|
||||
npm link
|
||||
|
||||
- name: Render a badge with the CLI
|
||||
run: |
|
||||
cd badge-maker
|
||||
badge cactus grown :green @flat
|
||||
34
.github/workflows/test-package-lib.yml
vendored
34
.github/workflows/test-package-lib.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: Package Library
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
- 'dependabot/**'
|
||||
|
||||
jobs:
|
||||
test-package-lib:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- node: '14'
|
||||
engine-strict: 'false'
|
||||
- node: '16'
|
||||
engine-strict: 'true'
|
||||
- node: '18'
|
||||
engine-strict: 'false'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
env:
|
||||
NPM_CONFIG_ENGINE_STRICT: ${{ matrix.engine-strict }}
|
||||
|
||||
- name: Package tests
|
||||
uses: ./.github/actions/package-tests
|
||||
106
CHANGELOG.md
106
CHANGELOG.md
@@ -4,112 +4,6 @@ Note: this changelog is for the shields.io server. The changelog for the badge-m
|
||||
|
||||
---
|
||||
|
||||
## server-2022-11-01
|
||||
|
||||
- fix [githubpullrequestcheckstate] service tests [#8548](https://github.com/badges/shields/issues/8548)
|
||||
- require array to have at least 1 item in [githublastcommit] [#8563](https://github.com/badges/shields/issues/8563)
|
||||
- [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)
|
||||
- fixes for integration test rate limit issues [#8538](https://github.com/badges/shields/issues/8538)
|
||||
- remove failing [lgtm] test case [#8547](https://github.com/badges/shields/issues/8547)
|
||||
- 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)
|
||||
- update [bitbucket] private repo test [#8520](https://github.com/badges/shields/issues/8520)
|
||||
- update [powershellgallery] platform example [#8518](https://github.com/badges/shields/issues/8518)
|
||||
- fix [myget] downloads (tenant) test [#8513](https://github.com/badges/shields/issues/8513)
|
||||
- [Whatpulse] badge for a user and for a team [#8466](https://github.com/badges/shields/issues/8466)
|
||||
- increase timeout for [reuse] not found test [#8519](https://github.com/badges/shields/issues/8519)
|
||||
- update [JenkinsPluginInstalls] test example [#8511](https://github.com/badges/shields/issues/8511)
|
||||
- migrate integration tests to GH actions [#8423](https://github.com/badges/shields/issues/8423)
|
||||
- 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)
|
||||
- fix [gitlab] auth [#8145](https://github.com/badges/shields/issues/8145) [#8162](https://github.com/badges/shields/issues/8162)
|
||||
- increase cache length on AUR version badge, run [AUR] [#8110](https://github.com/badges/shields/issues/8110)
|
||||
- Use GraphQL to fix GitHub file count badges [github] [#8112](https://github.com/badges/shields/issues/8112)
|
||||
- feat: add [gitlab] contributors service [#8084](https://github.com/badges/shields/issues/8084)
|
||||
- [greasyfork] Add Greasy Fork service badges [#8080](https://github.com/badges/shields/issues/8080)
|
||||
- Add [gitlablicense] services [#8024](https://github.com/badges/shields/issues/8024)
|
||||
- [Spack] Package Manager: Update Domain [#8046](https://github.com/badges/shields/issues/8046)
|
||||
- switch [jitpack] to use latestOk endpoint [#8041](https://github.com/badges/shields/issues/8041)
|
||||
- Dependency updates
|
||||
|
||||
## server-2022-06-01
|
||||
|
||||
- Update GitLab logo (2022) [#7984](https://github.com/badges/shields/issues/7984)
|
||||
- [GitHub] Added milestone property to GitHub issue details service [#7864](https://github.com/badges/shields/issues/7864)
|
||||
- [Spack] Package Manager: Update Endpoint [#7957](https://github.com/badges/shields/issues/7957)
|
||||
- Update Chocolatey API endpoint URL [#7952](https://github.com/badges/shields/issues/7952)
|
||||
- [Flathub]Add downloads badge [#7724](https://github.com/badges/shields/issues/7724)
|
||||
- replace the outdated Telegram logo with the newest [#7831](https://github.com/badges/shields/issues/7831)
|
||||
- add [PUB] points badge [#7918](https://github.com/badges/shields/issues/7918)
|
||||
- add [PUB] popularity badge [#7920](https://github.com/badges/shields/issues/7920)
|
||||
- add [PUB] likes badge [#7916](https://github.com/badges/shields/issues/7916)
|
||||
- Dependency updates
|
||||
|
||||
## server-2022-05-03
|
||||
|
||||
- [OSSFScorecard] Create scorecard badge service [#7687](https://github.com/badges/shields/issues/7687)
|
||||
- Stringify [githublanguagecount] message [#7881](https://github.com/badges/shields/issues/7881)
|
||||
- Stringify and trim whitespace from a few services [#7880](https://github.com/badges/shields/issues/7880)
|
||||
- add labels to Dockerfile [#7862](https://github.com/badges/shields/issues/7862)
|
||||
- handle missing 'fly-client-ip' [#7814](https://github.com/badges/shields/issues/7814)
|
||||
- Dependency updates
|
||||
|
||||
## server-2022-04-03
|
||||
|
||||
- Breaking change: This release updates ioredis from v4 to v5.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ COPY package.json package-lock.json /usr/src/app/
|
||||
COPY badge-maker /usr/src/app/badge-maker/
|
||||
|
||||
RUN apk add python3 make g++
|
||||
RUN npm install -g "npm@>=8"
|
||||
RUN npm install -g "npm@>=7"
|
||||
# We need dev deps to build the front end. We don't need Cypress, though.
|
||||
RUN NODE_ENV=development CYPRESS_INSTALL_BINARY=0 npm ci
|
||||
|
||||
|
||||
10
app.json
10
app.json
@@ -35,16 +35,6 @@
|
||||
"WEBLATE_API_KEY": {
|
||||
"description": "Configure the API key to be used for the Weblate service.",
|
||||
"required": false
|
||||
},
|
||||
"METRICS_INFLUX_ENABLED": {
|
||||
"description": "Disable influx metrics",
|
||||
"value": "false",
|
||||
"required": false
|
||||
},
|
||||
"REQUIRE_CLOUDFLARE": {
|
||||
"description": "Allow direct traffic",
|
||||
"value": "false",
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"formation": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 4.0.0 [WIP]
|
||||
|
||||
- Drop compatibility with Node < 14
|
||||
- Drop compatibility with Node 10
|
||||
|
||||
## 3.3.1
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict'
|
||||
|
||||
const { normalizeColor, toSvgColor } = require('./color')
|
||||
const { toSvgColor } = require('./color')
|
||||
const badgeRenderers = require('./badge-renderers')
|
||||
const { stripXmlWhitespace } = require('./xml')
|
||||
|
||||
@@ -9,7 +9,6 @@ note: makeBadge() is fairly thinly wrapped so if we are making changes here
|
||||
it is likely this will impact on the package's public interface in index.js
|
||||
*/
|
||||
module.exports = function makeBadge({
|
||||
format,
|
||||
style = 'flat',
|
||||
label,
|
||||
message,
|
||||
@@ -24,22 +23,6 @@ module.exports = function makeBadge({
|
||||
label = `${label}`.trim()
|
||||
message = `${message}`.trim()
|
||||
|
||||
// This ought to be the responsibility of the server, not `makeBadge`.
|
||||
if (format === 'json') {
|
||||
return JSON.stringify({
|
||||
label,
|
||||
message,
|
||||
logoWidth,
|
||||
// Only call normalizeColor for the JSON case: this is handled
|
||||
// internally by toSvgColor in the SVG case.
|
||||
color: normalizeColor(color),
|
||||
labelColor: normalizeColor(labelColor),
|
||||
link: links,
|
||||
name: label,
|
||||
value: message,
|
||||
})
|
||||
}
|
||||
|
||||
const render = badgeRenderers[style]
|
||||
if (!render) {
|
||||
throw new Error(`Unknown badge style: '${style}'`)
|
||||
|
||||
@@ -1,143 +1,48 @@
|
||||
'use strict'
|
||||
|
||||
const { test, given, forCases } = require('sazerac')
|
||||
const { expect } = require('chai')
|
||||
const snapshot = require('snap-shot-it')
|
||||
const isSvg = require('is-svg')
|
||||
const prettier = require('prettier')
|
||||
const makeBadge = require('./make-badge')
|
||||
|
||||
function expectBadgeToMatchSnapshot(format) {
|
||||
snapshot(prettier.format(makeBadge(format), { parser: 'html' }))
|
||||
}
|
||||
|
||||
function testColor(color = '', colorAttr = 'color') {
|
||||
return JSON.parse(
|
||||
makeBadge({
|
||||
label: 'name',
|
||||
message: 'Bob',
|
||||
[colorAttr]: color,
|
||||
format: 'json',
|
||||
})
|
||||
).color
|
||||
function expectBadgeToMatchSnapshot(badgeData) {
|
||||
snapshot(prettier.format(makeBadge(badgeData), { parser: 'html' }))
|
||||
}
|
||||
|
||||
describe('The badge generator', function () {
|
||||
describe('color test', function () {
|
||||
test(testColor, () => {
|
||||
// valid hex
|
||||
forCases([
|
||||
given('#4c1'),
|
||||
given('#4C1'),
|
||||
given('4C1'),
|
||||
given('4c1'),
|
||||
]).expect('#4c1')
|
||||
forCases([
|
||||
given('#abc123'),
|
||||
given('#ABC123'),
|
||||
given('abc123'),
|
||||
given('ABC123'),
|
||||
]).expect('#abc123')
|
||||
// valid rgb(a)
|
||||
given('rgb(0,128,255)').expect('rgb(0,128,255)')
|
||||
given('rgb(220,128,255,0.5)').expect('rgb(220,128,255,0.5)')
|
||||
given('rgba(0,0,255)').expect('rgba(0,0,255)')
|
||||
given('rgba(0,128,255,0)').expect('rgba(0,128,255,0)')
|
||||
// valid hsl(a)
|
||||
given('hsl(100, 56%, 10%)').expect('hsl(100, 56%, 10%)')
|
||||
given('hsl(360,50%,50%,0.5)').expect('hsl(360,50%,50%,0.5)')
|
||||
given('hsla(25,20%,0%,0.1)').expect('hsla(25,20%,0%,0.1)')
|
||||
given('hsla(0,50%,101%)').expect('hsla(0,50%,101%)')
|
||||
// CSS named color.
|
||||
given('papayawhip').expect('papayawhip')
|
||||
// Shields named color.
|
||||
given('red').expect('red')
|
||||
given('green').expect('green')
|
||||
given('blue').expect('blue')
|
||||
given('yellow').expect('yellow')
|
||||
// Semantic color alias
|
||||
given('success').expect('brightgreen')
|
||||
given('informational').expect('blue')
|
||||
|
||||
forCases(
|
||||
// invalid hex
|
||||
given('#123red'), // contains letter above F
|
||||
given('#red'), // contains letter above F
|
||||
// neither a css named color nor colorscheme
|
||||
given('notacolor'),
|
||||
given('bluish'),
|
||||
given('almostred'),
|
||||
given('brightmaroon'),
|
||||
given('cactus')
|
||||
).expect(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe('color aliases', function () {
|
||||
test(testColor, () => {
|
||||
forCases([given('#4c1', 'color')]).expect('#4c1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('SVG', function () {
|
||||
it('should produce SVG', function () {
|
||||
expect(makeBadge({ label: 'cactus', message: 'grown', format: 'svg' }))
|
||||
expect(makeBadge({ label: 'cactus', message: 'grown' }))
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('cactus')
|
||||
.and.to.include('grown')
|
||||
})
|
||||
|
||||
it('should match snapshot', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('JSON', function () {
|
||||
it('should produce the expected JSON', function () {
|
||||
const json = makeBadge({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'json',
|
||||
links: ['https://example.com/', 'https://other.example.com/'],
|
||||
})
|
||||
expect(JSON.parse(json)).to.deep.equal({
|
||||
name: 'cactus',
|
||||
label: 'cactus',
|
||||
value: 'grown',
|
||||
message: 'grown',
|
||||
link: ['https://example.com/', 'https://other.example.com/'],
|
||||
})
|
||||
expectBadgeToMatchSnapshot({ label: 'cactus', message: 'grown' })
|
||||
})
|
||||
|
||||
it('should replace undefined svg badge style with "flat"', function () {
|
||||
const jsonBadgeWithUnknownStyle = makeBadge({
|
||||
label: 'name',
|
||||
message: 'Bob',
|
||||
format: 'svg',
|
||||
})
|
||||
const jsonBadgeWithDefaultStyle = makeBadge({
|
||||
label: 'name',
|
||||
message: 'Bob',
|
||||
format: 'svg',
|
||||
style: 'flat',
|
||||
})
|
||||
expect(jsonBadgeWithUnknownStyle)
|
||||
.to.equal(jsonBadgeWithDefaultStyle)
|
||||
.and.to.satisfy(isSvg)
|
||||
expect(
|
||||
makeBadge({
|
||||
label: 'name',
|
||||
message: 'Bob',
|
||||
})
|
||||
)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.equal(
|
||||
makeBadge({
|
||||
label: 'name',
|
||||
message: 'Bob',
|
||||
style: 'flat',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should fail with unknown svg badge style', function () {
|
||||
expect(() =>
|
||||
makeBadge({
|
||||
label: 'name',
|
||||
message: 'Bob',
|
||||
format: 'svg',
|
||||
style: 'unknown_style',
|
||||
})
|
||||
makeBadge({ label: 'name', message: 'Bob', style: 'unknown_style' })
|
||||
).to.throw(Error, "Unknown badge style: 'unknown_style'")
|
||||
})
|
||||
})
|
||||
@@ -147,7 +52,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -158,7 +62,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -170,7 +73,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat',
|
||||
color: '#b3e',
|
||||
})
|
||||
@@ -180,7 +82,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat',
|
||||
color: '#b3e',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
@@ -191,7 +92,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -203,7 +103,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -215,7 +114,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat',
|
||||
color: '#000',
|
||||
labelColor: '#f3f3f3',
|
||||
@@ -226,7 +124,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat',
|
||||
color: '#e2ffe1',
|
||||
labelColor: '#000',
|
||||
@@ -239,7 +136,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat-square',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -250,7 +146,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat-square',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -262,7 +157,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat-square',
|
||||
color: '#b3e',
|
||||
})
|
||||
@@ -272,7 +166,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat-square',
|
||||
color: '#b3e',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
@@ -283,7 +176,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat-square',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -295,7 +187,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat-square',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -307,7 +198,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat-square',
|
||||
color: '#000',
|
||||
labelColor: '#f3f3f3',
|
||||
@@ -318,7 +208,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat-square',
|
||||
color: '#e2ffe1',
|
||||
labelColor: '#000',
|
||||
@@ -331,7 +220,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'plastic',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -342,7 +230,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'plastic',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -354,7 +241,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'plastic',
|
||||
color: '#b3e',
|
||||
})
|
||||
@@ -364,7 +250,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'plastic',
|
||||
color: '#b3e',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
@@ -375,7 +260,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'plastic',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -387,7 +271,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'plastic',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -399,7 +282,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'plastic',
|
||||
color: '#000',
|
||||
labelColor: '#f3f3f3',
|
||||
@@ -410,7 +292,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'plastic',
|
||||
color: '#e2ffe1',
|
||||
labelColor: '#000',
|
||||
@@ -425,7 +306,6 @@ describe('The badge generator', function () {
|
||||
makeBadge({
|
||||
label: 1998,
|
||||
message: 1999,
|
||||
format: 'svg',
|
||||
style: 'for-the-badge',
|
||||
})
|
||||
)
|
||||
@@ -438,7 +318,6 @@ describe('The badge generator', function () {
|
||||
makeBadge({
|
||||
label: 'Label',
|
||||
message: '1 string',
|
||||
format: 'svg',
|
||||
style: 'for-the-badge',
|
||||
})
|
||||
)
|
||||
@@ -450,7 +329,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'for-the-badge',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -461,7 +339,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'for-the-badge',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -473,7 +350,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'for-the-badge',
|
||||
color: '#b3e',
|
||||
})
|
||||
@@ -483,7 +359,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'for-the-badge',
|
||||
color: '#b3e',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
@@ -494,7 +369,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'for-the-badge',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -506,7 +380,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'for-the-badge',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -518,7 +391,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'for-the-badge',
|
||||
color: '#000',
|
||||
labelColor: '#f3f3f3',
|
||||
@@ -529,7 +401,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'for-the-badge',
|
||||
color: '#e2ffe1',
|
||||
labelColor: '#000',
|
||||
@@ -543,7 +414,6 @@ describe('The badge generator', function () {
|
||||
makeBadge({
|
||||
label: 'some-key',
|
||||
message: 'some-value',
|
||||
format: 'svg',
|
||||
style: 'social',
|
||||
})
|
||||
)
|
||||
@@ -557,11 +427,10 @@ describe('The badge generator', function () {
|
||||
makeBadge({
|
||||
label: '',
|
||||
message: 'some-value',
|
||||
format: 'json',
|
||||
style: 'social',
|
||||
})
|
||||
)
|
||||
.to.include('""')
|
||||
.to.include('></text>')
|
||||
.and.to.include('some-value')
|
||||
})
|
||||
|
||||
@@ -569,7 +438,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'social',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -580,7 +448,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'social',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -592,7 +459,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'social',
|
||||
color: '#b3e',
|
||||
})
|
||||
@@ -602,7 +468,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'social',
|
||||
color: '#b3e',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
@@ -613,7 +478,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'social',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -625,7 +489,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'social',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -639,7 +502,6 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'label',
|
||||
message: 'message',
|
||||
format: 'svg',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
})
|
||||
})
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"badge": "lib/badge-cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14",
|
||||
"node": ">= 12",
|
||||
"npm": ">= 6"
|
||||
},
|
||||
"collective": {
|
||||
|
||||
@@ -6,20 +6,13 @@ public:
|
||||
enabled: true
|
||||
url: https://metrics.shields.io/telegraf
|
||||
instanceIdFrom: env-var
|
||||
instanceIdEnvVarName: FLY_ALLOC_ID
|
||||
instanceIdEnvVarName: HEROKU_DYNO_ID
|
||||
envLabel: shields-production
|
||||
|
||||
ssl:
|
||||
isSecure: false
|
||||
isSecure: true
|
||||
|
||||
cors:
|
||||
allowedOrigin: ['http://shields.io', 'https://shields.io']
|
||||
|
||||
services:
|
||||
gitlab:
|
||||
authorizedOrigins: 'https://gitlab.com'
|
||||
|
||||
rasterUrl: 'https://raster.shields.io'
|
||||
userAgentBase: 'Shields.io'
|
||||
requireCloudflare: true
|
||||
requestTimeoutSeconds: 20
|
||||
|
||||
@@ -1,58 +1,29 @@
|
||||
import makeBadge from '../../badge-maker/lib/make-badge.js'
|
||||
import BaseService from './base.js'
|
||||
import {
|
||||
serverHasBeenUpSinceResourceCached,
|
||||
setCacheHeadersForStaticResource,
|
||||
} from './cache-headers.js'
|
||||
import { makeSend } from './legacy-result-sender.js'
|
||||
import { MetricHelper } from './metric-helper.js'
|
||||
import coalesceBadge from './coalesce-badge.js'
|
||||
import { prepareRoute, namedParamsForMatch } from './route.js'
|
||||
import { prepareRoute } from './route.js'
|
||||
|
||||
export default class BaseStaticService extends BaseService {
|
||||
static register({ camp, metricInstance }, serviceConfig) {
|
||||
const { regex, captureNames } = prepareRoute(this.route)
|
||||
static _applyCacheHeaders({ res }) {
|
||||
setCacheHeadersForStaticResource(res)
|
||||
}
|
||||
|
||||
const metricHelper = MetricHelper.create({
|
||||
metricInstance,
|
||||
ServiceClass: this,
|
||||
})
|
||||
|
||||
camp.route(regex, async (queryParams, match, end, ask) => {
|
||||
if (serverHasBeenUpSinceResourceCached(ask.req)) {
|
||||
// Send Not Modified.
|
||||
ask.res.statusCode = 304
|
||||
ask.res.end()
|
||||
return
|
||||
}
|
||||
|
||||
const metricHandle = metricHelper.startRequest()
|
||||
|
||||
const namedParams = namedParamsForMatch(captureNames, match, this)
|
||||
const serviceData = await this.invoke(
|
||||
{},
|
||||
serviceConfig,
|
||||
namedParams,
|
||||
queryParams
|
||||
)
|
||||
|
||||
const badgeData = coalesceBadge(
|
||||
queryParams,
|
||||
serviceData,
|
||||
this.defaultBadgeData,
|
||||
this
|
||||
)
|
||||
|
||||
// The final capture group is the extension.
|
||||
const format = (match.slice(-1)[0] || '.svg').replace(/^\./, '')
|
||||
badgeData.format = format
|
||||
|
||||
setCacheHeadersForStaticResource(ask.res)
|
||||
|
||||
const svg = makeBadge(badgeData)
|
||||
makeSend(format, ask.res, end)(svg)
|
||||
|
||||
metricHandle.noteResponseSent()
|
||||
})
|
||||
static register({ app, ...serviceContext }, serviceConfig) {
|
||||
const { regex } = prepareRoute(this.route)
|
||||
app.get(
|
||||
regex,
|
||||
(req, res, next) => {
|
||||
if (serverHasBeenUpSinceResourceCached(req)) {
|
||||
// Send Not Modified.
|
||||
res.status(304)
|
||||
res.end()
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
},
|
||||
this.makeExpressHandler(serviceContext, serviceConfig)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,13 @@
|
||||
import emojic from 'emojic'
|
||||
import Joi from 'joi'
|
||||
import log from '../server/log.js'
|
||||
import makeBadge from '../../badge-maker/lib/make-badge.js'
|
||||
import { AuthHelper } from './auth-helper.js'
|
||||
import { MetricHelper, MetricNames } from './metric-helper.js'
|
||||
import {
|
||||
coalesceCacheLength,
|
||||
setHeadersForCacheLength,
|
||||
} from './cache-headers.js'
|
||||
import { assertValidCategory } from './categories.js'
|
||||
import checkErrorResponse from './check-error-response.js'
|
||||
import coalesceBadge from './coalesce-badge.js'
|
||||
@@ -21,11 +26,12 @@ import {
|
||||
} from './errors.js'
|
||||
import { validateExample, transformExample } from './examples.js'
|
||||
import { fetch } from './got.js'
|
||||
import { makeJsonBadge } from './make-json-badge.js'
|
||||
import {
|
||||
makeFullUrl,
|
||||
assertValidRoute,
|
||||
paramsForReq,
|
||||
prepareRoute,
|
||||
namedParamsForMatch,
|
||||
getQueryParamNames,
|
||||
} from './route.js'
|
||||
import { assertValidServiceDefinition } from './service-definitions.js'
|
||||
@@ -423,60 +429,90 @@ class BaseService {
|
||||
return serviceData
|
||||
}
|
||||
|
||||
static register(
|
||||
{
|
||||
camp,
|
||||
handleRequest,
|
||||
githubApiProvider,
|
||||
librariesIoApiProvider,
|
||||
metricInstance,
|
||||
},
|
||||
// `defaultCacheLengthSeconds` can be overridden by
|
||||
// `serviceDefaultCacheLengthSeconds` (either by category or on a badge-
|
||||
// by-badge basis). Then in turn that can be overridden by
|
||||
// `serviceOverrideCacheLengthSeconds` (which we expect to be used only in
|
||||
// the dynamic badge) but only if `serviceOverrideCacheLengthSeconds` is
|
||||
// longer than `serviceDefaultCacheLengthSeconds` and then the `cacheSeconds`
|
||||
// query param can also override both of those but again only if `cacheSeconds`
|
||||
// is longer.
|
||||
//
|
||||
// Ref: https://github.com/badges/shields/pull/2755
|
||||
static _applyCacheHeaders({
|
||||
cacheHeaderConfig,
|
||||
req,
|
||||
res,
|
||||
serviceOverrideCacheLengthSeconds,
|
||||
}) {
|
||||
const cacheLengthSeconds = coalesceCacheLength({
|
||||
cacheHeaderConfig,
|
||||
serviceDefaultCacheLengthSeconds: this._cacheLength,
|
||||
serviceOverrideCacheLengthSeconds,
|
||||
queryParams: req.query,
|
||||
})
|
||||
setHeadersForCacheLength(res, cacheLengthSeconds)
|
||||
}
|
||||
|
||||
static makeExpressHandler(
|
||||
{ githubApiProvider, librariesIoApiProvider, metricInstance },
|
||||
serviceConfig
|
||||
) {
|
||||
const { cacheHeaders: cacheHeaderConfig } = serviceConfig
|
||||
const { regex, captureNames } = prepareRoute(this.route)
|
||||
const queryParams = getQueryParamNames(this.route)
|
||||
|
||||
const metricHelper = MetricHelper.create({
|
||||
metricInstance,
|
||||
ServiceClass: this,
|
||||
})
|
||||
const { captureNames } = prepareRoute(this.route)
|
||||
const { cacheHeaders: cacheHeaderConfig } = serviceConfig
|
||||
|
||||
camp.route(
|
||||
regex,
|
||||
handleRequest(cacheHeaderConfig, {
|
||||
queryParams,
|
||||
handler: async (queryParams, match, sendBadge) => {
|
||||
const metricHandle = metricHelper.startRequest()
|
||||
return async (req, res) => {
|
||||
const metricHandle = metricHelper.startRequest()
|
||||
|
||||
const namedParams = namedParamsForMatch(captureNames, match, this)
|
||||
const serviceData = await this.invoke(
|
||||
{
|
||||
requestFetcher: fetch,
|
||||
githubApiProvider,
|
||||
librariesIoApiProvider,
|
||||
metricHelper,
|
||||
},
|
||||
serviceConfig,
|
||||
namedParams,
|
||||
queryParams
|
||||
)
|
||||
|
||||
const badgeData = coalesceBadge(
|
||||
queryParams,
|
||||
serviceData,
|
||||
this.defaultBadgeData,
|
||||
this
|
||||
)
|
||||
// The final capture group is the extension.
|
||||
const format = (match.slice(-1)[0] || '.svg').replace(/^\./, '')
|
||||
sendBadge(format, badgeData)
|
||||
|
||||
metricHandle.noteResponseSent()
|
||||
const { namedParams, format } = paramsForReq(captureNames, req, this)
|
||||
const serviceData = await this.invoke(
|
||||
{
|
||||
requestFetcher: fetch,
|
||||
githubApiProvider,
|
||||
librariesIoApiProvider,
|
||||
metricHelper,
|
||||
},
|
||||
cacheLength: this._cacheLength,
|
||||
serviceConfig,
|
||||
namedParams,
|
||||
req.query
|
||||
)
|
||||
|
||||
const badgeData = coalesceBadge(
|
||||
req.query,
|
||||
serviceData,
|
||||
this.defaultBadgeData,
|
||||
this
|
||||
)
|
||||
|
||||
this._applyCacheHeaders({
|
||||
cacheHeaderConfig,
|
||||
req,
|
||||
res,
|
||||
serviceOverrideCacheLengthSeconds: badgeData.cacheLengthSeconds,
|
||||
})
|
||||
)
|
||||
|
||||
if (format === 'svg') {
|
||||
res.setHeader('Content-Type', 'image/svg+xml')
|
||||
res.send(makeBadge(badgeData))
|
||||
} else if (format === 'json') {
|
||||
res.json(makeJsonBadge(badgeData))
|
||||
} else {
|
||||
throw Error(`Unrecognized format: ${format}`)
|
||||
}
|
||||
|
||||
res.end()
|
||||
|
||||
metricHandle.noteResponseSent()
|
||||
}
|
||||
}
|
||||
|
||||
static register({ app, ...serviceContext }, serviceConfig) {
|
||||
const { regex } = prepareRoute(this.route)
|
||||
app.get(regex, this.makeExpressHandler(serviceContext, serviceConfig))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import Joi from 'joi'
|
||||
import chai from 'chai'
|
||||
import isSvg from 'is-svg'
|
||||
import sinon from 'sinon'
|
||||
import prometheus from 'prom-client'
|
||||
import chaiAsPromised from 'chai-as-promised'
|
||||
import PrometheusMetrics from '../server/prometheus-metrics.js'
|
||||
import { ExpressTestHarness } from '../express-test-harness.js'
|
||||
import trace from './trace.js'
|
||||
import {
|
||||
NotFound,
|
||||
@@ -15,6 +17,7 @@ import {
|
||||
import BaseService from './base.js'
|
||||
import { MetricHelper, MetricNames } from './metric-helper.js'
|
||||
import '../register-chai-plugins.spec.js'
|
||||
|
||||
const { expect } = chai
|
||||
chai.use(chaiAsPromised)
|
||||
|
||||
@@ -59,9 +62,12 @@ class DummyServiceWithServiceResponseSizeMetricEnabled extends DummyService {
|
||||
|
||||
describe('BaseService', function () {
|
||||
const defaultConfig = {
|
||||
handleInternalErrors: false,
|
||||
cacheHeaders: { defaultCacheLengthSeconds: 120 },
|
||||
public: {
|
||||
handleInternalErrors: false,
|
||||
services: {},
|
||||
cacheHeaders: { defaultCacheLengthSeconds: 120 },
|
||||
},
|
||||
private: {},
|
||||
}
|
||||
@@ -321,62 +327,45 @@ describe('BaseService', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('ScoutCamp integration', function () {
|
||||
// TODO Strangly, without the useless escape the regexes do not match in Node 12.
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const expectedRouteRegex = /^\/foo(?:\/([^\/#\?]+?))(|\.svg|\.json)$/
|
||||
describe('Express integration', function () {
|
||||
let harness
|
||||
beforeEach(async function () {
|
||||
harness = new ExpressTestHarness()
|
||||
DummyService.register({ app: harness.app }, defaultConfig)
|
||||
await harness.start()
|
||||
})
|
||||
|
||||
let mockCamp
|
||||
let mockHandleRequest
|
||||
afterEach(async function () {
|
||||
await harness.stop()
|
||||
})
|
||||
|
||||
beforeEach(function () {
|
||||
mockCamp = {
|
||||
route: sinon.spy(),
|
||||
}
|
||||
mockHandleRequest = sinon.spy()
|
||||
DummyService.register(
|
||||
{ camp: mockCamp, handleRequest: mockHandleRequest },
|
||||
defaultConfig
|
||||
it('fulfills the request for an SVG badge', async function () {
|
||||
const { headers, body } = await harness.get(
|
||||
'/foo/bar.svg?queryParamA=%3F'
|
||||
)
|
||||
|
||||
expect(headers).to.include({
|
||||
'content-type': 'image/svg+xml; charset=utf-8',
|
||||
})
|
||||
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('cat: Hello namedParamA: bar with queryParamA: ?')
|
||||
})
|
||||
|
||||
it('registers the service', function () {
|
||||
expect(mockCamp.route).to.have.been.calledOnce
|
||||
expect(mockCamp.route).to.have.been.calledWith(expectedRouteRegex)
|
||||
})
|
||||
it('fulfills the request for a JSON badge', async function () {
|
||||
const { headers, body } = await harness.get(
|
||||
'/foo/bar.json?queryParamA=%3F',
|
||||
{ responseType: 'json' }
|
||||
)
|
||||
|
||||
it('handles the request', async function () {
|
||||
expect(mockHandleRequest).to.have.been.calledOnce
|
||||
expect(headers).to.include({
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
})
|
||||
|
||||
const { queryParams: serviceQueryParams, handler: requestHandler } =
|
||||
mockHandleRequest.getCall(0).args[1]
|
||||
expect(serviceQueryParams).to.deep.equal([
|
||||
'queryParamA',
|
||||
'legacyQueryParamA',
|
||||
])
|
||||
|
||||
const mockSendBadge = sinon.spy()
|
||||
const mockRequest = {
|
||||
asPromise: sinon.spy(),
|
||||
}
|
||||
const queryParams = { queryParamA: '?' }
|
||||
const match = '/foo/bar.svg'.match(expectedRouteRegex)
|
||||
await requestHandler(queryParams, match, mockSendBadge, mockRequest)
|
||||
|
||||
const expectedFormat = 'svg'
|
||||
expect(mockSendBadge).to.have.been.calledOnce
|
||||
expect(mockSendBadge).to.have.been.calledWith(expectedFormat, {
|
||||
expect(body).to.include({
|
||||
label: 'cat',
|
||||
message: 'Hello namedParamA: bar with queryParamA: ?',
|
||||
color: 'lightgrey',
|
||||
style: 'flat',
|
||||
namedLogo: undefined,
|
||||
logo: undefined,
|
||||
logoWidth: undefined,
|
||||
logoPosition: undefined,
|
||||
links: [],
|
||||
labelColor: undefined,
|
||||
cacheLengthSeconds: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -574,9 +563,7 @@ describe('BaseService', function () {
|
||||
},
|
||||
private: {},
|
||||
},
|
||||
{
|
||||
namedParamA: 'bar.bar.bar',
|
||||
}
|
||||
{ namedParamA: 'bar.bar.bar' }
|
||||
)
|
||||
).to.deep.equal({
|
||||
color: 'lightgray',
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
import makeBadge from '../../badge-maker/lib/make-badge.js'
|
||||
import { setCacheHeaders } from './cache-headers.js'
|
||||
import { makeSend } from './legacy-result-sender.js'
|
||||
import coalesceBadge from './coalesce-badge.js'
|
||||
|
||||
// These query parameters are available to any badge. They are handled by
|
||||
// `coalesceBadge`.
|
||||
const globalQueryParams = new Set([
|
||||
'label',
|
||||
'style',
|
||||
'link',
|
||||
'logo',
|
||||
'logoColor',
|
||||
'logoPosition',
|
||||
'logoWidth',
|
||||
'link',
|
||||
'colorA',
|
||||
'colorB',
|
||||
'color',
|
||||
'labelColor',
|
||||
])
|
||||
|
||||
function flattenQueryParams(queryParams) {
|
||||
const union = new Set(globalQueryParams)
|
||||
;(queryParams || []).forEach(name => {
|
||||
union.add(name)
|
||||
})
|
||||
return Array.from(union).sort()
|
||||
}
|
||||
|
||||
// handlerOptions can contain:
|
||||
// - handler: The service's request handler function
|
||||
// - queryParams: An array of the field names of any custom query parameters
|
||||
// the service uses
|
||||
// - cacheLength: An optional badge or category-specific cache length
|
||||
// (in number of seconds) to be used in preference to the default
|
||||
//
|
||||
// For safety, the service must declare the query parameters it wants to use.
|
||||
// Only the declared parameters (and the global parameters) are provided to
|
||||
// the service. Consequently, failure to declare a parameter results in the
|
||||
// parameter not working at all (which is undesirable, but easy to debug)
|
||||
// rather than indeterminate behavior that depends on the cache state
|
||||
// (undesirable and hard to debug).
|
||||
//
|
||||
// Pass just the handler function as shorthand.
|
||||
function handleRequest(cacheHeaderConfig, handlerOptions) {
|
||||
if (!cacheHeaderConfig) {
|
||||
throw Error('cacheHeaderConfig is required')
|
||||
}
|
||||
|
||||
if (typeof handlerOptions === 'function') {
|
||||
handlerOptions = { handler: handlerOptions }
|
||||
}
|
||||
|
||||
const allowedKeys = flattenQueryParams(handlerOptions.queryParams)
|
||||
const { cacheLength: serviceDefaultCacheLengthSeconds } = handlerOptions
|
||||
|
||||
return (queryParams, match, end, ask) => {
|
||||
/*
|
||||
This is here for legacy reasons. The badge server and frontend used to live
|
||||
on two different servers. When we merged them there was a conflict so we
|
||||
did this to avoid moving the endpoint docs to another URL.
|
||||
|
||||
Never ever do this again.
|
||||
*/
|
||||
if (match[0] === '/endpoint' && Object.keys(queryParams).length === 0) {
|
||||
ask.res.statusCode = 301
|
||||
ask.res.setHeader('Location', '/endpoint/')
|
||||
ask.res.end()
|
||||
return
|
||||
}
|
||||
|
||||
// `defaultCacheLengthSeconds` can be overridden by
|
||||
// `serviceDefaultCacheLengthSeconds` (either by category or on a badge-
|
||||
// by-badge basis). Then in turn that can be overridden by
|
||||
// `serviceOverrideCacheLengthSeconds` (which we expect to be used only in
|
||||
// the dynamic badge) but only if `serviceOverrideCacheLengthSeconds` is
|
||||
// longer than `serviceDefaultCacheLengthSeconds` and then the `cacheSeconds`
|
||||
// query param can also override both of those but again only if `cacheSeconds`
|
||||
// is longer.
|
||||
//
|
||||
// When the legacy services have been rewritten, all the code in here
|
||||
// will go away, which should achieve this goal in a simpler way.
|
||||
//
|
||||
// Ref: https://github.com/badges/shields/pull/2755
|
||||
function setCacheHeadersOnResponse(res, serviceOverrideCacheLengthSeconds) {
|
||||
setCacheHeaders({
|
||||
cacheHeaderConfig,
|
||||
serviceDefaultCacheLengthSeconds,
|
||||
serviceOverrideCacheLengthSeconds,
|
||||
queryParams,
|
||||
res,
|
||||
})
|
||||
}
|
||||
|
||||
const filteredQueryParams = {}
|
||||
allowedKeys.forEach(key => {
|
||||
filteredQueryParams[key] = queryParams[key]
|
||||
})
|
||||
|
||||
// In case our vendor servers are unresponsive.
|
||||
let serverUnresponsive = false
|
||||
const serverResponsive = setTimeout(() => {
|
||||
serverUnresponsive = true
|
||||
ask.res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||
const badgeData = coalesceBadge(
|
||||
filteredQueryParams,
|
||||
{ label: 'vendor', message: 'unresponsive' },
|
||||
{}
|
||||
)
|
||||
const svg = makeBadge(badgeData)
|
||||
const extension = (match.slice(-1)[0] || '.svg').replace(/^\./, '')
|
||||
setCacheHeadersOnResponse(ask.res)
|
||||
makeSend(extension, ask.res, end)(svg)
|
||||
}, 25000)
|
||||
|
||||
const result = handlerOptions.handler(
|
||||
filteredQueryParams,
|
||||
match,
|
||||
// eslint-disable-next-line mocha/prefer-arrow-callback
|
||||
function sendBadge(format, badgeData) {
|
||||
if (serverUnresponsive) {
|
||||
return
|
||||
}
|
||||
clearTimeout(serverResponsive)
|
||||
// Add format to badge data.
|
||||
badgeData.format = format
|
||||
const svg = makeBadge(badgeData)
|
||||
setCacheHeadersOnResponse(ask.res, badgeData.cacheLengthSeconds)
|
||||
makeSend(format, ask.res, end)(svg)
|
||||
}
|
||||
)
|
||||
// eslint-disable-next-line promise/prefer-await-to-then
|
||||
if (result && result.catch) {
|
||||
// eslint-disable-next-line promise/prefer-await-to-then
|
||||
result.catch(err => {
|
||||
throw err
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { handleRequest }
|
||||
@@ -1,251 +0,0 @@
|
||||
import { expect } from 'chai'
|
||||
import portfinder from 'portfinder'
|
||||
import Camp from '@shields_io/camp'
|
||||
import got from '../got-test-client.js'
|
||||
import coalesceBadge from './coalesce-badge.js'
|
||||
import { handleRequest } from './legacy-request-handler.js'
|
||||
|
||||
async function performTwoRequests(baseUrl, first, second) {
|
||||
expect((await got(`${baseUrl}${first}`)).statusCode).to.equal(200)
|
||||
expect((await got(`${baseUrl}${second}`)).statusCode).to.equal(200)
|
||||
}
|
||||
|
||||
function fakeHandler(queryParams, match, sendBadge, request) {
|
||||
const [, someValue, format] = match
|
||||
const badgeData = coalesceBadge(
|
||||
queryParams,
|
||||
{
|
||||
label: 'testing',
|
||||
message: someValue,
|
||||
},
|
||||
{}
|
||||
)
|
||||
sendBadge(format, badgeData)
|
||||
}
|
||||
|
||||
function createFakeHandlerWithCacheLength(cacheLengthSeconds) {
|
||||
return function fakeHandler(queryParams, match, sendBadge, request) {
|
||||
const [, someValue, format] = match
|
||||
const badgeData = coalesceBadge(
|
||||
queryParams,
|
||||
{
|
||||
label: 'testing',
|
||||
message: someValue,
|
||||
},
|
||||
{},
|
||||
{
|
||||
_cacheLength: cacheLengthSeconds,
|
||||
}
|
||||
)
|
||||
sendBadge(format, badgeData)
|
||||
}
|
||||
}
|
||||
|
||||
describe('The request handler', function () {
|
||||
let port, baseUrl
|
||||
beforeEach(async function () {
|
||||
port = await portfinder.getPortPromise()
|
||||
baseUrl = `http://127.0.0.1:${port}`
|
||||
})
|
||||
|
||||
let camp
|
||||
beforeEach(function (done) {
|
||||
camp = Camp.start({ port, hostname: '::' })
|
||||
camp.on('listening', () => done())
|
||||
})
|
||||
afterEach(function (done) {
|
||||
if (camp) {
|
||||
camp.close(() => done())
|
||||
camp = null
|
||||
}
|
||||
})
|
||||
|
||||
const standardCacheHeaders = { defaultCacheLengthSeconds: 120 }
|
||||
|
||||
describe('the options object calling style', function () {
|
||||
beforeEach(function () {
|
||||
camp.route(
|
||||
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
|
||||
handleRequest(standardCacheHeaders, { handler: fakeHandler })
|
||||
)
|
||||
})
|
||||
|
||||
it('should return the expected response', async function () {
|
||||
const { statusCode, body } = await got(`${baseUrl}/testing/123.json`, {
|
||||
responseType: 'json',
|
||||
})
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body).to.deep.equal({
|
||||
name: 'testing',
|
||||
value: '123',
|
||||
label: 'testing',
|
||||
message: '123',
|
||||
color: 'lightgrey',
|
||||
link: [],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('the function shorthand calling style', function () {
|
||||
beforeEach(function () {
|
||||
camp.route(
|
||||
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
|
||||
handleRequest(standardCacheHeaders, fakeHandler)
|
||||
)
|
||||
})
|
||||
|
||||
it('should return the expected response', async function () {
|
||||
const { statusCode, body } = await got(`${baseUrl}/testing/123.json`, {
|
||||
responseType: 'json',
|
||||
})
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body).to.deep.equal({
|
||||
name: 'testing',
|
||||
value: '123',
|
||||
label: 'testing',
|
||||
message: '123',
|
||||
color: 'lightgrey',
|
||||
link: [],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('caching', function () {
|
||||
describe('standard query parameters', function () {
|
||||
function register({ cacheHeaderConfig }) {
|
||||
camp.route(
|
||||
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
|
||||
handleRequest(
|
||||
cacheHeaderConfig,
|
||||
(queryParams, match, sendBadge, request) => {
|
||||
fakeHandler(queryParams, match, sendBadge, request)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
it('should set the expires header to current time + defaultCacheLengthSeconds', async function () {
|
||||
register({ cacheHeaderConfig: { defaultCacheLengthSeconds: 900 } })
|
||||
const { headers } = await got(`${baseUrl}/testing/123.json`)
|
||||
const expectedExpiry = new Date(
|
||||
+new Date(headers.date) + 900000
|
||||
).toGMTString()
|
||||
expect(headers.expires).to.equal(expectedExpiry)
|
||||
expect(headers['cache-control']).to.equal('max-age=900, s-maxage=900')
|
||||
})
|
||||
|
||||
it('should set the expected cache headers on cached responses', async function () {
|
||||
register({ cacheHeaderConfig: { defaultCacheLengthSeconds: 900 } })
|
||||
|
||||
// Make first request.
|
||||
await got(`${baseUrl}/testing/123.json`)
|
||||
|
||||
const { headers } = await got(`${baseUrl}/testing/123.json`)
|
||||
const expectedExpiry = new Date(
|
||||
+new Date(headers.date) + 900000
|
||||
).toGMTString()
|
||||
expect(headers.expires).to.equal(expectedExpiry)
|
||||
expect(headers['cache-control']).to.equal('max-age=900, s-maxage=900')
|
||||
})
|
||||
|
||||
it('should let live service data override the default cache headers with longer value', async function () {
|
||||
camp.route(
|
||||
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
|
||||
handleRequest(
|
||||
{ defaultCacheLengthSeconds: 300 },
|
||||
(queryParams, match, sendBadge, request) => {
|
||||
createFakeHandlerWithCacheLength(400)(
|
||||
queryParams,
|
||||
match,
|
||||
sendBadge,
|
||||
request
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
const { headers } = await got(`${baseUrl}/testing/123.json`)
|
||||
expect(headers['cache-control']).to.equal('max-age=400, s-maxage=400')
|
||||
})
|
||||
|
||||
it('should not let live service data override the default cache headers with shorter value', async function () {
|
||||
camp.route(
|
||||
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
|
||||
handleRequest(
|
||||
{ defaultCacheLengthSeconds: 300 },
|
||||
(queryParams, match, sendBadge, request) => {
|
||||
createFakeHandlerWithCacheLength(200)(
|
||||
queryParams,
|
||||
match,
|
||||
sendBadge,
|
||||
request
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
const { headers } = await got(`${baseUrl}/testing/123.json`)
|
||||
expect(headers['cache-control']).to.equal('max-age=300, s-maxage=300')
|
||||
})
|
||||
|
||||
it('should set the expires header to current time + cacheSeconds', async function () {
|
||||
register({ cacheHeaderConfig: { defaultCacheLengthSeconds: 0 } })
|
||||
const { headers } = await got(
|
||||
`${baseUrl}/testing/123.json?cacheSeconds=3600`
|
||||
)
|
||||
const expectedExpiry = new Date(
|
||||
+new Date(headers.date) + 3600000
|
||||
).toGMTString()
|
||||
expect(headers.expires).to.equal(expectedExpiry)
|
||||
expect(headers['cache-control']).to.equal('max-age=3600, s-maxage=3600')
|
||||
})
|
||||
|
||||
it('should ignore cacheSeconds when shorter than defaultCacheLengthSeconds', async function () {
|
||||
register({ cacheHeaderConfig: { defaultCacheLengthSeconds: 600 } })
|
||||
const { headers } = await got(
|
||||
`${baseUrl}/testing/123.json?cacheSeconds=300`
|
||||
)
|
||||
const expectedExpiry = new Date(
|
||||
+new Date(headers.date) + 600000
|
||||
).toGMTString()
|
||||
expect(headers.expires).to.equal(expectedExpiry)
|
||||
expect(headers['cache-control']).to.equal('max-age=600, s-maxage=600')
|
||||
})
|
||||
|
||||
it('should set Cache-Control: no-cache, no-store, must-revalidate if cache seconds is 0', async function () {
|
||||
register({ cacheHeaderConfig: { defaultCacheLengthSeconds: 0 } })
|
||||
const { headers } = await got(`${baseUrl}/testing/123.json`)
|
||||
expect(headers.expires).to.equal(headers.date)
|
||||
expect(headers['cache-control']).to.equal(
|
||||
'no-cache, no-store, must-revalidate'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('custom query parameters', function () {
|
||||
let handlerCallCount
|
||||
beforeEach(function () {
|
||||
handlerCallCount = 0
|
||||
camp.route(
|
||||
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
|
||||
handleRequest(standardCacheHeaders, {
|
||||
queryParams: ['foo'],
|
||||
handler: (queryParams, match, sendBadge, request) => {
|
||||
++handlerCallCount
|
||||
fakeHandler(queryParams, match, sendBadge, request)
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should differentiate them', async function () {
|
||||
await performTwoRequests(
|
||||
baseUrl,
|
||||
'/testing/123.svg?foo=1',
|
||||
'/testing/123.svg?foo=2'
|
||||
)
|
||||
expect(handlerCallCount).to.equal(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,35 +0,0 @@
|
||||
import stream from 'stream'
|
||||
|
||||
function streamFromString(str) {
|
||||
const newStream = new stream.Readable()
|
||||
newStream._read = () => {
|
||||
newStream.push(str)
|
||||
newStream.push(null)
|
||||
}
|
||||
return newStream
|
||||
}
|
||||
|
||||
function sendSVG(res, askres, end) {
|
||||
askres.setHeader('Content-Type', 'image/svg+xml;charset=utf-8')
|
||||
askres.setHeader('Content-Length', Buffer.byteLength(res, 'utf8'))
|
||||
end(null, { template: streamFromString(res) })
|
||||
}
|
||||
|
||||
function sendJSON(res, askres, end) {
|
||||
askres.setHeader('Content-Type', 'application/json')
|
||||
askres.setHeader('Access-Control-Allow-Origin', '*')
|
||||
askres.setHeader('Content-Length', Buffer.byteLength(res, 'utf8'))
|
||||
end(null, { template: streamFromString(res) })
|
||||
}
|
||||
|
||||
function makeSend(format, askres, end) {
|
||||
if (format === 'svg') {
|
||||
return res => sendSVG(res, askres, end)
|
||||
} else if (format === 'json') {
|
||||
return res => sendJSON(res, askres, end)
|
||||
} else {
|
||||
throw Error(`Unrecognized format: ${format}`)
|
||||
}
|
||||
}
|
||||
|
||||
export { makeSend }
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
16
core/base-service/make-json-badge.js
Normal file
16
core/base-service/make-json-badge.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { normalizeColor } from 'badge-maker/lib/color.js'
|
||||
|
||||
export function makeJsonBadge(badgeData) {
|
||||
const { label, message, logoWidth, color, labelColor, links } = badgeData
|
||||
|
||||
return {
|
||||
label,
|
||||
message,
|
||||
logoWidth,
|
||||
color: normalizeColor(color),
|
||||
labelColor: normalizeColor(labelColor),
|
||||
link: links,
|
||||
name: label,
|
||||
value: message,
|
||||
}
|
||||
}
|
||||
23
core/base-service/make-json-badge.spec.js
Normal file
23
core/base-service/make-json-badge.spec.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { expect } from 'chai'
|
||||
import { makeJsonBadge } from './make-json-badge.js'
|
||||
|
||||
describe('makeJsonBadge()', function () {
|
||||
it('should produce the expected JSON', function () {
|
||||
expect(
|
||||
makeJsonBadge({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
links: ['https://example.com/', 'https://other.example.com/'],
|
||||
})
|
||||
).to.deep.equal({
|
||||
name: 'cactus',
|
||||
label: 'cactus',
|
||||
value: 'grown',
|
||||
message: 'grown',
|
||||
link: ['https://example.com/', 'https://other.example.com/'],
|
||||
color: undefined,
|
||||
labelColor: undefined,
|
||||
logoWidth: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,3 +1,4 @@
|
||||
import url from 'url'
|
||||
import camelcase from 'camelcase'
|
||||
import emojic from 'emojic'
|
||||
import Joi from 'joi'
|
||||
@@ -9,7 +10,7 @@ import {
|
||||
} from './cache-headers.js'
|
||||
import { isValidCategory } from './categories.js'
|
||||
import { MetricHelper } from './metric-helper.js'
|
||||
import { isValidRoute, prepareRoute, namedParamsForMatch } from './route.js'
|
||||
import { isValidRoute, prepareRoute, paramsForReq } from './route.js'
|
||||
import trace from './trace.js'
|
||||
|
||||
const attrSchema = Joi.object({
|
||||
@@ -54,7 +55,7 @@ export default function redirector(attrs) {
|
||||
static route = route
|
||||
static examples = examples
|
||||
|
||||
static register({ camp, metricInstance }, { rasterUrl }) {
|
||||
static register({ app, metricInstance }, { rasterUrl }) {
|
||||
const { regex, captureNames } = prepareRoute({
|
||||
...this.route,
|
||||
withPng: Boolean(rasterUrl),
|
||||
@@ -65,17 +66,17 @@ export default function redirector(attrs) {
|
||||
ServiceClass: this,
|
||||
})
|
||||
|
||||
camp.route(regex, async (queryParams, match, end, ask) => {
|
||||
if (serverHasBeenUpSinceResourceCached(ask.req)) {
|
||||
app.get(regex, async (req, res) => {
|
||||
if (serverHasBeenUpSinceResourceCached(req)) {
|
||||
// Send Not Modified.
|
||||
ask.res.statusCode = 304
|
||||
ask.res.end()
|
||||
res.status(304)
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
|
||||
const metricHandle = metricHelper.startRequest()
|
||||
|
||||
const namedParams = namedParamsForMatch(captureNames, match, this)
|
||||
const { namedParams, format } = paramsForReq(captureNames, req, this)
|
||||
trace.logTrace(
|
||||
'inbound',
|
||||
emojic.arrowHeadingUp,
|
||||
@@ -83,12 +84,12 @@ export default function redirector(attrs) {
|
||||
route.base
|
||||
)
|
||||
trace.logTrace('inbound', emojic.ticket, 'Named params', namedParams)
|
||||
trace.logTrace('inbound', emojic.crayon, 'Query params', queryParams)
|
||||
trace.logTrace('inbound', emojic.crayon, 'Query params', req.query)
|
||||
|
||||
const targetPath = encodeURI(transformPath(namedParams))
|
||||
trace.logTrace('validate', emojic.dart, 'Target', targetPath)
|
||||
|
||||
let urlSuffix = ask.uri.search || ''
|
||||
let urlSuffix = url.parse(req.url).search ?? '' // eslint-disable-line node/no-deprecated-api
|
||||
|
||||
if (transformQueryParams) {
|
||||
const specifiedParams = queryString.parse(urlSuffix)
|
||||
@@ -100,21 +101,18 @@ export default function redirector(attrs) {
|
||||
urlSuffix = `?${outQueryString}`
|
||||
}
|
||||
|
||||
// The final capture group is the extension.
|
||||
const format = (match.slice(-1)[0] || '.svg').replace(/^\./, '')
|
||||
const redirectUrl = `${
|
||||
format === 'png' ? rasterUrl : ''
|
||||
}${targetPath}.${format}${urlSuffix}`
|
||||
const baseUrl = format === 'png' ? rasterUrl : ''
|
||||
const redirectUrl = `${baseUrl}${targetPath}.${format}${urlSuffix}`
|
||||
trace.logTrace('outbound', emojic.shield, 'Redirect URL', redirectUrl)
|
||||
|
||||
ask.res.statusCode = 301
|
||||
ask.res.setHeader('Location', redirectUrl)
|
||||
res.status(301)
|
||||
res.setHeader('Location', redirectUrl)
|
||||
|
||||
// To avoid caching mistakes for a long time, and to make this simpler
|
||||
// to reason about, use the same cache semantics as the static badge.
|
||||
setCacheHeadersForStaticResource(ask.res)
|
||||
setCacheHeadersForStaticResource(res)
|
||||
|
||||
ask.res.end()
|
||||
res.end()
|
||||
|
||||
metricHandle.noteResponseSent()
|
||||
})
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import Camp from '@shields_io/camp'
|
||||
import portfinder from 'portfinder'
|
||||
import { expect } from 'chai'
|
||||
import got from '../got-test-client.js'
|
||||
import { ExpressTestHarness } from '../express-test-harness.js'
|
||||
import redirector from './redirector.js'
|
||||
|
||||
describe('Redirector', function () {
|
||||
@@ -63,28 +61,12 @@ describe('Redirector', function () {
|
||||
expect(redirector({ ...attrs, examples }).examples).to.equal(examples)
|
||||
})
|
||||
|
||||
describe('ScoutCamp integration', function () {
|
||||
let port, baseUrl
|
||||
beforeEach(async function () {
|
||||
port = await portfinder.getPortPromise()
|
||||
baseUrl = `http://127.0.0.1:${port}`
|
||||
})
|
||||
|
||||
let camp
|
||||
beforeEach(async function () {
|
||||
camp = Camp.start({ port, hostname: '::' })
|
||||
await new Promise(resolve => camp.on('listening', () => resolve()))
|
||||
})
|
||||
afterEach(async function () {
|
||||
if (camp) {
|
||||
await new Promise(resolve => camp.close(resolve))
|
||||
camp = undefined
|
||||
}
|
||||
})
|
||||
|
||||
describe('Express integration', function () {
|
||||
const transformPath = ({ namedParamA }) => `/new/service/${namedParamA}`
|
||||
|
||||
beforeEach(function () {
|
||||
let harness
|
||||
beforeEach(async function () {
|
||||
harness = new ExpressTestHarness()
|
||||
const ServiceClass = redirector({
|
||||
category,
|
||||
route,
|
||||
@@ -92,17 +74,20 @@ describe('Redirector', function () {
|
||||
dateAdded,
|
||||
})
|
||||
ServiceClass.register(
|
||||
{ camp },
|
||||
{ app: harness.app },
|
||||
{ rasterUrl: 'http://raster.example.test' }
|
||||
)
|
||||
await harness.start()
|
||||
})
|
||||
|
||||
afterEach(async function () {
|
||||
await harness.stop()
|
||||
})
|
||||
|
||||
it('should redirect as configured', async function () {
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}/very/old/service/hello-world.svg`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
const { statusCode, headers } = await harness.get(
|
||||
'/very/old/service/hello-world.svg',
|
||||
{ followRedirect: false }
|
||||
)
|
||||
|
||||
expect(statusCode).to.equal(301)
|
||||
@@ -110,11 +95,9 @@ describe('Redirector', function () {
|
||||
})
|
||||
|
||||
it('should redirect raster extensions to the canonical path as configured', async function () {
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}/very/old/service/hello-world.png`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
const { statusCode, headers } = await harness.get(
|
||||
'/very/old/service/hello-world.png',
|
||||
{ followRedirect: false }
|
||||
)
|
||||
|
||||
expect(statusCode).to.equal(301)
|
||||
@@ -124,11 +107,9 @@ describe('Redirector', function () {
|
||||
})
|
||||
|
||||
it('should forward the query params', async function () {
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}/very/old/service/hello-world.svg?color=123&style=flat-square`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
const { statusCode, headers } = await harness.get(
|
||||
'/very/old/service/hello-world.svg?color=123&style=flat-square',
|
||||
{ followRedirect: false }
|
||||
)
|
||||
|
||||
expect(statusCode).to.equal(301)
|
||||
@@ -138,11 +119,9 @@ describe('Redirector', function () {
|
||||
})
|
||||
|
||||
it('should correctly encode the redirect URL', async function () {
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}/very/old/service/hello%0Dworld.svg?foobar=a%0Db`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
const { statusCode, headers } = await harness.get(
|
||||
'/very/old/service/hello%0Dworld.svg?foobar=a%0Db',
|
||||
{ followRedirect: false }
|
||||
)
|
||||
|
||||
expect(statusCode).to.equal(301)
|
||||
@@ -166,15 +145,13 @@ describe('Redirector', function () {
|
||||
transformQueryParams,
|
||||
dateAdded,
|
||||
})
|
||||
ServiceClass.register({ camp }, {})
|
||||
ServiceClass.register({ app: harness.app }, {})
|
||||
})
|
||||
|
||||
it('should forward the transformed query params', async function () {
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}/another/old/service/token/abc123/hello-world.svg`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
const { statusCode, headers } = await harness.get(
|
||||
'/another/old/service/token/abc123/hello-world.svg',
|
||||
{ followRedirect: false }
|
||||
)
|
||||
|
||||
expect(statusCode).to.equal(301)
|
||||
@@ -184,11 +161,9 @@ describe('Redirector', function () {
|
||||
})
|
||||
|
||||
it('should forward the specified and transformed query params', async function () {
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}/another/old/service/token/abc123/hello-world.svg?color=123&style=flat-square`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
const { statusCode, headers } = await harness.get(
|
||||
'/another/old/service/token/abc123/hello-world.svg?color=123&style=flat-square',
|
||||
{ followRedirect: false }
|
||||
)
|
||||
|
||||
expect(statusCode).to.equal(301)
|
||||
@@ -198,11 +173,9 @@ describe('Redirector', function () {
|
||||
})
|
||||
|
||||
it('should use transformed query params on param conflicts by default', async function () {
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}/another/old/service/token/abc123/hello-world.svg?color=123&style=flat-square&token=def456`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
const { statusCode, headers } = await harness.get(
|
||||
'/another/old/service/token/abc123/hello-world.svg?color=123&style=flat-square&token=def456',
|
||||
{ followRedirect: false }
|
||||
)
|
||||
|
||||
expect(statusCode).to.equal(301)
|
||||
@@ -224,12 +197,10 @@ describe('Redirector', function () {
|
||||
overrideTransformedQueryParams: true,
|
||||
dateAdded,
|
||||
})
|
||||
ServiceClass.register({ camp }, {})
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}/override/service/token/abc123/hello-world.svg?style=flat-square&token=def456`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
ServiceClass.register({ app: harness.app }, {})
|
||||
const { statusCode, headers } = await harness.get(
|
||||
'/override/service/token/abc123/hello-world.svg?style=flat-square&token=def456',
|
||||
{ followRedirect: false }
|
||||
)
|
||||
|
||||
expect(statusCode).to.equal(301)
|
||||
|
||||
@@ -44,23 +44,29 @@ function prepareRoute({ base, pattern, format, capture, withPng }) {
|
||||
return { regex, captureNames }
|
||||
}
|
||||
|
||||
function namedParamsForMatch(captureNames = [], match, ServiceClass) {
|
||||
// Assume the last match is the format, and drop match[0], which is the
|
||||
// entire match.
|
||||
const captures = match.slice(1, -1)
|
||||
|
||||
if (captureNames.length !== captures.length) {
|
||||
function paramsForReq(captureNames = [], req, ServiceClass) {
|
||||
// In addition to the parameters declared by the service, we have one match
|
||||
// for the format.
|
||||
const expectedNamedParamCount = Object.keys(req.params).length - 1
|
||||
if (captureNames.length !== expectedNamedParamCount) {
|
||||
throw new Error(
|
||||
`Service ${ServiceClass.name} declares incorrect number of named params ` +
|
||||
`(expected ${captures.length}, got ${captureNames.length})`
|
||||
`(expected ${expectedNamedParamCount}, got ${captureNames.length})`
|
||||
)
|
||||
}
|
||||
|
||||
const result = {}
|
||||
const namedParams = {}
|
||||
captureNames.forEach((name, index) => {
|
||||
result[name] = captures[index]
|
||||
namedParams[name] = req.params[index]
|
||||
})
|
||||
return result
|
||||
|
||||
// The final capture group is the extension.
|
||||
const format = (req.params[expectedNamedParamCount] || '.svg').replace(
|
||||
/^\./,
|
||||
''
|
||||
)
|
||||
|
||||
return { namedParams, format }
|
||||
}
|
||||
|
||||
function getQueryParamNames({ queryParamSchema }) {
|
||||
@@ -77,6 +83,6 @@ export {
|
||||
isValidRoute,
|
||||
assertValidRoute,
|
||||
prepareRoute,
|
||||
namedParamsForMatch,
|
||||
paramsForReq,
|
||||
getQueryParamNames,
|
||||
}
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
import { expect } from 'chai'
|
||||
import Joi from 'joi'
|
||||
import { test, given, forCases } from 'sazerac'
|
||||
import {
|
||||
prepareRoute,
|
||||
namedParamsForMatch,
|
||||
getQueryParamNames,
|
||||
} from './route.js'
|
||||
import { test, given } from 'sazerac'
|
||||
import { prepareRoute, paramsForReq, getQueryParamNames } from './route.js'
|
||||
|
||||
function paramsForPath({ regex, captureNames, ServiceClass }, path) {
|
||||
// Prepare a mock express `req` object.
|
||||
const params = {}
|
||||
regex.exec(path).forEach((param, i) => {
|
||||
// regex.exec(path)[0] contains the entire path. We want [1] ... [n].
|
||||
if (i > 0) {
|
||||
params[i - 1] = param
|
||||
}
|
||||
})
|
||||
const req = { params }
|
||||
|
||||
return paramsForReq(captureNames, req, ServiceClass)
|
||||
}
|
||||
|
||||
describe('Route helpers', function () {
|
||||
const ServiceClass = { name: 'MyService' }
|
||||
|
||||
context('A `pattern` with a named param is declared', function () {
|
||||
const { regex, captureNames } = prepareRoute({
|
||||
base: 'foo',
|
||||
@@ -15,22 +27,31 @@ describe('Route helpers', function () {
|
||||
queryParamSchema: Joi.object({ queryParamA: Joi.string() }).required(),
|
||||
})
|
||||
|
||||
const regexExec = str => regex.exec(str)
|
||||
const regexExec = path => regex.exec(path)
|
||||
test(regexExec, () => {
|
||||
given('/foo/bar/bar.svg').expect(null)
|
||||
})
|
||||
|
||||
const namedParams = str =>
|
||||
namedParamsForMatch(captureNames, regex.exec(str))
|
||||
test(namedParams, () => {
|
||||
forCases([
|
||||
given('/foo/bar.bar.bar.svg'),
|
||||
given('/foo/bar.bar.bar.json'),
|
||||
]).expect({ namedParamA: 'bar.bar.bar' })
|
||||
|
||||
const params = path =>
|
||||
paramsForPath({ regex, captureNames, ServiceClass }, path)
|
||||
test(params, () => {
|
||||
given('/foo/bar.bar.bar.svg').expect({
|
||||
namedParams: { namedParamA: 'bar.bar.bar' },
|
||||
format: 'svg',
|
||||
})
|
||||
given('/foo/bar.bar.bar.json').expect({
|
||||
namedParams: { namedParamA: 'bar.bar.bar' },
|
||||
format: 'json',
|
||||
})
|
||||
// This pattern catches bugs related to escaping the extension separator.
|
||||
given('/foo/bar.bar.bar_svg').expect({ namedParamA: 'bar.bar.bar_svg' })
|
||||
given('/foo/bar.bar.bar.zip').expect({ namedParamA: 'bar.bar.bar.zip' })
|
||||
given('/foo/bar.bar.bar_svg').expect({
|
||||
namedParams: { namedParamA: 'bar.bar.bar_svg' },
|
||||
format: 'svg',
|
||||
})
|
||||
given('/foo/bar.bar.bar.zip').expect({
|
||||
namedParams: { namedParamA: 'bar.bar.bar.zip' },
|
||||
format: 'svg',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -46,33 +67,41 @@ describe('Route helpers', function () {
|
||||
given('/foo/bar/bar.svg').expect(null)
|
||||
})
|
||||
|
||||
const namedParams = str =>
|
||||
namedParamsForMatch(captureNames, regex.exec(str))
|
||||
test(namedParams, () => {
|
||||
forCases([
|
||||
given('/foo/bar.bar.bar.svg'),
|
||||
given('/foo/bar.bar.bar.json'),
|
||||
]).expect({ namedParamA: 'bar.bar.bar' })
|
||||
const params = path =>
|
||||
paramsForPath({ regex, captureNames, ServiceClass }, path)
|
||||
test(params, () => {
|
||||
given('/foo/bar.bar.bar.svg').expect({
|
||||
namedParams: { namedParamA: 'bar.bar.bar' },
|
||||
format: 'svg',
|
||||
})
|
||||
given('/foo/bar.bar.bar.json').expect({
|
||||
namedParams: { namedParamA: 'bar.bar.bar' },
|
||||
format: 'json',
|
||||
})
|
||||
|
||||
// This pattern catches bugs related to escaping the extension separator.
|
||||
given('/foo/bar.bar.bar_svg').expect({ namedParamA: 'bar.bar.bar_svg' })
|
||||
given('/foo/bar.bar.bar.zip').expect({ namedParamA: 'bar.bar.bar.zip' })
|
||||
given('/foo/bar.bar.bar_svg').expect({
|
||||
namedParams: { namedParamA: 'bar.bar.bar_svg' },
|
||||
format: 'svg',
|
||||
})
|
||||
given('/foo/bar.bar.bar.zip').expect({
|
||||
namedParams: { namedParamA: 'bar.bar.bar.zip' },
|
||||
format: 'svg',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('No named params are declared', function () {
|
||||
const { regex, captureNames } = prepareRoute({
|
||||
base: 'foo',
|
||||
format: '(?:[^/]+)',
|
||||
format: '(?:[^/]+?)',
|
||||
})
|
||||
|
||||
const namedParams = str =>
|
||||
namedParamsForMatch(captureNames, regex.exec(str))
|
||||
test(namedParams, () => {
|
||||
forCases([
|
||||
given('/foo/bar.bar.bar.svg'),
|
||||
given('/foo/bar.bar.bar.json'),
|
||||
]).expect({})
|
||||
const params = path =>
|
||||
paramsForPath({ regex, captureNames, ServiceClass }, path)
|
||||
test(params, () => {
|
||||
given('/foo/bar.bar.bar.svg').expect({ namedParams: {}, format: 'svg' })
|
||||
given('/foo/bar.bar.bar.json').expect({ namedParams: {}, format: 'json' })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -83,13 +112,13 @@ describe('Route helpers', function () {
|
||||
capture: ['namedParamA'],
|
||||
})
|
||||
|
||||
expect(() =>
|
||||
namedParamsForMatch(captureNames, regex.exec('/foo/bar/baz.svg'), {
|
||||
name: 'MyService',
|
||||
})
|
||||
).to.throw(
|
||||
'Service MyService declares incorrect number of named params (expected 2, got 1)'
|
||||
)
|
||||
it('Throws the expected error', function () {
|
||||
expect(() =>
|
||||
paramsForPath({ regex, captureNames, ServiceClass }, '/foo/bar/baz.svg')
|
||||
).to.throw(
|
||||
'Service MyService declares incorrect number of named params (expected 2, got 1)'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('getQueryParamNames', function () {
|
||||
|
||||
37
core/express-test-harness.js
Normal file
37
core/express-test-harness.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import express from 'express'
|
||||
import portfinder from 'portfinder'
|
||||
import got from './got-test-client.js'
|
||||
|
||||
export class ExpressTestHarness {
|
||||
constructor() {
|
||||
this.app = express()
|
||||
}
|
||||
|
||||
async start() {
|
||||
const port = (this.port = await portfinder.getPortPromise())
|
||||
this.baseUrl = `http://127.0.0.1:${port}`
|
||||
await new Promise(resolve => {
|
||||
this.server = this.app.listen({ host: '::', port }, () => resolve())
|
||||
})
|
||||
}
|
||||
|
||||
async stop() {
|
||||
await new Promise(resolve => this.server.close(resolve))
|
||||
}
|
||||
|
||||
ensureStarted() {
|
||||
if (!this.server) {
|
||||
throw Error('Server has not been started')
|
||||
}
|
||||
}
|
||||
|
||||
async get(url, options) {
|
||||
this.ensureStarted()
|
||||
return got.get(`${this.baseUrl}${url}`, options)
|
||||
}
|
||||
|
||||
async post(url, options) {
|
||||
this.ensureStarted()
|
||||
return got.post(`${this.baseUrl}${url}`, options)
|
||||
}
|
||||
}
|
||||
@@ -37,12 +37,13 @@ export default class PrometheusMetrics {
|
||||
})
|
||||
}
|
||||
|
||||
async registerMetricsEndpoint(server) {
|
||||
async registerMetricsEndpoint(app) {
|
||||
const { register } = this
|
||||
|
||||
server.route(/^\/metrics$/, async (data, match, end, ask) => {
|
||||
ask.res.setHeader('Content-Type', register.contentType)
|
||||
ask.res.end(await register.metrics())
|
||||
app.get('/metrics', async (req, res) => {
|
||||
res.setHeader('Content-Type', register.contentType)
|
||||
res.send(await register.metrics())
|
||||
res.end()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +1,24 @@
|
||||
import { expect } from 'chai'
|
||||
import Camp from '@shields_io/camp'
|
||||
import portfinder from 'portfinder'
|
||||
import got from '../got-test-client.js'
|
||||
import { ExpressTestHarness } from '../express-test-harness.js'
|
||||
import Metrics from './prometheus-metrics.js'
|
||||
|
||||
describe('Prometheus metrics route', function () {
|
||||
let port, baseUrl, camp, metrics
|
||||
let harness, metrics
|
||||
beforeEach(async function () {
|
||||
port = await portfinder.getPortPromise()
|
||||
baseUrl = `http://127.0.0.1:${port}`
|
||||
camp = Camp.start({ port, hostname: '::' })
|
||||
await new Promise(resolve => camp.on('listening', () => resolve()))
|
||||
harness = new ExpressTestHarness()
|
||||
|
||||
metrics = new Metrics()
|
||||
metrics.registerMetricsEndpoint(harness.app)
|
||||
|
||||
await harness.start()
|
||||
})
|
||||
|
||||
afterEach(async function () {
|
||||
if (metrics) {
|
||||
metrics.stop()
|
||||
}
|
||||
if (camp) {
|
||||
await new Promise(resolve => camp.close(resolve))
|
||||
camp = undefined
|
||||
}
|
||||
await harness.stop()
|
||||
})
|
||||
|
||||
it('returns default metrics', async function () {
|
||||
metrics = new Metrics()
|
||||
metrics.registerMetricsEndpoint(camp)
|
||||
|
||||
const { statusCode, body } = await got(`${baseUrl}/metrics`)
|
||||
const { statusCode, body } = await harness.get('/metrics')
|
||||
|
||||
expect(statusCode).to.be.equal(200)
|
||||
expect(body).to.contain('nodejs_version_info')
|
||||
|
||||
@@ -2,19 +2,20 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import http from 'http'
|
||||
import https from 'https'
|
||||
import path from 'path'
|
||||
import url, { fileURLToPath } from 'url'
|
||||
import express from 'express'
|
||||
import { bootstrap } from 'global-agent'
|
||||
import cloudflareMiddleware from 'cloudflare-middleware'
|
||||
import Camp from '@shields_io/camp'
|
||||
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 { setRoutes as setSuggestRoutes } 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'
|
||||
import { makeJsonBadge } from '../base-service/make-json-badge.js'
|
||||
import { clearResourceCache } from '../base-service/resource-cache.js'
|
||||
import { rasterRedirectUrl } from '../badge-urls/make-badge-url.js'
|
||||
import { fileSize, nonNegativeInteger } from '../../services/validators.js'
|
||||
@@ -140,7 +141,9 @@ const publicConfigSchema = Joi.object({
|
||||
weblate: defaultService,
|
||||
trace: Joi.boolean().required(),
|
||||
}).required(),
|
||||
cacheHeaders: { defaultCacheLengthSeconds: nonNegativeInteger },
|
||||
cacheHeaders: Joi.object({
|
||||
defaultCacheLengthSeconds: nonNegativeInteger,
|
||||
}).required(),
|
||||
handleInternalErrors: Joi.boolean().required(),
|
||||
fetchLimit: fileSize,
|
||||
userAgentBase: Joi.string().required(),
|
||||
@@ -169,8 +172,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()),
|
||||
@@ -199,23 +200,11 @@ const privateMetricsInfluxConfigSchema = privateConfigSchema.append({
|
||||
influx_password: Joi.string().required(),
|
||||
})
|
||||
|
||||
function addHandlerAtIndex(camp, index, handlerFn) {
|
||||
camp.stack.splice(index, 0, handlerFn)
|
||||
}
|
||||
|
||||
function isOnHeroku() {
|
||||
return !!process.env.DYNO
|
||||
}
|
||||
|
||||
function isOnFly() {
|
||||
return !!process.env.FLY_APP_NAME
|
||||
}
|
||||
|
||||
/**
|
||||
* The Server is based on the web framework Scoutcamp. It creates
|
||||
* an http server, sets up helpers for token persistence and monitoring.
|
||||
* Then it loads all the services, injecting dependencies as it
|
||||
* asks each one to register its route with Scoutcamp.
|
||||
* The Server is based on Express. It creates an http server and sets up helpers
|
||||
* for token persistence and monitoring. Then it loads all the services,
|
||||
* injecting dependencies, as it asks each one to register its route with
|
||||
* Express.
|
||||
*/
|
||||
class Server {
|
||||
/**
|
||||
@@ -308,45 +297,25 @@ class Server {
|
||||
|
||||
// See https://www.viget.com/articles/heroku-cloudflare-the-right-way/
|
||||
requireCloudflare() {
|
||||
// Set `req.ip`, which is expected by `cloudflareMiddleware()`. This is set
|
||||
// by Express but not Scoutcamp.
|
||||
addHandlerAtIndex(this.camp, 0, function (req, res, next) {
|
||||
if (isOnHeroku()) {
|
||||
// On Heroku, `req.socket.remoteAddress` is the Heroku router. However,
|
||||
// the router ensures that the last item in the `X-Forwarded-For` header
|
||||
// is the real origin.
|
||||
// https://stackoverflow.com/a/18517550/893113
|
||||
req.ip = req.headers['x-forwarded-for'].split(', ').pop()
|
||||
} else if (isOnFly()) {
|
||||
// On Fly we can use the Fly-Client-IP header
|
||||
// https://fly.io/docs/reference/runtime-environment/#request-headers
|
||||
req.ip = req.headers['fly-client-ip']
|
||||
? req.headers['fly-client-ip']
|
||||
: req.socket.remoteAddress
|
||||
} else {
|
||||
req.ip = req.socket.remoteAddress
|
||||
}
|
||||
next()
|
||||
})
|
||||
addHandlerAtIndex(this.camp, 1, cloudflareMiddleware())
|
||||
const { app } = this
|
||||
app.use(cloudflareMiddleware())
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up Scoutcamp routes for 404/not found responses
|
||||
* Set up Express routes for 404/not found responses.
|
||||
*/
|
||||
registerErrorHandlers() {
|
||||
const { camp, config } = this
|
||||
const { app, config } = this
|
||||
const {
|
||||
public: { rasterUrl },
|
||||
} = config
|
||||
|
||||
camp.route(/\.(gif|jpg)$/, (query, match, end, request) => {
|
||||
const [, format] = match
|
||||
makeSend(
|
||||
'svg',
|
||||
request.res,
|
||||
end
|
||||
)(
|
||||
app.get(/\.(gif|jpg)$/, (req, res) => {
|
||||
res.status(410)
|
||||
res.setHeader('Content-Type', 'image/svg+xml;charset=utf-8')
|
||||
|
||||
const format = req.params[0]
|
||||
res.send(
|
||||
makeBadge({
|
||||
label: '410',
|
||||
message: `${format} no longer available`,
|
||||
@@ -354,41 +323,53 @@ class Server {
|
||||
format: 'svg',
|
||||
})
|
||||
)
|
||||
|
||||
res.end()
|
||||
})
|
||||
|
||||
if (!rasterUrl) {
|
||||
camp.route(/\.png$/, (query, match, end, request) => {
|
||||
makeSend(
|
||||
'svg',
|
||||
request.res,
|
||||
end
|
||||
)(
|
||||
app.get(/\.png$/, (req, res) => {
|
||||
res.status(404)
|
||||
res.setHeader('Content-Type', 'image/svg+xml;charset=utf-8')
|
||||
res.send(
|
||||
makeBadge({
|
||||
label: '404',
|
||||
message: 'raster badges not available',
|
||||
color: 'lightgray',
|
||||
format: 'svg',
|
||||
})
|
||||
)
|
||||
res.end()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
camp.notfound(/(\.svg|\.json|)$/, (query, match, end, request) => {
|
||||
const [, extension] = match
|
||||
const format = (extension || '.svg').replace(/^\./, '')
|
||||
registerNotFoundHandlers() {
|
||||
const { app } = this
|
||||
|
||||
makeSend(
|
||||
format,
|
||||
request.res,
|
||||
end
|
||||
)(
|
||||
app.get(/\.json$/, (req, res) => {
|
||||
res.status(404)
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.json(
|
||||
makeJsonBadge({
|
||||
label: '404',
|
||||
message: 'badge not found',
|
||||
color: 'red',
|
||||
})
|
||||
)
|
||||
res.end()
|
||||
})
|
||||
|
||||
app.get(/(?:\.svg|)$/, (req, res) => {
|
||||
res.status(404)
|
||||
res.setHeader('Content-Type', 'image/svg+xml;charset=utf-8')
|
||||
res.send(
|
||||
makeBadge({
|
||||
label: '404',
|
||||
message: 'badge not found',
|
||||
color: 'red',
|
||||
format,
|
||||
})
|
||||
)
|
||||
res.end()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -400,54 +381,62 @@ class Server {
|
||||
* to {@link https://shields.io/} )
|
||||
*/
|
||||
registerRedirects() {
|
||||
const { config, camp } = this
|
||||
const { config, app } = this
|
||||
const {
|
||||
public: { rasterUrl, redirectUrl },
|
||||
} = config
|
||||
|
||||
if (rasterUrl) {
|
||||
// Redirect to the raster server for raster versions of modern badges.
|
||||
camp.route(/\.png$/, (queryParams, match, end, ask) => {
|
||||
ask.res.statusCode = 301
|
||||
ask.res.setHeader(
|
||||
'Location',
|
||||
rasterRedirectUrl({ rasterUrl }, ask.req.url)
|
||||
)
|
||||
app.get(/\.png$/, (req, res) => {
|
||||
res.status(301)
|
||||
res.setHeader('Location', rasterRedirectUrl({ rasterUrl }, req.url))
|
||||
|
||||
const cacheDuration = (30 * 24 * 3600) | 0 // 30 days.
|
||||
ask.res.setHeader('Cache-Control', `max-age=${cacheDuration}`)
|
||||
res.setHeader('Cache-Control', `max-age=${cacheDuration}`)
|
||||
|
||||
ask.res.end()
|
||||
res.end()
|
||||
})
|
||||
}
|
||||
|
||||
if (redirectUrl) {
|
||||
camp.route(/^\/$/, (data, match, end, ask) => {
|
||||
ask.res.statusCode = 302
|
||||
ask.res.setHeader('Location', redirectUrl)
|
||||
ask.res.end()
|
||||
app.get('/', (req, res) => {
|
||||
res.status(302)
|
||||
res.setHeader('Location', redirectUrl)
|
||||
res.end()
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
This is here for legacy reasons. The badge server and frontend used to live
|
||||
on two different servers. When we merged them there was a conflict so we did
|
||||
this to avoid moving the endpoint docs to another URL.
|
||||
|
||||
Never ever do this again.
|
||||
*/
|
||||
app.use('/endpoint', (req, res, next) => {
|
||||
if (Object.keys(req.query).length === 0) {
|
||||
res.status(301)
|
||||
res.setHeader('Location', '/endpoint/')
|
||||
res.end()
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate all the service classes defined in /services,
|
||||
* load each service and register a Scoutcamp route for each service.
|
||||
* load each service and register an Express route for each service.
|
||||
*/
|
||||
async registerServices() {
|
||||
const { config, camp, metricInstance } = this
|
||||
const { app, config, metricInstance } = this
|
||||
const { apiProvider: githubApiProvider } = this.githubConstellation
|
||||
const { apiProvider: librariesIoApiProvider } =
|
||||
this.librariesioConstellation
|
||||
;(await loadServiceClasses()).forEach(serviceClass =>
|
||||
serviceClass.register(
|
||||
{
|
||||
camp,
|
||||
handleRequest,
|
||||
githubApiProvider,
|
||||
librariesIoApiProvider,
|
||||
metricInstance,
|
||||
},
|
||||
{ app, githubApiProvider, librariesIoApiProvider, metricInstance },
|
||||
{
|
||||
handleInternalErrors: config.public.handleInternalErrors,
|
||||
cacheHeaders: config.public.cacheHeaders,
|
||||
@@ -480,11 +469,14 @@ class Server {
|
||||
|
||||
/**
|
||||
* Start the HTTP server:
|
||||
* Bootstrap Scoutcamp,
|
||||
* Bootstrap Express,
|
||||
* Register handlers,
|
||||
* Start listening for requests on this.baseUrl()
|
||||
*
|
||||
* @param {Function} registerExtras Optional function to register additional
|
||||
* routes, used for testing.
|
||||
*/
|
||||
async start() {
|
||||
async start(registerExtras) {
|
||||
const {
|
||||
bind: { port, address: hostname },
|
||||
ssl: { isSecure: secure, cert, key },
|
||||
@@ -496,25 +488,17 @@ class Server {
|
||||
|
||||
log.log(`Server is starting up: ${this.baseUrl}`)
|
||||
|
||||
const camp = (this.camp = Camp.create({
|
||||
documentRoot: this.config.public.documentRoot,
|
||||
port,
|
||||
hostname,
|
||||
secure,
|
||||
staticMaxAge: 300,
|
||||
cert,
|
||||
key,
|
||||
}))
|
||||
const app = (this.app = express())
|
||||
|
||||
if (requireCloudflare) {
|
||||
this.requireCloudflare()
|
||||
}
|
||||
|
||||
const { githubConstellation, metricInstance } = this
|
||||
await githubConstellation.initialize(camp)
|
||||
await githubConstellation.initialize(app)
|
||||
if (metricInstance) {
|
||||
if (this.config.public.metrics.prometheus.endpointEnabled) {
|
||||
metricInstance.registerMetricsEndpoint(camp)
|
||||
metricInstance.registerMetricsEndpoint(app)
|
||||
}
|
||||
if (this.influxMetrics) {
|
||||
this.influxMetrics.startPushingMetrics()
|
||||
@@ -522,39 +506,47 @@ class Server {
|
||||
}
|
||||
|
||||
const { apiProvider: githubApiProvider } = this.githubConstellation
|
||||
setRoutes(allowedOrigin, githubApiProvider, camp)
|
||||
setSuggestRoutes(allowedOrigin, githubApiProvider, app)
|
||||
|
||||
// https://github.com/badges/shields/issues/3273
|
||||
camp.handle((req, res, next) => {
|
||||
app.use((req, res, next) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*')
|
||||
next()
|
||||
})
|
||||
|
||||
this.registerErrorHandlers()
|
||||
this.registerRedirects()
|
||||
await this.registerServices()
|
||||
|
||||
camp.timeout = this.config.public.requestTimeoutSeconds * 1000
|
||||
if (this.config.public.requestTimeoutSeconds > 0) {
|
||||
camp.on('timeout', socket => {
|
||||
const maxAge = this.config.public.requestTimeoutMaxAgeSeconds
|
||||
socket.write('HTTP/1.1 408 Request Timeout\r\n')
|
||||
socket.write('Content-Type: text/html; charset=UTF-8\r\n')
|
||||
socket.write('Content-Encoding: UTF-8\r\n')
|
||||
socket.write(`Cache-Control: max-age=${maxAge}, s-maxage=${maxAge}\r\n`)
|
||||
socket.write('Connection: close\r\n\r\n')
|
||||
socket.write('Request Timeout')
|
||||
socket.end()
|
||||
app.use(
|
||||
express.static(this.config.public.documentRoot, {
|
||||
// Since express's `maxAge` parameter sets `Cache-Control: public`, set
|
||||
// the headers manually insetad.
|
||||
cacheControl: false,
|
||||
setHeaders: res =>
|
||||
res.setHeader('Cache-Control', 'max-age=300, s-maxage=300'),
|
||||
})
|
||||
)
|
||||
await this.registerServices()
|
||||
if (registerExtras) {
|
||||
registerExtras(app)
|
||||
}
|
||||
camp.listenAsConfigured()
|
||||
this.registerNotFoundHandlers()
|
||||
|
||||
await new Promise(resolve => camp.on('listening', () => resolve()))
|
||||
if (secure) {
|
||||
this.server = https.createServer({ hostname, cert, key }, app)
|
||||
} else {
|
||||
this.server = http.createServer({ hostname }, app)
|
||||
}
|
||||
|
||||
this.server.setTimeout(this.config.public.requestTimeoutSeconds * 1000)
|
||||
|
||||
await new Promise(resolve =>
|
||||
this.server.listen({ host: hostname, port }, () => resolve())
|
||||
)
|
||||
}
|
||||
|
||||
static resetGlobalState() {
|
||||
// This state should be migrated to instance state. When possible, do not add new
|
||||
// global state.
|
||||
// TODO: This state should be migrated to instance state. When possible, do
|
||||
// not add new global state.
|
||||
clearResourceCache()
|
||||
}
|
||||
|
||||
@@ -566,10 +558,11 @@ class Server {
|
||||
* Stop the HTTP server and clean up helpers
|
||||
*/
|
||||
async stop() {
|
||||
if (this.camp) {
|
||||
await new Promise(resolve => this.camp.close(resolve))
|
||||
this.camp = undefined
|
||||
if (this.server) {
|
||||
await new Promise(resolve => this.server.close(() => resolve()))
|
||||
this.server = undefined
|
||||
}
|
||||
this.app = undefined
|
||||
|
||||
if (this.cleanupMonitor) {
|
||||
this.cleanupMonitor()
|
||||
|
||||
@@ -73,9 +73,7 @@ describe('The server', function () {
|
||||
it('should redirect colorscheme PNG badges as configured', async function () {
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}:fruit-apple-green.png`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
{ followRedirect: false }
|
||||
)
|
||||
expect(statusCode).to.equal(301)
|
||||
expect(headers.location).to.equal(
|
||||
@@ -98,7 +96,7 @@ describe('The server', function () {
|
||||
`${baseUrl}:fruit-apple-green.svg`
|
||||
)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(headers['content-type']).to.equal('image/svg+xml;charset=utf-8')
|
||||
expect(headers['content-type']).to.equal('image/svg+xml; charset=utf-8')
|
||||
expect(headers['content-length']).to.equal('1130')
|
||||
})
|
||||
|
||||
@@ -112,7 +110,9 @@ describe('The server', function () {
|
||||
`${baseUrl}:fruit-apple-green.json`
|
||||
)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(headers['content-type']).to.equal('application/json')
|
||||
expect(headers['content-type']).to.equal(
|
||||
'application/json; charset=utf-8'
|
||||
)
|
||||
expect(headers['access-control-allow-origin']).to.equal('*')
|
||||
expect(headers['content-length']).to.equal('92')
|
||||
expect(() => JSON.parse(body)).not.to.throw()
|
||||
@@ -200,19 +200,12 @@ describe('The server', function () {
|
||||
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(statusCode).to.equal(410)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.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 () {
|
||||
@@ -245,22 +238,12 @@ describe('The server', function () {
|
||||
|
||||
// configure server to time out requests that take >2 seconds
|
||||
server = await createTestServer({ public: { requestTimeoutSeconds: 2 } })
|
||||
await server.start()
|
||||
await server.start(app => {
|
||||
// /fast returns a 200 OK after a 1 second delay
|
||||
app.get('/fast', (req, res) => setTimeout(() => res.end(), 1000))
|
||||
|
||||
// /fast returns a 200 OK after a 1 second delay
|
||||
server.camp.route(/^\/fast$/, (data, match, end, ask) => {
|
||||
setTimeout(() => {
|
||||
ask.res.statusCode = 200
|
||||
ask.res.end()
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
// /slow returns a 200 OK after a 3 second delay
|
||||
server.camp.route(/^\/slow$/, (data, match, end, ask) => {
|
||||
setTimeout(() => {
|
||||
ask.res.statusCode = 200
|
||||
ask.res.end()
|
||||
}, 3000)
|
||||
// /slow returns a 200 OK after a 3 second delay
|
||||
app.get('/slow', (req, res) => setTimeout(() => res.end(), 3000))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -273,11 +256,9 @@ describe('The server', function () {
|
||||
|
||||
it('should time out slow requests', async function () {
|
||||
this.timeout(10000)
|
||||
const { statusCode, body } = await got(`${server.baseUrl}slow`, {
|
||||
throwHttpErrors: false,
|
||||
})
|
||||
expect(statusCode).to.be.equal(408)
|
||||
expect(body).to.equal('Request Timeout')
|
||||
await expect(got(`${server.baseUrl}slow`)).to.be.rejectedWith(
|
||||
'socket hang up'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not time out fast requests', async function () {
|
||||
|
||||
@@ -59,9 +59,7 @@ function _inferPullRequestFromTravisEnv(env) {
|
||||
}
|
||||
|
||||
function _inferPullRequestFromCircleEnv(env) {
|
||||
return parseGithubPullRequestUrl(
|
||||
env.CI_PULL_REQUEST || env.CIRCLE_PULL_REQUEST
|
||||
)
|
||||
return parseGithubPullRequestUrl(env.CI_PULL_REQUEST)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { defineConfig } from 'cypress'
|
||||
|
||||
export default defineConfig({
|
||||
fixturesFolder: false,
|
||||
env: {
|
||||
backend_url: 'http://localhost:8080',
|
||||
},
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {},
|
||||
baseUrl: 'http://localhost:3000',
|
||||
supportFile: false,
|
||||
},
|
||||
})
|
||||
9
cypress.json
Normal file
9
cypress.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"baseUrl": "http://localhost:3000",
|
||||
"fixturesFolder": false,
|
||||
"pluginsFile": false,
|
||||
"supportFile": false,
|
||||
"env": {
|
||||
"backend_url": "http://localhost:8080"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,3 @@
|
||||
import { registerCommand } from 'cypress-wait-for-stable-dom'
|
||||
|
||||
registerCommand()
|
||||
|
||||
describe('Main page', function () {
|
||||
const backendUrl = Cypress.env('backend_url')
|
||||
const SEARCH_INPUT = 'input[placeholder="search / project URL"]'
|
||||
@@ -13,13 +9,8 @@ describe('Main page', function () {
|
||||
.should('have.attr', 'src', previewUrl)
|
||||
}
|
||||
|
||||
function visitAndWait(page) {
|
||||
cy.visit(page)
|
||||
cy.waitForStableDOM({ pollInterval: 1000, timeout: 10000 })
|
||||
}
|
||||
|
||||
it('Search for badges', function () {
|
||||
visitAndWait('/')
|
||||
cy.visit('/')
|
||||
|
||||
cy.get(SEARCH_INPUT).type('pypi')
|
||||
|
||||
@@ -27,7 +18,7 @@ describe('Main page', function () {
|
||||
})
|
||||
|
||||
it('Shows badge from category', function () {
|
||||
visitAndWait('/category/chat')
|
||||
cy.visit('/category/chat')
|
||||
|
||||
expectBadgeExample(
|
||||
'Discourse status',
|
||||
@@ -38,7 +29,7 @@ describe('Main page', function () {
|
||||
|
||||
it('Suggest badges', function () {
|
||||
const badgeUrl = `${backendUrl}/github/issues/badges/shields`
|
||||
visitAndWait('/')
|
||||
cy.visit('/')
|
||||
|
||||
cy.get(SEARCH_INPUT).type('https://github.com/badges/shields')
|
||||
cy.contains('Suggest badges').click()
|
||||
@@ -48,7 +39,7 @@ describe('Main page', function () {
|
||||
|
||||
it('Customization form is filled with suggested badge details', function () {
|
||||
const badgeUrl = `${backendUrl}/github/issues/badges/shields`
|
||||
visitAndWait('/')
|
||||
cy.visit('/')
|
||||
cy.get(SEARCH_INPUT).type('https://github.com/badges/shields')
|
||||
cy.contains('Suggest badges').click()
|
||||
|
||||
@@ -60,7 +51,7 @@ describe('Main page', function () {
|
||||
|
||||
it('Customizate suggested badge', function () {
|
||||
const badgeUrl = `${backendUrl}/github/issues/badges/shields`
|
||||
visitAndWait('/')
|
||||
cy.visit('/')
|
||||
cy.get(SEARCH_INPUT).type('https://github.com/badges/shields')
|
||||
cy.contains('Suggest badges').click()
|
||||
cy.contains(badgeUrl).click()
|
||||
@@ -71,7 +62,7 @@ describe('Main page', function () {
|
||||
})
|
||||
|
||||
it('Do not duplicate example parameters', function () {
|
||||
visitAndWait('/category/funding')
|
||||
cy.visit('/category/funding')
|
||||
|
||||
cy.contains('GitHub Sponsors').click()
|
||||
cy.get('[name="style"]').should($style => {
|
||||
@@ -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('')
|
||||
|
||||
@@ -25,7 +25,7 @@ and learn about the [GitHub workflow](http://try.github.io/).
|
||||
|
||||
#### Node, NPM
|
||||
|
||||
Node >=16 and NPM >=8 is required. If you don't already have them,
|
||||
Node >=16 and NPM >=7 is required. If you don't already have them,
|
||||
install node and npm: https://nodejs.org/en/download/
|
||||
|
||||
### Setup a dev install
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
- The format of new badges should be of the form `/SERVICE/NOUN/PARAMETERS?QUERYSTRING` e.g:
|
||||
`/github/issues/:user/:repo`. The service is github, the
|
||||
badge is for issues, and the parameters are `:user/:repo`.
|
||||
- The `NOUN` part of the route is:
|
||||
- singular if the badge message represents a single entity, such as the current status of a build (e.g: `/build`), or a more abstract or aggregate representation of the thing (e.g.: `/coverage`, `/quality`)
|
||||
- plural if there are (or may) be many of the thing (e.g: `/dependencies`, `/stars`)
|
||||
- Parameters should always be part of the route if they are required to display a badge e.g: `:packageName`.
|
||||
- Common optional params like, `:branch` or `:tag` should also be passed as part of the route.
|
||||
- Query string parameters should be used when:
|
||||
|
||||
@@ -58,7 +58,7 @@ The tests are also divided into several parts:
|
||||
[redis-token-persistence.integration]: https://github.com/badges/shields/blob/master/core/token-pooling/redis-token-persistence.integration.js
|
||||
[github-api-provider.integration]: https://github.com/badges/shields/blob/master/services/github/github-api-provider.integration.js
|
||||
|
||||
Our goal is 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.
|
||||
|
||||
@@ -80,29 +80,22 @@ test this kind of logic through unit tests (e.g. of `render()` and
|
||||
reporting, loads config, and creates an instance of the server.
|
||||
|
||||
2. The Server, which is defined in
|
||||
[`core/server/server.js`][core/server/server], is based on the web
|
||||
framework [Scoutcamp][]. It creates an http server, sets up helpers for
|
||||
token persistence and monitoring. Then it loads all the services,
|
||||
injecting dependencies as it asks each one to register its route
|
||||
with Scoutcamp.
|
||||
[`core/server/server.js`][core/server/server], is based on [Express][].
|
||||
It creates an http server, sets up helpers for token persistence and
|
||||
monitoring. Then it loads all the services, injecting dependencies as it
|
||||
asks each one to register its route with the Express app.
|
||||
|
||||
3. The service registration continues in `BaseService.register`. From its
|
||||
`route` property, it derives a regular expression to match the route
|
||||
path, and invokes `camp.route` with this value.
|
||||
path, and invokes `app.get` with this value.
|
||||
|
||||
4. At this point the situation gets gnarly and hard to follow. For the
|
||||
purpose of initialization, suffice it to say that `camp.route` invokes a
|
||||
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
|
||||
parameters `( queryParams, match, sendBadge )`, which
|
||||
then runs `BaseService.invoke`. `BaseService.invoke` instantiates the
|
||||
service and runs `BaseService#handle`.
|
||||
4. TODO: Explain what happens here (i.e. now that we've migrated from Scoutcamp
|
||||
to Express). `BaseService.invoke` instantiates the service and runs
|
||||
`BaseService#handle`.
|
||||
|
||||
[entrypoint]: https://github.com/badges/shields/blob/master/server.js
|
||||
[core/server/server]: https://github.com/badges/shields/blob/master/core/server/server.js
|
||||
[scoutcamp]: https://github.com/espadrine/sc
|
||||
[express]: https://expressjs.com/
|
||||
[legacy-request-handler]: https://github.com/badges/shields/blob/master/core/base-service/legacy-request-handler.js
|
||||
|
||||
## Downstream caching
|
||||
@@ -119,24 +112,15 @@ test this kind of logic through unit tests (e.g. of `render()` and
|
||||
|
||||
## How the server makes a badge
|
||||
|
||||
1. An HTTPS request arrives. Scoutcamp inspects the URL path and matches it
|
||||
against the regexes for all the registered routes until it finds one that
|
||||
matches. (See *Initialization* above for an explanation of how routes are
|
||||
1. An HTTPS request arrives. Express inspects the URL path and matches it
|
||||
against all the registered routes until it finds one that matches. (See
|
||||
*Initialization* above for an explanation of how routes are
|
||||
registered.)
|
||||
2. Scoutcamp invokes a callback with the four parameters:
|
||||
`( queryParams, match, end, ask )`. This callback is defined in
|
||||
[`legacy-request-handler`][legacy-request-handler]. A timeout is set to
|
||||
handle unresponsive service code and the next callback is invoked: the
|
||||
legacy handler function.
|
||||
3. The legacy handler function receives
|
||||
`( queryParams, match, sendBadge )`. Its job is to extract data
|
||||
from the regex `match` and `queryParams`, and then invoke `sendBadge`
|
||||
with the result.
|
||||
4. The implementation of this function is in `BaseService.register`. It
|
||||
works by running `BaseService.invoke`, which instantiates the service,
|
||||
injects more dependencies, and invokes `BaseService.handle` which is
|
||||
implemented by the service subclass.
|
||||
5. The job of `handle()`, which should be implemented by each service
|
||||
2. Invoke the request handler function, defined in `BaseService.register`,
|
||||
which handles the request. It runs `BaseService.invoke`, which instantiates
|
||||
the service, injects more dependencies, and invokes `BaseService.handle`
|
||||
which is implemented by the service subclass.
|
||||
3. The job of `handle()`, which should be implemented by each service
|
||||
subclass, is to return an object which partially describes a badge or
|
||||
throw one of the handled error classes. "Partially rendered" most
|
||||
commonly means a non-empty message and an optional color. In the case
|
||||
@@ -146,7 +130,7 @@ test this kind of logic through unit tests (e.g. of `render()` and
|
||||
Throwing any other error is a programmer error which will be
|
||||
[reported][error reporting] and described to the user as a **shields
|
||||
internal error**.
|
||||
6. A typical `handle()` function delegates to one or more helpers to
|
||||
4. A typical `handle()` function delegates to one or more helpers to
|
||||
handle stages of the request:
|
||||
1. **fetch**: load the needed data from the upstream service and
|
||||
validate it
|
||||
@@ -154,13 +138,13 @@ test this kind of logic through unit tests (e.g. of `render()` and
|
||||
into a few properties which will be displayed on the badge
|
||||
3. **render**: given a few properties, return a message, optional
|
||||
color, and optional label.
|
||||
7. When an error is thrown, BaseService steps in and converts the error
|
||||
5. When an error is thrown, BaseService steps in and converts the error
|
||||
object to renderable properties: `{ isError, message, color }`.
|
||||
8. The service invokes [`coalesceBadge`][coalescebadge] whose job is to
|
||||
6. The service invokes [`coalesceBadge`][coalescebadge] whose job is to
|
||||
coalesce query string overrides with values from the service and the
|
||||
service’s defaults to produce an object that fully describes the badge to
|
||||
be rendered.
|
||||
9. `sendBadge` is invoked with that object. It does some housekeeping on the
|
||||
7. `sendBadge` is invoked with that object. It does some housekeeping on the
|
||||
timeout. Then it renders the badge to svg or raster and pushes out the
|
||||
result over the HTTPS connection.
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ If you are submitting a pull request for a custom logo, please:
|
||||
- Install SVGO
|
||||
- With npm: `npm install -g svgo`
|
||||
- With Homebrew: `brew install svgo`
|
||||
- Run the following command `svgo --precision=3 icon.svg -o icon.min.svg`
|
||||
- Run the following command `svgo --precision=3 icon.svg icon.min.svg`
|
||||
- Check if there is a loss of quality in the output, if so increase the precision.
|
||||
- The [SVGOMG Online Tool][svgomg]
|
||||
- Click "Open SVG" and select an SVG file.
|
||||
|
||||
@@ -16,8 +16,9 @@ Production hosting is managed by the Shields ops team:
|
||||
|
||||
| Component | Subcomponent | People with access |
|
||||
| ----------------------------- | ------------------------------- | --------------------------------------------------------------- |
|
||||
| shields-io-production | Full access | @calebcartwright, @chris48s, @paulmelnikow |
|
||||
| shields-io-production | Access management | @calebcartwright, @chris48s, @paulmelnikow |
|
||||
| shields-production-us | Account owner | @calebcartwright, @paulmelnikow |
|
||||
| shields-production-us | Full access | @calebcartwright, @chris48s, @paulmelnikow, @pyvesb |
|
||||
| shields-production-us | Access management | @calebcartwright, @chris48s, @paulmelnikow, @pyvesb |
|
||||
| Compose.io Redis | Account owner | @paulmelnikow |
|
||||
| Compose.io Redis | Account access | @paulmelnikow |
|
||||
| Compose.io Redis | Database connection credentials | @calebcartwright, @chris48s, @paulmelnikow, @pyvesb |
|
||||
@@ -94,10 +95,13 @@ The raster server `raster.shields.io` (a.k.a. the rasterizing proxy) is
|
||||
hosted on Heroku. It's managed in the
|
||||
[squint](https://github.com/badges/squint/) repo.
|
||||
|
||||
### Fly.io Deployment
|
||||
### Heroku Deployment
|
||||
|
||||
Both the badge server and frontend are served from Fly.io. Deployments are
|
||||
triggered using GitHub actions in a private repo.
|
||||
Both the badge server and frontend are served from Heroku.
|
||||
|
||||
After merging a commit to master, heroku should create a staging deploy. Check this has deployed correctly in the `shields-staging` pipeline and review https://shields-staging.herokuapp.com/
|
||||
|
||||
If we're happy with it, "promote to production". This will deploy what's on staging to the `shields-production-eu` and `shields-production-us` pieplines.
|
||||
|
||||
## DNS
|
||||
|
||||
@@ -105,15 +109,19 @@ DNS is registered with [DNSimple][].
|
||||
|
||||
[dnsimple]: https://dnsimple.com/
|
||||
|
||||
## Logs
|
||||
|
||||
Logs can be retrieved [from heroku](https://devcenter.heroku.com/articles/logging#log-retrieval).
|
||||
|
||||
## Error reporting
|
||||
|
||||
[Error reporting][sentry] is one of the most useful tools we have for monitoring
|
||||
the server. It's generously donated by [Sentry][sentry home]. We bundle
|
||||
[`@sentry/node`][sentry-node] into the application, and the Sentry DSN is configured
|
||||
via `local-shields-io-production.yml` (see [documentation][sentry configuration]).
|
||||
[`raven`][raven] into the application, and the Sentry DSN is configured via
|
||||
`local-shields-io-production.yml` (see [documentation][sentry configuration]).
|
||||
|
||||
[sentry]: https://sentry.io/shields/
|
||||
[sentry-node]: https://www.npmjs.com/package/@sentry/node
|
||||
[raven]: https://www.npmjs.com/package/raven
|
||||
[sentry home]: https://sentry.io/shields/
|
||||
[sentry configuration]: https://github.com/badges/shields/blob/master/doc/self-hosting.md#sentry
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ machine.
|
||||
|
||||
If you want to host PNG badges, you can also self-host a [raster server][]
|
||||
which points to your badge server. It's a docker container. We host it on
|
||||
Fly.io but should be possible to host on a wide variety of platforms.
|
||||
Heroku but should be possible to host on a wide variety of platforms.
|
||||
|
||||
- In your raster instance, set `BASE_URL` to your Shields instance, e.g.
|
||||
`https://shields.example.co`.
|
||||
|
||||
@@ -67,7 +67,7 @@ t.create('Build status')
|
||||
- All badges on shields can be requested in a number of formats. As well as calling https://img.shields.io/wercker/build/wercker/go-wercker-api.svg to generate  we can also call https://img.shields.io/wercker/build/wercker/go-wercker-api.json to request the same content as JSON. When writing service tests, we request the badge in JSON format so it is easier to make assertions about the content.
|
||||
- We don't need to explicitly call `/wercker/build/wercker/go-wercker-api.json` here, only `/build/wercker/go-wercker-api.json`. When we create a tester object with `createServiceTester()` the URL base defined in our service class (in this case `/wercker`) is used as the base URL for any requests made by the tester object.
|
||||
3. `expectBadge()` is a helper function which accepts either a string literal, a [RegExp][] or a [Joi][] schema for the different fields.
|
||||
Joi is a validation library that is built into IcedFrisby which you can use to
|
||||
Joi is a validation library that is build into IcedFrisby which you can use to
|
||||
match based on a set of allowed strings, regexes, or specific values. You can
|
||||
refer to their [API reference][joi api].
|
||||
4. We expect `label` to be a string literal `"build"`.
|
||||
|
||||
@@ -51,14 +51,14 @@ test(reStructuredText, () => {
|
||||
'.. image:: https://img.shields.io/badge'
|
||||
)
|
||||
given('https://img.shields.io/badge', undefined, 'Example').expect(
|
||||
'.. image:: https://img.shields.io/badge\n :alt: Example'
|
||||
'.. image:: https://img.shields.io/badge :alt: Example'
|
||||
)
|
||||
given(
|
||||
'https://img.shields.io/badge',
|
||||
'https://example.com/example',
|
||||
'Example'
|
||||
).expect(
|
||||
'.. image:: https://img.shields.io/badge\n :alt: Example\n :target: https://example.com/example'
|
||||
'.. image:: https://img.shields.io/badge :alt: Example :target: https://example.com/example'
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -33,10 +33,10 @@ export function reStructuredText(
|
||||
): string {
|
||||
let result = `.. image:: ${badgeUrl}`
|
||||
if (title) {
|
||||
result += `\n :alt: ${title}`
|
||||
result += ` :alt: ${title}`
|
||||
}
|
||||
if (link) {
|
||||
result += `\n :target: ${link}`
|
||||
result += ` :target: ${link}`
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ export default function EndpointPage(): JSX.Element {
|
||||
</p>
|
||||
<p>
|
||||
The endpoint badge is a better alternative than redirecting to the
|
||||
static badge endpoint or generating SVG on your server:
|
||||
static badge enpoint or generating SVG on your server:
|
||||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
@@ -142,7 +142,7 @@ export default function EndpointPage(): JSX.Element {
|
||||
Content and presentation are separate.
|
||||
</a>{' '}
|
||||
The service provider authors the badge, and Shields takes input from
|
||||
the user to format it. As a service provider, you author the badge
|
||||
the user to format it. As a service provider you author the badge
|
||||
but don't have to concern yourself with styling. You don't even have
|
||||
to pass the formatting options through to Shields.
|
||||
</li>
|
||||
@@ -152,12 +152,12 @@ export default function EndpointPage(): JSX.Element {
|
||||
</li>
|
||||
<li>
|
||||
A JSON response is easy to implement; easier than an HTTP redirect.
|
||||
It is trivial in almost any framework and is more compatible with
|
||||
It is trivial in almost any framework, and is more compatible with
|
||||
hosting environments such as{' '}
|
||||
<a href="https://runkit.com/docs/endpoint">RunKit endpoints</a>.
|
||||
</li>
|
||||
<li>
|
||||
As a service provider, you can rely on the Shields CDN. There's no
|
||||
As a service provider you can rely on the Shields CDN. There's no
|
||||
need to study the HTTP headers. Adjusting cache behavior is as
|
||||
simple as setting a property in the JSON response.
|
||||
</li>
|
||||
@@ -197,7 +197,7 @@ export default function EndpointPage(): JSX.Element {
|
||||
<dd>
|
||||
Default: <code>false</code>. <code>true</code> to treat this as an
|
||||
error badge. This prevents the user from overriding the color. In the
|
||||
future, it may affect cache behavior.
|
||||
future it may affect cache behavior.
|
||||
</dd>
|
||||
<dt>namedLogo</dt>
|
||||
<dd>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as originalSimpleIcons from 'simple-icons/icons'
|
||||
import originalSimpleIcons from 'simple-icons'
|
||||
import { svg2base64 } from './svg-helpers.js'
|
||||
|
||||
function loadSimpleIcons() {
|
||||
@@ -14,29 +14,26 @@ function loadSimpleIcons() {
|
||||
// https://github.com/badges/shields/issues/4273
|
||||
Object.keys(originalSimpleIcons).forEach(key => {
|
||||
const icon = originalSimpleIcons[key]
|
||||
const { title, slug, hex } = icon
|
||||
|
||||
const title = icon.title.toLowerCase()
|
||||
const legacyTitle = title.replace(/ /g, '-')
|
||||
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"')),
|
||||
default: svg2base64(icon.svg.replace('<svg', `<svg fill="#${icon.hex}"`)),
|
||||
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
|
||||
// (e.g. 'Hive'). If a by-title reference we generate for
|
||||
// backwards compatibility collides with a proper slug from Simple Icons
|
||||
// then do nothing, so that the proper slug will always map to the correct icon.
|
||||
// Starting in v7, the exported object with the full icon set has updated the keys
|
||||
// to include a lowercase `si` prefix, and utilizes proper case naming conventions.
|
||||
if (!(`si${title}` in originalSimpleIcons)) {
|
||||
simpleIcons[title.toLowerCase()] = icon
|
||||
if (!(title in originalSimpleIcons)) {
|
||||
simpleIcons[title] = icon
|
||||
}
|
||||
const legacyTitle = title.replace(/ /g, '-')
|
||||
if (!(`si${legacyTitle}` in originalSimpleIcons)) {
|
||||
simpleIcons[legacyTitle.toLowerCase()] = icon
|
||||
if (!(legacyTitle in originalSimpleIcons)) {
|
||||
simpleIcons[legacyTitle] = icon
|
||||
}
|
||||
|
||||
simpleIcons[slug] = icon
|
||||
simpleIcons[key] = icon
|
||||
})
|
||||
return simpleIcons
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ describe('Logo helpers', function () {
|
||||
test(prepareNamedLogo, () => {
|
||||
// NPM uses multiple colors so the color param should be ignored
|
||||
const npmLogo =
|
||||
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZD0iTTAgMGg0MHY0MEgwVjB6IiBmaWxsPSIjY2IwMDAwIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTcgN2gyNnYyNmgtN1YxNGgtNnYxOUg3eiIvPjwvc3ZnPg=='
|
||||
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZD0iTTAgMGg0MHY0MEgwVjB6IiBmaWxsPSIjY2IwMDAwIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTcgN2gyNnYyNmgtN1YxNGgtNnYxOUg3eiIvPjwvc3ZnPgo='
|
||||
given({ name: 'npm' }).expect(npmLogo)
|
||||
given({ name: 'npm', color: 'blue' }).expect(npmLogo)
|
||||
|
||||
@@ -115,7 +115,7 @@ describe('Logo helpers', function () {
|
||||
undefined
|
||||
)
|
||||
given('npm', {}).expect(
|
||||
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZD0iTTAgMGg0MHY0MEgwVjB6IiBmaWxsPSIjY2IwMDAwIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTcgN2gyNnYyNmgtN1YxNGgtNnYxOUg3eiIvPjwvc3ZnPg=='
|
||||
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZD0iTTAgMGg0MHY0MEgwVjB6IiBmaWxsPSIjY2IwMDAwIi8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTcgN2gyNnYyNmgtN1YxNGgtNnYxOUg3eiIvPjwvc3ZnPgo='
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="93 93 194 194"><defs><style>.b{fill:#fc6d26}</style></defs><path style="fill:#e24329" d="m282.83 170.73-.27-.69-26.14-68.22a6.81 6.81 0 0 0-2.69-3.24 7 7 0 0 0-8 .43 7 7 0 0 0-2.32 3.52l-17.65 54h-71.47l-17.65-54a6.86 6.86 0 0 0-2.32-3.53 7 7 0 0 0-8-.43 6.87 6.87 0 0 0-2.69 3.24L97.44 170l-.26.69a48.54 48.54 0 0 0 16.1 56.1l.09.07.24.17 39.82 29.82 19.7 14.91 12 9.06a8.07 8.07 0 0 0 9.76 0l12-9.06 19.7-14.91 40.06-30 .1-.08a48.56 48.56 0 0 0 16.08-56.04Z"/><path class="b" d="m282.83 170.73-.27-.69a88.3 88.3 0 0 0-35.15 15.8L190 229.25c19.55 14.79 36.57 27.64 36.57 27.64l40.06-30 .1-.08a48.56 48.56 0 0 0 16.1-56.08Z"/><path style="fill:#fca326" d="m153.43 256.89 19.7 14.91 12 9.06a8.07 8.07 0 0 0 9.76 0l12-9.06 19.7-14.91S209.55 244 190 229.25c-19.55 14.75-36.57 27.64-36.57 27.64Z"/><path class="b" d="M132.58 185.84A88.19 88.19 0 0 0 97.44 170l-.26.69a48.54 48.54 0 0 0 16.1 56.1l.09.07.24.17 39.82 29.82L190 229.21Z"/></svg>
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M23.956 13.587l-1.344-4.133a4549.814 4549.814 0 0 0-2.663-8.189.456.456 0 0 0-.87 0l-2.658 8.189H7.585L4.92 1.265a.456.456 0 0 0-.87 0A4549.814 4549.814 0 0 0 .044 13.587a.908.908 0 0 0 .336 1.02L12 23.054l11.62-8.447a.908.908 0 0 0 .336-1.02" fill="#fc6d26"/><path d="M12 23.054l4.421-13.6H7.58z" fill="#e24329"/><path d="M7.579 9.454H1.388L12 23.054z" fill="#fc6d26"/><path d="M1.388 9.454L.044 13.587a.908.908 0 0 0 .336 1.02L12 23.054z" fill="#fca326"/><path d="M7.579 9.454L4.92 1.265a.456.456 0 0 0-.87 0L1.388 9.454z" fill="#e24329"/><path d="M16.421 9.454h6.191L12 23.054z" fill="#fc6d26"/><path d="M22.612 9.454l1.344 4.133a.908.908 0 0 1-.336 1.02L12 23.054z" fill="#fca326"/><path d="M16.421 9.454l2.658-8.189a.456.456 0 0 1 .87 0l2.663 8.189z" fill="#e24329"/></svg>
|
||||
|
Before Width: | Height: | Size: 986 B After Width: | Height: | Size: 847 B |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 24c6.627 0 12-5.373 12-12S18.627 0 12 0 0 5.373 0 12s5.373 12 12 12Z" fill="url(#a)"/><path fill-rule="evenodd" clip-rule="evenodd" d="M5.425 11.871a796.414 796.414 0 0 1 6.994-3.018c3.328-1.388 4.027-1.628 4.477-1.638.1 0 .32.02.47.14.12.1.15.23.17.33.02.1.04.31.02.47-.18 1.898-.96 6.504-1.36 8.622-.17.9-.5 1.199-.819 1.229-.7.06-1.229-.46-1.898-.9-1.06-.689-1.649-1.119-2.678-1.798-1.19-.78-.42-1.209.26-1.908.18-.18 3.247-2.978 3.307-3.228.01-.03.01-.15-.06-.21-.07-.06-.17-.04-.25-.02-.11.02-1.788 1.14-5.056 3.348-.48.33-.909.49-1.299.48-.43-.01-1.248-.24-1.868-.44-.75-.24-1.349-.37-1.299-.79.03-.22.33-.44.89-.669Z" fill="#fff"/><defs><linearGradient id="a" x1="11.99" y1="0" x2="11.99" y2="23.81" gradientUnits="userSpaceOnUse"><stop stop-color="#2AABEE"/><stop offset="1" stop-color="#229ED9"/></linearGradient></defs></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="12" cy="12" r="12" fill="#2ca5e0"/><path d="M9.8 17.5c-.389 0-.323-.147-.457-.517L8.2 13.221 17 8" fill="#a9c9dd"/><path d="M9.8 17.5c.3 0 .433-.137.6-.3l1.6-1.556-1.996-1.203" fill="#c8daea"/><path d="M10.004 14.441l4.836 3.573c.552.304.95.147 1.088-.512l1.968-9.277c.202-.808-.308-1.174-.836-.935L5.501 11.748c-.789.316-.784.756-.144.952l2.967.926 6.867-4.332c.324-.197.622-.091.377.125" fill="#fff"/></svg>
|
||||
|
Before Width: | Height: | Size: 909 B After Width: | Height: | Size: 481 B |
23227
package-lock.json
generated
23227
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
125
package.json
125
package.json
@@ -21,48 +21,48 @@
|
||||
"url": "https://github.com/badges/shields"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/lato": "^4.5.10",
|
||||
"@fontsource/lekton": "^4.5.11",
|
||||
"@fontsource/lato": "^4.5.5",
|
||||
"@fontsource/lekton": "^4.5.6",
|
||||
"@renovate/pep440": "^1.0.0",
|
||||
"@renovatebot/ruby-semver": "^1.1.6",
|
||||
"@sentry/node": "^7.17.2",
|
||||
"@shields_io/camp": "^18.1.1",
|
||||
"@sentry/node": "^6.19.6",
|
||||
"badge-maker": "file:badge-maker",
|
||||
"bytes": "^3.1.2",
|
||||
"camelcase": "^7.0.0",
|
||||
"chalk": "^5.1.2",
|
||||
"camelcase": "^6.3.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.6",
|
||||
"decamelize": "^3.2.0",
|
||||
"emojic": "^1.1.17",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"fast-xml-parser": "^4.0.11",
|
||||
"glob": "^8.0.3",
|
||||
"express": "^4.17.3",
|
||||
"fast-xml-parser": "^4.0.7",
|
||||
"glob": "^8.0.1",
|
||||
"global-agent": "^3.0.0",
|
||||
"got": "^12.5.2",
|
||||
"got": "^12.0.3",
|
||||
"graphql": "^15.6.1",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"ioredis": "5.2.3",
|
||||
"joi": "17.6.4",
|
||||
"ioredis": "5.0.4",
|
||||
"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.2",
|
||||
"multer": "^1.4.4",
|
||||
"node-env-flag": "^0.1.0",
|
||||
"parse-link-header": "^2.0.0",
|
||||
"path-to-regexp": "^6.2.1",
|
||||
"path-to-regexp": "^6.2.0",
|
||||
"pretty-bytes": "^6.0.0",
|
||||
"priorityqueuejs": "^2.0.0",
|
||||
"prom-client": "^14.1.0",
|
||||
"qs": "^6.11.0",
|
||||
"prom-client": "^14.0.1",
|
||||
"qs": "^6.10.3",
|
||||
"query-string": "^7.1.1",
|
||||
"semver": "~7.3.8",
|
||||
"simple-icons": "7.17.0",
|
||||
"semver": "~7.3.7",
|
||||
"simple-icons": "6.18.0",
|
||||
"webextension-store-meta": "^1.0.5",
|
||||
"xmldom": "~0.6.0",
|
||||
"xpath": "~0.0.32"
|
||||
@@ -142,25 +142,25 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.19.6",
|
||||
"@babel/core": "^7.17.9",
|
||||
"@babel/polyfill": "^7.12.1",
|
||||
"@babel/register": "7.18.9",
|
||||
"@babel/register": "7.17.7",
|
||||
"@istanbuljs/schema": "^0.1.3",
|
||||
"@mapbox/react-click-to-select": "^2.2.1",
|
||||
"@types/chai": "^4.3.3",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/lodash.groupby": "^4.6.7",
|
||||
"@types/mocha": "^10.0.0",
|
||||
"@types/chai": "^4.3.1",
|
||||
"@types/lodash.debounce": "^4.0.6",
|
||||
"@types/lodash.groupby": "^4.6.6",
|
||||
"@types/mocha": "^9.1.0",
|
||||
"@types/node": "^16.7.10",
|
||||
"@types/react-helmet": "^6.1.5",
|
||||
"@types/react-modal": "^3.13.1",
|
||||
"@types/react-select": "^4.0.17",
|
||||
"@types/styled-components": "5.1.26",
|
||||
"@typescript-eslint/eslint-plugin": "^5.41.0",
|
||||
"@typescript-eslint/parser": "^5.30.7",
|
||||
"@types/styled-components": "5.1.25",
|
||||
"@typescript-eslint/eslint-plugin": "^5.19.0",
|
||||
"@typescript-eslint/parser": "^5.15.0",
|
||||
"babel-plugin-inline-react-svg": "^2.0.1",
|
||||
"babel-preset-gatsby": "^2.22.0",
|
||||
"c8": "^7.12.0",
|
||||
"babel-preset-gatsby": "^2.11.1",
|
||||
"c8": "^7.11.0",
|
||||
"caller": "^1.1.0",
|
||||
"chai": "^4.3.6",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
@@ -168,10 +168,9 @@
|
||||
"chai-string": "^1.4.0",
|
||||
"child-process-promise": "^2.2.1",
|
||||
"clipboard-copy": "^4.0.1",
|
||||
"concurrently": "^7.5.0",
|
||||
"cypress": "^10.11.0",
|
||||
"cypress-wait-for-stable-dom": "^0.1.0",
|
||||
"danger": "^11.1.4",
|
||||
"concurrently": "^7.1.0",
|
||||
"cypress": "^9.5.4",
|
||||
"danger": "^11.0.2",
|
||||
"danger-plugin-no-test-shortcuts": "^2.0.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"eslint": "^7.32.0",
|
||||
@@ -182,69 +181,69 @@
|
||||
"eslint-plugin-chai-friendly": "^0.7.2",
|
||||
"eslint-plugin-cypress": "^2.12.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsdoc": "^39.3.25",
|
||||
"eslint-plugin-mocha": "^10.1.0",
|
||||
"eslint-plugin-jsdoc": "^39.2.7",
|
||||
"eslint-plugin-mocha": "^10.0.3",
|
||||
"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.10",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-sort-class-members": "^1.15.2",
|
||||
"eslint-plugin-react": "^7.29.4",
|
||||
"eslint-plugin-react-hooks": "^4.4.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.19.0",
|
||||
"gatsby-plugin-page-creator": "^4.24.0",
|
||||
"gatsby-plugin-react-helmet": "^5.22.0",
|
||||
"gatsby": "4.6.2",
|
||||
"gatsby-plugin-catch-links": "^4.11.0",
|
||||
"gatsby-plugin-page-creator": "^4.7.0",
|
||||
"gatsby-plugin-react-helmet": "^5.10.0",
|
||||
"gatsby-plugin-remove-trailing-slashes": "^4.9.0",
|
||||
"gatsby-plugin-styled-components": "^5.24.0",
|
||||
"gatsby-plugin-typescript": "^4.22.0",
|
||||
"gatsby-plugin-styled-components": "^5.11.0",
|
||||
"gatsby-plugin-typescript": "^4.11.1",
|
||||
"humanize-string": "^2.1.0",
|
||||
"icedfrisby": "4.0.0",
|
||||
"icedfrisby-nock": "^2.1.0",
|
||||
"is-svg": "^4.3.2",
|
||||
"js-yaml-loader": "^1.2.2",
|
||||
"jsdoc": "^3.6.11",
|
||||
"lint-staged": "^13.0.3",
|
||||
"jsdoc": "^3.6.10",
|
||||
"lint-staged": "^12.3.8",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.difference": "^4.5.0",
|
||||
"minimist": "^1.2.7",
|
||||
"mocha": "^10.1.0",
|
||||
"minimist": "^1.2.6",
|
||||
"mocha": "^9.2.2",
|
||||
"mocha-env-reporter": "^4.0.0",
|
||||
"mocha-junit-reporter": "^2.1.1",
|
||||
"mocha-junit-reporter": "^2.0.2",
|
||||
"mocha-yaml-loader": "^1.0.3",
|
||||
"nock": "13.2.9",
|
||||
"nock": "13.2.4",
|
||||
"node-mocks-http": "^1.11.0",
|
||||
"nodemon": "^2.0.20",
|
||||
"nodemon": "^2.0.15",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"open-cli": "^7.1.0",
|
||||
"portfinder": "^1.0.32",
|
||||
"prettier": "2.7.1",
|
||||
"open-cli": "^7.0.1",
|
||||
"portfinder": "^1.0.28",
|
||||
"prettier": "2.6.2",
|
||||
"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.14.4",
|
||||
"react-pose": "^4.0.10",
|
||||
"react-select": "^4.3.1",
|
||||
"read-all-stdin-sync": "^1.0.5",
|
||||
"redis-server": "^1.2.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"sazerac": "^2.0.0",
|
||||
"simple-git-hooks": "^2.8.1",
|
||||
"sinon": "^14.0.1",
|
||||
"simple-git-hooks": "^2.7.0",
|
||||
"sinon": "^13.0.2",
|
||||
"sinon-chai": "^3.7.0",
|
||||
"snap-shot-it": "^7.9.6",
|
||||
"start-server-and-test": "1.14.0",
|
||||
"styled-components": "^5.3.6",
|
||||
"ts-mocha": "^10.0.0",
|
||||
"tsd": "^0.24.1",
|
||||
"typescript": "^4.8.4",
|
||||
"styled-components": "^5.3.5",
|
||||
"ts-mocha": "^9.0.2",
|
||||
"tsd": "^0.20.0",
|
||||
"typescript": "^4.6.3",
|
||||
"url": "^0.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.13.0",
|
||||
"npm": ">=8.0.0"
|
||||
"npm": ">=7.0.0"
|
||||
},
|
||||
"type": "module",
|
||||
"collective": {
|
||||
|
||||
@@ -29,7 +29,7 @@ async function captureTimings(warmupIterations) {
|
||||
function logResults({ times, iterations, warmupIterations }) {
|
||||
if (isNaN(iterations)) {
|
||||
console.log(
|
||||
'No timings captured. Have you included console.time statements in the badge creation code path?'
|
||||
`No timings captured. Have you included console.time statements in the badge creation code path?`
|
||||
)
|
||||
} else {
|
||||
const timedIterations = iterations - warmupIterations
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import fs from 'fs'
|
||||
|
||||
let data
|
||||
let title
|
||||
|
||||
try {
|
||||
if (process.argv.length < 4) {
|
||||
throw new Error()
|
||||
}
|
||||
title = process.argv[2]
|
||||
data = JSON.parse(fs.readFileSync(process.argv[3]))
|
||||
} catch (e) {
|
||||
process.stdout.write('failed to write summary\n')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
process.stdout.write(`# ${title}\n\n`)
|
||||
|
||||
if (data.stats.passes > 0) {
|
||||
process.stdout.write(`✔ ${data.stats.passes} passed\n`)
|
||||
}
|
||||
if (data.stats.failures > 0) {
|
||||
process.stdout.write(`✖ ${data.stats.failures} failed\n\n`)
|
||||
}
|
||||
|
||||
if (data.stats.failures > 0) {
|
||||
for (const test of data.tests) {
|
||||
if (test.err && Object.keys(test.err).length > 0) {
|
||||
process.stdout.write(`### ${test.title}\n\n`)
|
||||
process.stdout.write(`${test.fullTitle}\n\n`)
|
||||
process.stdout.write('```\n')
|
||||
process.stdout.write(`${test.err.stack}\n`)
|
||||
process.stdout.write('```\n\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
34
scripts/run_package_tests.sh
Executable file
34
scripts/run_package_tests.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
|
||||
# https://discuss.circleci.com/t/switch-nodejs-version-on-machine-executor-solved/26675/3
|
||||
|
||||
# Start off less strict to work around various nvm errors.
|
||||
set -e
|
||||
export NVM_DIR="/opt/circleci/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh"
|
||||
nvm install $NODE_VERSION
|
||||
nvm use $NODE_VERSION
|
||||
|
||||
# Stricter.
|
||||
set -euo pipefail
|
||||
node --version
|
||||
|
||||
# Install the shields.io dependencies.
|
||||
npm ci
|
||||
|
||||
# Run the package tests.
|
||||
npm run test:package
|
||||
npm run check-types:package
|
||||
|
||||
# Delete the full shields.io dependency tree
|
||||
rm -rf node_modules/
|
||||
|
||||
|
||||
# Run a smoke test (render a badge with the CLI) with only the package
|
||||
# dependencies installed.
|
||||
cd badge-maker
|
||||
|
||||
npm install # install only the package dependencies for this test
|
||||
npm link
|
||||
badge cactus grown :green @flat
|
||||
rm package-lock.json && rm -rf node_modules/ # clean up package dependencies
|
||||
@@ -108,7 +108,9 @@ class AurVotes extends BaseAurService {
|
||||
|
||||
class AurVersion extends BaseAurService {
|
||||
static category = 'version'
|
||||
|
||||
static route = { base: 'aur/version', pattern: ':packageName' }
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'AUR version',
|
||||
@@ -117,8 +119,6 @@ class AurVersion extends BaseAurService {
|
||||
},
|
||||
]
|
||||
|
||||
static _cacheLength = 3600
|
||||
|
||||
static render({ version, outOfDate }) {
|
||||
const color = outOfDate === null ? 'blue' : 'orange'
|
||||
return { message: addv(version), color }
|
||||
|
||||
@@ -8,7 +8,7 @@ import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('unknown build definition')
|
||||
.get('/swellaby/opensource/99999999.json')
|
||||
.get(`/swellaby/opensource/99999999.json`)
|
||||
.expectBadge({ label: 'tests', message: 'build pipeline not found' })
|
||||
|
||||
t.create('404 latest build error response')
|
||||
@@ -51,7 +51,7 @@ t.create('no test result summary response')
|
||||
})
|
||||
|
||||
t.create('no build response')
|
||||
.get('/swellaby/opensource/174.json')
|
||||
.get(`/swellaby/opensource/174.json`)
|
||||
.expectBadge({ label: 'tests', message: 'build pipeline not found' })
|
||||
|
||||
t.create('no tests in test result summary response')
|
||||
|
||||
@@ -27,7 +27,7 @@ function pullRequestClassGenerator(raw) {
|
||||
static category = 'issue-tracking'
|
||||
static route = {
|
||||
base: `bitbucket/${routePrefix}`,
|
||||
pattern: ':user/:repo',
|
||||
pattern: `:user/:repo`,
|
||||
queryParamSchema,
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ t.create('pr-raw (not found)')
|
||||
|
||||
t.create('pr-raw (private repo)')
|
||||
.get('/pr-raw/chris48s/example-private-repo.json')
|
||||
.expectBadge({ label: 'pull requests', message: 'not found' })
|
||||
.expectBadge({ label: 'pull requests', message: 'private repo' })
|
||||
|
||||
t.create('pr (valid)').get('/pr/atlassian/python-bitbucket.json').expectBadge({
|
||||
label: 'pull requests',
|
||||
@@ -33,7 +33,7 @@ t.create('pr (not found)')
|
||||
|
||||
t.create('pr (private repo)')
|
||||
.get('/pr/chris48s/example-private-repo.json')
|
||||
.expectBadge({ label: 'pull requests', message: 'not found' })
|
||||
.expectBadge({ label: 'pull requests', message: 'private repo' })
|
||||
|
||||
t.create('pr (server)')
|
||||
.get('/pr/project/repo.json?server=https://bitbucket.mydomain.net')
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import Joi from 'joi'
|
||||
import { nonNegativeInteger } from '../validators.js'
|
||||
import { metric } from '../text-formatters.js'
|
||||
import { BaseJsonService } from '../index.js'
|
||||
|
||||
const schema = Joi.object({ activity_total: Joi.number().required() })
|
||||
const schema = Joi.object({ activity_total: nonNegativeInteger })
|
||||
|
||||
export default class Bountysource extends BaseJsonService {
|
||||
static category = 'funding'
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import {
|
||||
isVPlusDottedVersionAtLeastOne,
|
||||
isVPlusDottedVersionNClausesWithOptionalSuffix,
|
||||
} from '../test-validators.js'
|
||||
import Joi from 'joi'
|
||||
import { isVPlusDottedVersionAtLeastOne } from '../test-validators.js'
|
||||
import { ServiceTester } from '../tester.js'
|
||||
export const t = new ServiceTester({
|
||||
id: 'BowerVersion',
|
||||
@@ -9,17 +7,21 @@ export const t = new ServiceTester({
|
||||
pathPrefix: '/bower',
|
||||
})
|
||||
|
||||
t.create('version').timeout(10000).get('/v/angular.json').expectBadge({
|
||||
const isBowerPrereleaseVersion = Joi.string().regex(
|
||||
/^v\d+(\.\d+)?(\.\d+)?(-?[.\w\d])+?$/
|
||||
)
|
||||
|
||||
t.create('version').timeout(10000).get('/v/bootstrap.json').expectBadge({
|
||||
label: 'bower',
|
||||
message: isVPlusDottedVersionAtLeastOne,
|
||||
})
|
||||
|
||||
t.create('pre version')
|
||||
t.create('pre version') // e.g. bower|v0.2.5-alpha-rc-pre
|
||||
.timeout(10000)
|
||||
.get('/v/angular.json?include_prereleases')
|
||||
.get('/v/bootstrap.json?include_prereleases')
|
||||
.expectBadge({
|
||||
label: 'bower',
|
||||
message: isVPlusDottedVersionNClausesWithOptionalSuffix,
|
||||
message: isBowerPrereleaseVersion,
|
||||
})
|
||||
|
||||
t.create('Version for Invalid Package')
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
/**
|
||||
* Common functions and schemas for tasks related to build status.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import Joi from 'joi'
|
||||
|
||||
const greenStatuses = [
|
||||
@@ -56,23 +50,8 @@ const allStatuses = greenStatuses
|
||||
.concat(redStatuses)
|
||||
.concat(otherStatuses)
|
||||
|
||||
/**
|
||||
* Joi schema for validating Build Status.
|
||||
* Checks if the build status is present in the list of allowed build status.
|
||||
*
|
||||
* @type {Joi}
|
||||
*/
|
||||
const isBuildStatus = Joi.equal(...allStatuses)
|
||||
|
||||
/**
|
||||
* Handles rendering concerns of badges that display build status.
|
||||
* Determines the message and color of the badge according to the build status.
|
||||
*
|
||||
* @param {object} attrs Refer to individual attributes
|
||||
* @param {string} [attrs.label] If provided then badge label is set to this value
|
||||
* @param {string} attrs.status Build status
|
||||
* @returns {object} Badge with label, message and color properties
|
||||
*/
|
||||
function renderBuildStatusBadge({ label, status }) {
|
||||
let message
|
||||
let color
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createServiceFamily } from '../nuget/nuget-v2-service-family.js'
|
||||
export default createServiceFamily({
|
||||
defaultLabel: 'chocolatey',
|
||||
serviceBaseUrl: 'chocolatey',
|
||||
apiBaseUrl: 'https://community.chocolatey.org/api/v2',
|
||||
apiBaseUrl: 'https://www.chocolatey.org/api/v2',
|
||||
odataFormat: 'json',
|
||||
title: 'Chocolatey',
|
||||
examplePackageName: 'git',
|
||||
|
||||
@@ -35,7 +35,7 @@ export default class CIIBestPracticesService extends BaseJsonService {
|
||||
pattern: ':metric(level|percentage|summary)/:projectId',
|
||||
}
|
||||
|
||||
static examples = [
|
||||
static exampless = [
|
||||
{
|
||||
title: 'CII Best Practices Level',
|
||||
pattern: 'level/:projectId',
|
||||
|
||||
@@ -3,32 +3,32 @@ import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('level known project')
|
||||
.get('/level/1.json')
|
||||
.get(`/level/1.json`)
|
||||
.expectBadge({
|
||||
label: 'cii',
|
||||
message: withRegex(/in progress|passing|silver|gold/),
|
||||
})
|
||||
|
||||
t.create('percentage known project')
|
||||
.get('/percentage/29.json')
|
||||
.get(`/percentage/29.json`)
|
||||
.expectBadge({
|
||||
label: 'cii',
|
||||
message: withRegex(/([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-9][0-9]|300)%/),
|
||||
})
|
||||
|
||||
t.create('summary known project')
|
||||
.get('/summary/33.json')
|
||||
.get(`/summary/33.json`)
|
||||
.expectBadge({
|
||||
label: 'cii',
|
||||
message: withRegex(/(in progress [0-9]|[1-9][0-9]%)|passing|silver|gold/),
|
||||
})
|
||||
|
||||
t.create('unknown project')
|
||||
.get('/level/abc.json')
|
||||
.get(`/level/abc.json`)
|
||||
.expectBadge({ label: 'cii', message: 'project not found' })
|
||||
|
||||
t.create('level: gold project')
|
||||
.get('/level/1.json')
|
||||
.get(`/level/1.json`)
|
||||
.intercept(nock =>
|
||||
nock('https://bestpractices.coreinfrastructure.org/projects')
|
||||
.get('/1/badge.json')
|
||||
@@ -43,7 +43,7 @@ t.create('level: gold project')
|
||||
})
|
||||
|
||||
t.create('level: silver project')
|
||||
.get('/level/34.json')
|
||||
.get(`/level/34.json`)
|
||||
.intercept(nock =>
|
||||
nock('https://bestpractices.coreinfrastructure.org/projects')
|
||||
.get('/34/badge.json')
|
||||
@@ -58,7 +58,7 @@ t.create('level: silver project')
|
||||
})
|
||||
|
||||
t.create('level: passing project')
|
||||
.get('/level/29.json')
|
||||
.get(`/level/29.json`)
|
||||
.intercept(nock =>
|
||||
nock('https://bestpractices.coreinfrastructure.org/projects')
|
||||
.get('/29/badge.json')
|
||||
@@ -73,7 +73,7 @@ t.create('level: passing project')
|
||||
})
|
||||
|
||||
t.create('level: in progress project')
|
||||
.get('/level/33.json')
|
||||
.get(`/level/33.json`)
|
||||
.intercept(nock =>
|
||||
nock('https://bestpractices.coreinfrastructure.org/projects')
|
||||
.get('/33/badge.json')
|
||||
@@ -88,7 +88,7 @@ t.create('level: in progress project')
|
||||
})
|
||||
|
||||
t.create('percentage: gold project')
|
||||
.get('/percentage/1.json')
|
||||
.get(`/percentage/1.json`)
|
||||
.intercept(nock =>
|
||||
nock('https://bestpractices.coreinfrastructure.org/projects')
|
||||
.get('/1/badge.json')
|
||||
@@ -103,7 +103,7 @@ t.create('percentage: gold project')
|
||||
})
|
||||
|
||||
t.create('percentage: silver project')
|
||||
.get('/percentage/34.json')
|
||||
.get(`/percentage/34.json`)
|
||||
.intercept(nock =>
|
||||
nock('https://bestpractices.coreinfrastructure.org/projects')
|
||||
.get('/34/badge.json')
|
||||
@@ -118,7 +118,7 @@ t.create('percentage: silver project')
|
||||
})
|
||||
|
||||
t.create('percentage: passing project')
|
||||
.get('/percentage/29.json')
|
||||
.get(`/percentage/29.json`)
|
||||
.intercept(nock =>
|
||||
nock('https://bestpractices.coreinfrastructure.org/projects')
|
||||
.get('/29/badge.json')
|
||||
@@ -133,7 +133,7 @@ t.create('percentage: passing project')
|
||||
})
|
||||
|
||||
t.create('percentage: in progress project')
|
||||
.get('/percentage/33.json')
|
||||
.get(`/percentage/33.json`)
|
||||
.intercept(nock =>
|
||||
nock('https://bestpractices.coreinfrastructure.org/projects')
|
||||
.get('/33/badge.json')
|
||||
@@ -148,7 +148,7 @@ t.create('percentage: in progress project')
|
||||
})
|
||||
|
||||
t.create('summary: gold project')
|
||||
.get('/summary/1.json')
|
||||
.get(`/summary/1.json`)
|
||||
.intercept(nock =>
|
||||
nock('https://bestpractices.coreinfrastructure.org/projects')
|
||||
.get('/1/badge.json')
|
||||
@@ -163,7 +163,7 @@ t.create('summary: gold project')
|
||||
})
|
||||
|
||||
t.create('summary: silver project')
|
||||
.get('/summary/34.json')
|
||||
.get(`/summary/34.json`)
|
||||
.intercept(nock =>
|
||||
nock('https://bestpractices.coreinfrastructure.org/projects')
|
||||
.get('/34/badge.json')
|
||||
@@ -178,7 +178,7 @@ t.create('summary: silver project')
|
||||
})
|
||||
|
||||
t.create('summary: passing project')
|
||||
.get('/summary/29.json')
|
||||
.get(`/summary/29.json`)
|
||||
.intercept(nock =>
|
||||
nock('https://bestpractices.coreinfrastructure.org/projects')
|
||||
.get('/29/badge.json')
|
||||
@@ -193,7 +193,7 @@ t.create('summary: passing project')
|
||||
})
|
||||
|
||||
t.create('summary: in progress project')
|
||||
.get('/summary/33.json')
|
||||
.get(`/summary/33.json`)
|
||||
.intercept(nock =>
|
||||
nock('https://bestpractices.coreinfrastructure.org/projects')
|
||||
.get('/33/badge.json')
|
||||
|
||||
@@ -107,7 +107,7 @@ export default class Codecov extends BaseSvgScrapingService {
|
||||
async legacyFetch({ vcsName, user, repo, branch, token }) {
|
||||
// Codecov Docs: https://docs.codecov.io/reference#section-get-a-single-repository
|
||||
const url = `https://codecov.io/api/${vcsName}/${user}/${repo}${
|
||||
branch ? `/branch/${branch}` : ''
|
||||
branch ? `/branches/${branch}` : ''
|
||||
}`
|
||||
const { buffer } = await this._request({
|
||||
url,
|
||||
|
||||
@@ -60,7 +60,7 @@ t.create('handles unauthorized private repository')
|
||||
.intercept(nock =>
|
||||
nock('https://codecov.io')
|
||||
.get('/github/codecov/private-example-python/graph/badge.svg')
|
||||
.reply(200, '<g><text x="105.5" y="14">unknown</text></g>', {
|
||||
.reply(200, `<g><text x="105.5" y="14">unknown</text></g>`, {
|
||||
'Content-Type': 'image/svg+xml',
|
||||
})
|
||||
)
|
||||
@@ -110,7 +110,7 @@ t.create('gets coverage for private repository')
|
||||
.get(
|
||||
'/gh/codecov/private-example-python/graph/badge.svg?token=a1b2c3d4e5'
|
||||
)
|
||||
.reply(200, '<g><text x="105.5" y="14">100%</text></g>', {
|
||||
.reply(200, `<g><text x="105.5" y="14">100%</text></g>`, {
|
||||
'Content-Type': 'image/svg+xml',
|
||||
})
|
||||
)
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
/**
|
||||
* Commonly-used functions for determining the colour to use for a badge,
|
||||
* including colours based off download count, version number, etc.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import dayjs from 'dayjs'
|
||||
import moment from 'moment'
|
||||
import pep440 from '@renovate/pep440'
|
||||
|
||||
/**
|
||||
* Determines the color used for a badge based on version.
|
||||
*
|
||||
* @param {string|number} version Version used for determining badge color
|
||||
* @returns {string} Badge color
|
||||
*/
|
||||
function version(version) {
|
||||
if (typeof version !== 'string' && typeof version !== 'number') {
|
||||
throw new Error(`Can't generate a version color for ${version}`)
|
||||
@@ -23,19 +14,13 @@ function version(version) {
|
||||
if (first === 'v') {
|
||||
first = version[1]
|
||||
}
|
||||
if (first === '0' || /alpha|beta|snapshot|dev|pre|rc/i.test(version)) {
|
||||
if (first === '0' || /alpha|beta|snapshot|dev|pre/i.test(version)) {
|
||||
return 'orange'
|
||||
} else {
|
||||
return 'blue'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the color used for a badge based on PEP440 versioning.
|
||||
*
|
||||
* @param {string|number} version Version used for determining badge color
|
||||
* @returns {string} Badge color
|
||||
*/
|
||||
function pep440VersionColor(version) {
|
||||
if (!pep440.valid(version)) {
|
||||
return 'lightgrey'
|
||||
@@ -47,18 +32,6 @@ function pep440VersionColor(version) {
|
||||
return 'blue'
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the color used for a badge by comparing the value and floor count values.
|
||||
* The color can vary from red to bright green depending on the range the value lies in.
|
||||
* Decreasing the value will shift the color towards red.
|
||||
* Increasing the value will shift the color towards bright green.
|
||||
*
|
||||
* @param {number} value Current value
|
||||
* @param {number} yellow Yellow color threshold, should be greater than 0
|
||||
* @param {number} yellowgreen Yellowgreen color threshold, should be greater than yellow
|
||||
* @param {number} green Green color threshold, should be greater than yellowgreen
|
||||
* @returns {string} Badge color
|
||||
*/
|
||||
function floorCount(value, yellow, yellowgreen, green) {
|
||||
if (value <= 0) {
|
||||
return 'red'
|
||||
@@ -73,37 +46,14 @@ function floorCount(value, yellow, yellowgreen, green) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the color used for a badge by comparing the download count and floor values.
|
||||
* The color varies from red to bright green as the download count increases.
|
||||
*
|
||||
* @param {number} downloads Download count
|
||||
* @returns {string} Badge color
|
||||
*/
|
||||
function downloadCount(downloads) {
|
||||
return floorCount(downloads, 10, 100, 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the color used for a badge by comparing percentage and floor values.
|
||||
* The color varies from red to bright green as the percentage increases.
|
||||
*
|
||||
* @param {number} percentage Percentage value
|
||||
* @returns {string} Badge color
|
||||
*/
|
||||
function coveragePercentage(percentage) {
|
||||
return floorCount(percentage, 80, 90, 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the color used for a badge by matching score with grade values.
|
||||
* The color varies from bright green to red as the score decreases.
|
||||
* The score can be one of the following grade value: ['A', 'B', 'C', 'D', 'E'].
|
||||
* The color defaults to red if the score does not matches with any of the grade values.
|
||||
*
|
||||
* @param {string} score Score value
|
||||
* @returns {string} Badge color
|
||||
*/
|
||||
function letterScore(score) {
|
||||
if (score === 'A') {
|
||||
return 'brightgreen'
|
||||
@@ -120,18 +70,6 @@ function letterScore(score) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a callback function that determines badge color from the colors array.
|
||||
* If the colors array is provided then for n steps, there should be n + 1 color.
|
||||
* If the colors array is not provided then it is chosen from the default colors array
|
||||
* according to the size of the steps array.
|
||||
*
|
||||
* @param {number[]} steps Steps array
|
||||
* @param {string[]} colors Colors array. If provided, should be of length steps.length + 1
|
||||
* @param {boolean} reversed If true then the colors array will be considered in reverse order
|
||||
* @returns {function(number): string} Function that finds the step index by comparing value
|
||||
* with steps array and returns color from colors array for the corresponding step index
|
||||
*/
|
||||
function colorScale(steps, colors, reversed) {
|
||||
if (steps === undefined) {
|
||||
throw Error('When invoking colorScale, steps should be provided.')
|
||||
@@ -172,17 +110,9 @@ function colorScale(steps, colors, reversed) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the color used for a badge according to the age.
|
||||
* Age is calculated as days elapsed till current date.
|
||||
* The color varies from bright green to red as the age increases.
|
||||
*
|
||||
* @param {string} date Date string
|
||||
* @returns {string} Badge color
|
||||
*/
|
||||
function age(date) {
|
||||
const colorByAge = colorScale([7, 30, 180, 365, 730], undefined, true)
|
||||
const daysElapsed = dayjs().diff(dayjs(date), 'days')
|
||||
const daysElapsed = moment().diff(moment(date), 'days')
|
||||
return colorByAge(daysElapsed)
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,6 @@ describe('Color formatters', function () {
|
||||
given('6.0-SNAPSHOT'),
|
||||
given('1.0.1-dev'),
|
||||
given('2.1.6-prerelease'),
|
||||
given('2.1.6-RC1'),
|
||||
]).expect('orange')
|
||||
|
||||
expect(() => version(null)).to.throw(
|
||||
|
||||
@@ -31,7 +31,7 @@ export default class CondaVersion extends BaseCondaService {
|
||||
|
||||
static render({ variant, channel, version }) {
|
||||
return {
|
||||
label: variant === 'vn' ? channel : `conda | ${channel}`,
|
||||
label: variant === 'vn' ? channel : `conda|${channel}`,
|
||||
message: versionText(version),
|
||||
color: versionColor(version),
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('version').get('/v/conda-forge/zlib.json').expectBadge({
|
||||
label: 'conda | conda-forge',
|
||||
label: 'conda|conda-forge',
|
||||
message: isVPlusTripleDottedVersion,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,18 +1,5 @@
|
||||
/**
|
||||
* Common functions and utilities for tasks related to contributor count.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { metric } from './text-formatters.js'
|
||||
|
||||
/**
|
||||
* Determines the color used for a badge based on the contributor count.
|
||||
* The color varies from red to bright green as the contributor count increases.
|
||||
*
|
||||
* @param {number} contributorCount Contributor count
|
||||
* @returns {string} Badge color
|
||||
*/
|
||||
function contributorColor(contributorCount) {
|
||||
if (contributorCount > 2) {
|
||||
return 'brightgreen'
|
||||
@@ -23,15 +10,6 @@ function contributorColor(contributorCount) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles rendering concerns of badges that display contributor count.
|
||||
* Determines the message and color of the badge according to the contributor count.
|
||||
*
|
||||
* @param {object} attrs Refer to individual attributes
|
||||
* @param {string} [attrs.label] If provided then badge label is set to this value
|
||||
* @param {number} attrs.contributorCount Contributor count
|
||||
* @returns {object} Badge with label, message and color properties
|
||||
*/
|
||||
function renderContributorBadge({ label, contributorCount }) {
|
||||
return {
|
||||
label,
|
||||
|
||||
@@ -1,11 +1,66 @@
|
||||
import { deprecatedService } from '../index.js'
|
||||
import Joi from 'joi'
|
||||
import { BaseJsonService } from '../index.js'
|
||||
import {
|
||||
IMPROVED_STATUS,
|
||||
NOT_FOUND_STATUS,
|
||||
REGRESSED_STATUS,
|
||||
NO_CHANGE_STATUS,
|
||||
} from './constants.js'
|
||||
|
||||
export default deprecatedService({
|
||||
category: 'analysis',
|
||||
route: {
|
||||
base: 'criterion',
|
||||
pattern: ':various*',
|
||||
},
|
||||
label: 'criterion',
|
||||
dateAdded: new Date('2022-10-07'),
|
||||
})
|
||||
const schema = Joi.string()
|
||||
.allow(IMPROVED_STATUS, REGRESSED_STATUS, NO_CHANGE_STATUS)
|
||||
.required()
|
||||
|
||||
/**
|
||||
* Criterion Badge Service
|
||||
*
|
||||
* Support and Contact:
|
||||
* - https://github.com/chmoder/api.criterion.dev
|
||||
*
|
||||
* API Documentation:
|
||||
* - https://app.swaggerhub.com/apis-docs/chmoder/Criterion.dev
|
||||
*/
|
||||
export default class Criterion extends BaseJsonService {
|
||||
static category = 'analysis'
|
||||
static route = { base: 'criterion', pattern: ':user/:repo' }
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'Criterion',
|
||||
namedParams: {
|
||||
user: 'chmoder',
|
||||
repo: 'data_vault',
|
||||
},
|
||||
staticPreview: this.render({ status: IMPROVED_STATUS }),
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = { label: 'criterion' }
|
||||
|
||||
static render({ status }) {
|
||||
let statusColor = 'lightgrey'
|
||||
|
||||
if (status === IMPROVED_STATUS) {
|
||||
statusColor = 'brightgreen'
|
||||
} else if (status === NO_CHANGE_STATUS) {
|
||||
statusColor = 'green'
|
||||
} else if (statusColor === REGRESSED_STATUS) {
|
||||
statusColor = 'red'
|
||||
}
|
||||
|
||||
return {
|
||||
message: `${status}`,
|
||||
color: statusColor,
|
||||
}
|
||||
}
|
||||
|
||||
async handle({ user, repo }) {
|
||||
const status = await this._requestJson({
|
||||
url: `https://api.criterion.dev/v1/${user}/${repo}/status`,
|
||||
errorMessages: { 404: NOT_FOUND_STATUS },
|
||||
schema,
|
||||
})
|
||||
|
||||
return this.constructor.render({ status })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
import { ServiceTester } from '../tester.js'
|
||||
import Joi from 'joi'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
import {
|
||||
IMPROVED_STATUS,
|
||||
REGRESSED_STATUS,
|
||||
NO_CHANGE_STATUS,
|
||||
NOT_FOUND_STATUS,
|
||||
} from './constants.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
export const t = new ServiceTester({
|
||||
id: 'criterion',
|
||||
title: 'Criterion',
|
||||
pathPrefix: '/criterion',
|
||||
})
|
||||
const isStatus = Joi.string()
|
||||
.allow(IMPROVED_STATUS, REGRESSED_STATUS, NOT_FOUND_STATUS, NO_CHANGE_STATUS)
|
||||
.required()
|
||||
|
||||
t.create('Criterion')
|
||||
t.create('Criterion (valid repo)')
|
||||
.get('/chmoder/credit_card.json')
|
||||
.expectBadge({ label: 'criterion', message: 'no longer available' })
|
||||
.expectBadge({ label: 'criterion', message: isStatus })
|
||||
|
||||
t.create('Criterion (not found)')
|
||||
.get('/chmoder/not-a-repo.json')
|
||||
.expectBadge({ label: 'criterion', message: NOT_FOUND_STATUS })
|
||||
|
||||
9
services/deprecation-helpers.js
Normal file
9
services/deprecation-helpers.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Deprecated } from './index.js'
|
||||
|
||||
function enforceDeprecation(effectiveDate) {
|
||||
if (Date.now() >= effectiveDate.getTime()) {
|
||||
throw new Deprecated()
|
||||
}
|
||||
}
|
||||
|
||||
export { enforceDeprecation }
|
||||
15
services/deprecation-helpers.spec.js
Normal file
15
services/deprecation-helpers.spec.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { expect } from 'chai'
|
||||
import { Deprecated } from '../core/base-service/errors.js'
|
||||
import { enforceDeprecation } from './deprecation-helpers.js'
|
||||
|
||||
describe('enforceDeprecation', function () {
|
||||
it('throws Deprecated for a date in the past', function () {
|
||||
expect(() => enforceDeprecation(new Date())).to.throw(Deprecated)
|
||||
})
|
||||
|
||||
it('does not throw for a date in the future', function () {
|
||||
expect(() =>
|
||||
enforceDeprecation(new Date(Date.now() + 10000))
|
||||
).not.to.throw()
|
||||
})
|
||||
})
|
||||
@@ -14,12 +14,12 @@ describe('Discord', function () {
|
||||
},
|
||||
}
|
||||
|
||||
const scope = nock('https://discord.com', {
|
||||
const scope = nock(`https://discord.com`, {
|
||||
// This ensures that the expected credential is actually being sent with the HTTP request.
|
||||
// Without this the request wouldn't match and the test would fail.
|
||||
reqheaders: { Authorization: 'Bot password' },
|
||||
reqheaders: { Authorization: `Bot password` },
|
||||
})
|
||||
.get('/api/v6/guilds/12345/widget.json')
|
||||
.get(`/api/v6/guilds/12345/widget.json`)
|
||||
.reply(200, {
|
||||
presence_count: 125,
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user