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 |
385
.circleci/config.yml
Normal file
385
.circleci/config.yml
Normal file
@@ -0,0 +1,385 @@
|
||||
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
|
||||
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: |
|
||||
npm install --dry-run
|
||||
npm ci
|
||||
environment:
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
|
||||
- run:
|
||||
name: Identify services tagged in the PR title
|
||||
command: npm run test:services:pr:prepare
|
||||
|
||||
- run:
|
||||
name: Run tests for tagged services
|
||||
environment:
|
||||
mocha_reporter: mocha-junit-reporter
|
||||
MOCHA_FILE: junit/services/results.xml
|
||||
command: RETRY_COUNT=3 npm run test:services:pr:run
|
||||
|
||||
- store_test_results:
|
||||
path: junit
|
||||
|
||||
package_steps: &package_steps
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- run:
|
||||
name: Install node and npm
|
||||
command: |
|
||||
set +e
|
||||
export NVM_DIR="/opt/circleci/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||
nvm install v14
|
||||
nvm use v14
|
||||
npm install -g npm
|
||||
|
||||
# Run the package tests on each currently supported node version. See:
|
||||
# https://github.com/badges/shields/blob/master/badge-maker/README.md#node-version-support
|
||||
# https://nodejs.org/en/about/releases/
|
||||
|
||||
- run:
|
||||
environment:
|
||||
mocha_reporter: mocha-junit-reporter
|
||||
MOCHA_FILE: junit/badge-maker/v12/results.xml
|
||||
NODE_VERSION: v12
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
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.14
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: npm ci
|
||||
environment:
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
|
||||
- run:
|
||||
name: Danger
|
||||
when: always
|
||||
environment:
|
||||
# https://github.com/gatsbyjs/gatsby/pull/11555
|
||||
NODE_ENV: test
|
||||
command: npm run danger ci
|
||||
|
||||
frontend:
|
||||
docker:
|
||||
- image: cimg/node:16.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
|
||||
|
||||
- run:
|
||||
name: Prepare frontend tests
|
||||
command: npm run defs && npm run features
|
||||
|
||||
- run:
|
||||
name: Check types
|
||||
command: npm run check-types:frontend
|
||||
|
||||
- run:
|
||||
name: Frontend unit tests
|
||||
environment:
|
||||
mocha_reporter: mocha-junit-reporter
|
||||
MOCHA_FILE: junit/frontend/results.xml
|
||||
when: always
|
||||
command: npm run test:frontend
|
||||
|
||||
- store_test_results:
|
||||
path: junit
|
||||
|
||||
- run:
|
||||
name: Frontend build completes successfully
|
||||
when: always
|
||||
command: npm run build
|
||||
|
||||
package:
|
||||
machine:
|
||||
image: 'ubuntu-2004:202111-02'
|
||||
|
||||
<<: *package_steps
|
||||
|
||||
services:
|
||||
docker:
|
||||
- image: cimg/node:16.14
|
||||
environment:
|
||||
NPM_CONFIG_ENGINE_STRICT: 'true'
|
||||
NPM_CONFIG_STRICT_PEER_DEPS: 'true'
|
||||
|
||||
<<: *services_steps
|
||||
|
||||
services@node-17:
|
||||
docker:
|
||||
- image: cimg/node:17.7
|
||||
|
||||
<<: *services_steps
|
||||
|
||||
e2e:
|
||||
docker:
|
||||
- image: cypress/base:16.13.0
|
||||
environment:
|
||||
NPM_CONFIG_ENGINE_STRICT: 'true'
|
||||
NPM_CONFIG_STRICT_PEER_DEPS: 'true'
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- restore_cache:
|
||||
name: Restore Cypress binary
|
||||
keys:
|
||||
- v2-cypress-dependencies-{{ checksum "package-lock.json" }}
|
||||
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: |
|
||||
npm install --dry-run
|
||||
npm ci
|
||||
|
||||
- run:
|
||||
name: Frontend build
|
||||
command: GATSBY_BASE_URL=http://localhost:8080 npm run build
|
||||
|
||||
- run:
|
||||
name: Run tests
|
||||
environment:
|
||||
CYPRESS_REPORTER: junit
|
||||
MOCHA_FILE: junit/e2e/results.xml
|
||||
command: npm run e2e-on-build
|
||||
|
||||
- store_test_results:
|
||||
path: junit
|
||||
|
||||
- store_artifacts:
|
||||
path: cypress/videos
|
||||
|
||||
- store_artifacts:
|
||||
path: cypress/screenshots
|
||||
|
||||
- save_cache:
|
||||
name: Cache Cypress binary
|
||||
paths:
|
||||
# https://docs.cypress.io/guides/getting-started/installing-cypress.html#Binary-cache
|
||||
- ~/.cache/Cypress
|
||||
key: v2-cypress-dependencies-{{ checksum "package-lock.json" }}
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
|
||||
on-commit:
|
||||
jobs:
|
||||
- main:
|
||||
filters:
|
||||
branches:
|
||||
ignore: gh-pages
|
||||
- main@node-17:
|
||||
filters:
|
||||
branches:
|
||||
ignore: gh-pages
|
||||
- integration@node-17:
|
||||
filters:
|
||||
branches:
|
||||
ignore: gh-pages
|
||||
- frontend:
|
||||
filters:
|
||||
branches:
|
||||
ignore: gh-pages
|
||||
- package:
|
||||
filters:
|
||||
branches:
|
||||
ignore: gh-pages
|
||||
- services:
|
||||
filters:
|
||||
branches:
|
||||
ignore:
|
||||
- master
|
||||
- gh-pages
|
||||
- services@node-17:
|
||||
filters:
|
||||
branches:
|
||||
ignore:
|
||||
- master
|
||||
- gh-pages
|
||||
- danger:
|
||||
filters:
|
||||
branches:
|
||||
ignore:
|
||||
- master
|
||||
- gh-pages
|
||||
- /dependabot\/.*/
|
||||
- e2e:
|
||||
filters:
|
||||
branches:
|
||||
ignore: gh-pages
|
||||
# on-commit-with-cache:
|
||||
# jobs:
|
||||
# - npm-install:
|
||||
# filters:
|
||||
# branches:
|
||||
# ignore: gh-pages
|
||||
# - main:
|
||||
# requires:
|
||||
# - npm-install
|
||||
# - main@node-latest:
|
||||
# requires:
|
||||
# - npm-install
|
||||
# - frontend:
|
||||
# requires:
|
||||
# - npm-install
|
||||
# - services:
|
||||
# requires:
|
||||
# - npm-install
|
||||
# filters:
|
||||
# branches:
|
||||
# ignore: master
|
||||
# - services@node-latest:
|
||||
# requires:
|
||||
# - npm-install
|
||||
# filters:
|
||||
# branches:
|
||||
# ignore: master
|
||||
# - danger:
|
||||
# requires:
|
||||
# - npm-install
|
||||
# filters:
|
||||
# branches:
|
||||
# ignore: /dependabot\/.*/
|
||||
@@ -24,7 +24,6 @@ plugins:
|
||||
- chai-friendly
|
||||
- jsdoc
|
||||
- mocha
|
||||
- icedfrisby
|
||||
- no-extension-in-require
|
||||
- sort-class-members
|
||||
- import
|
||||
@@ -114,16 +113,9 @@ overrides:
|
||||
mocha: true
|
||||
rules:
|
||||
mocha/no-exclusive-tests: 'error'
|
||||
mocha/no-skipped-tests: 'error'
|
||||
mocha/no-mocha-arrows: 'error'
|
||||
mocha/prefer-arrow-callback: 'error'
|
||||
|
||||
- files:
|
||||
- 'services/**/*.tester.js'
|
||||
rules:
|
||||
icedfrisby/no-exclusive-tests: 'error'
|
||||
icedfrisby/no-skipped-tests: 'error'
|
||||
|
||||
rules:
|
||||
# Disable some rules from eslint:recommended.
|
||||
no-empty: ['error', { 'allowEmptyCatch': true }]
|
||||
@@ -152,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
|
||||
|
||||
37
.github/ISSUE_TEMPLATE/3_Badge_request.md
vendored
Normal file
37
.github/ISSUE_TEMPLATE/3_Badge_request.md
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: 💡 Badge Request
|
||||
about: Ideas for new badges
|
||||
labels: 'service-badge'
|
||||
---
|
||||
|
||||
:clipboard: **Description**
|
||||
|
||||
<!--
|
||||
A clear and concise description of the new badge.
|
||||
|
||||
- Which service is this badge for e.g: GitHub, Travis CI
|
||||
- What sort of information should this badge show?
|
||||
Provide an example in plain text e.g: "version | v1.01" or as a static badge
|
||||
(static badge generator can be found at https://shields.io)
|
||||
-->
|
||||
|
||||
:link: **Data**
|
||||
|
||||
<!--
|
||||
Where can we get the data from?
|
||||
|
||||
- Is there a public API?
|
||||
- Does the API requires an API key?
|
||||
- Link to the API documentation.
|
||||
-->
|
||||
|
||||
:microphone: **Motivation**
|
||||
|
||||
<!--
|
||||
Please explain why this feature should be implemented and how it would be used.
|
||||
|
||||
- What is the specific use case?
|
||||
-->
|
||||
|
||||
<!-- Love Shields? Please consider donating $10 to sustain our activities:
|
||||
👉 https://opencollective.com/shields -->
|
||||
62
.github/ISSUE_TEMPLATE/3_Badge_request.yml
vendored
62
.github/ISSUE_TEMPLATE/3_Badge_request.yml
vendored
@@ -1,62 +0,0 @@
|
||||
name: '💡 Badge Request'
|
||||
description: Ideas for new badges
|
||||
labels: ['service-badge']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
## Ideas for new badges
|
||||
|
||||
|
||||
This issue template is for suggesting new badges which
|
||||
**fetch and display data from an upstream service**.
|
||||
If your suggestion is for a static badge
|
||||
(which shows the same information every time it is requested), it is
|
||||
[already possible to make these](https://github.com/badges/shields/blob/master/doc/static-badges.md).
|
||||
We don't add specific routes for badges which only show static information.
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: '📋 Description'
|
||||
description: |
|
||||
A clear and concise description of the new badge.
|
||||
|
||||
- Which service is this badge for e.g: GitHub, Travis CI
|
||||
- What sort of information should this badge show?
|
||||
Provide an example in plain text e.g: "version | v1.01" or as a static badge
|
||||
(static badge generator can be found at https://shields.io/#your-badge )
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: data
|
||||
attributes:
|
||||
label: '🔗 Data'
|
||||
description: |
|
||||
Where can we get the data from?
|
||||
|
||||
Please consider and cover details like:
|
||||
- Is there a public API?
|
||||
- Does the API require authentication or an API key?
|
||||
If so, please review our documentation on [Badges Requiring Authentication](https://github.com/badges/shields/blob/master/doc/authentication.md)
|
||||
- Link to the API documentation.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: motivation
|
||||
attributes:
|
||||
label: '🎤 Motivation'
|
||||
description: |
|
||||
Please explain why this feature should be implemented and how it would be used.
|
||||
|
||||
- What is the specific use case?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## :heart: Love Shields?
|
||||
Please consider donating $10 to sustain our activities: [https://opencollective.com/shields](https://opencollective.com/shields)
|
||||
2
.github/actions/close-bot/action.yml
vendored
2
.github/actions/close-bot/action.yml
vendored
@@ -8,5 +8,5 @@ inputs:
|
||||
description: 'The GITHUB_TOKEN secret'
|
||||
required: true
|
||||
runs:
|
||||
using: 'node16'
|
||||
using: 'node12'
|
||||
main: 'index.js'
|
||||
|
||||
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
|
||||
31
.github/actions/frontend-tests/action.yml
vendored
31
.github/actions/frontend-tests/action.yml
vendored
@@ -1,31 +0,0 @@
|
||||
name: 'Frontend tests'
|
||||
description: 'Run frontend tests and check types'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Prepare frontend tests
|
||||
if: always()
|
||||
run: npm run defs && npm run features
|
||||
shell: bash
|
||||
|
||||
- name: Tests
|
||||
if: always()
|
||||
run: npm run test:frontend -- --reporter json --reporter-option 'output=reports/frontend-tests.json'
|
||||
shell: bash
|
||||
|
||||
- name: Type Checks
|
||||
if: always()
|
||||
run: |
|
||||
set -o pipefail
|
||||
npm run check-types:frontend 2>&1 | tee reports/frontend-types.txt
|
||||
shell: bash
|
||||
|
||||
- name: Write Markdown Summary
|
||||
if: always()
|
||||
run: |
|
||||
node scripts/mocha2md.js 'Frontend Tests' reports/frontend-tests.json >> $GITHUB_STEP_SUMMARY
|
||||
echo '# Frontend Types' >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
cat reports/frontend-types.txt >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
shell: bash
|
||||
28
.github/actions/integration-tests/action.yml
vendored
28
.github/actions/integration-tests/action.yml
vendored
@@ -1,28 +0,0 @@
|
||||
name: 'Integration tests'
|
||||
description: 'Run integration tests'
|
||||
inputs:
|
||||
github-token:
|
||||
description: 'The GITHUB_TOKEN secret'
|
||||
required: true
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Migrate DB
|
||||
if: always()
|
||||
run: npm run migrate up
|
||||
env:
|
||||
POSTGRES_URL: postgresql://postgres:postgres@localhost:5432/ci_test
|
||||
shell: bash
|
||||
|
||||
- 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 }}'
|
||||
POSTGRES_URL: postgresql://postgres:postgres@localhost:5432/ci_test
|
||||
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
|
||||
86
.github/actions/service-tests/action.yml
vendored
86
.github/actions/service-tests/action.yml
vendored
@@ -1,86 +0,0 @@
|
||||
name: 'Service tests'
|
||||
description: 'Run tests for selected services'
|
||||
inputs:
|
||||
github-token:
|
||||
description: 'The GITHUB_TOKEN secret'
|
||||
required: true
|
||||
librariesio-tokens:
|
||||
description: 'The SERVICETESTS_LIBRARIESIO_TOKENS secret'
|
||||
required: false
|
||||
default: ''
|
||||
obs-user:
|
||||
description: 'The SERVICETESTS_OBS_USER secret'
|
||||
required: false
|
||||
default: ''
|
||||
obs-pass:
|
||||
description: 'The SERVICETESTS_OBS_PASS secret'
|
||||
required: false
|
||||
default: ''
|
||||
sl-insight-user-uuid:
|
||||
description: 'The SERVICETESTS_SL_INSIGHT_USER_UUID secret'
|
||||
required: false
|
||||
default: ''
|
||||
sl-insight-api-token:
|
||||
description: 'The SERVICETESTS_SL_INSIGHT_API_TOKEN secret'
|
||||
required: false
|
||||
default: ''
|
||||
twitch-client-id:
|
||||
description: 'The SERVICETESTS_TWITCH_CLIENT_ID secret'
|
||||
required: false
|
||||
default: ''
|
||||
twitch-client-secret:
|
||||
description: 'The SERVICETESTS_TWITCH_CLIENT_SECRET secret'
|
||||
required: false
|
||||
default: ''
|
||||
wheelmap-token:
|
||||
description: 'The SERVICETESTS_WHEELMAP_TOKEN secret'
|
||||
required: false
|
||||
default: ''
|
||||
youtube-api-key:
|
||||
description: 'The SERVICETESTS_YOUTUBE_API_KEY secret'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Derive list of service tests to run
|
||||
# Note: In this step we are using an intermediate env var instead of
|
||||
# passing github.event.pull_request.title as an argument
|
||||
# to prevent a shell injection attack. Further reading:
|
||||
# https://securitylab.github.com/research/github-actions-untrusted-input/#exploitability-and-impact
|
||||
# https://securitylab.github.com/research/github-actions-untrusted-input/#remediation
|
||||
if: always()
|
||||
env:
|
||||
TITLE: ${{ github.event.pull_request.title }}
|
||||
run: npm run test:services:pr:prepare "$TITLE"
|
||||
shell: bash
|
||||
|
||||
- name: Run service tests
|
||||
if: always()
|
||||
run: npm run test:services:pr:run -- --reporter json --reporter-option 'output=reports/service-tests.json'
|
||||
shell: bash
|
||||
env:
|
||||
RETRY_COUNT: 3
|
||||
GH_TOKEN: '${{ inputs.github-token }}'
|
||||
LIBRARIESIO_TOKENS: '${{ inputs.librariesio-tokens }}'
|
||||
OBS_USER: '${{ inputs.obs-user }}'
|
||||
OBS_PASS: '${{ inputs.obs-pass }}'
|
||||
SL_INSIGHT_USER_UUID: '${{ inputs.sl-insight-user-uuid }}'
|
||||
SL_INSIGHT_API_TOKEN: '${{ inputs.sl-insight-api-token }}'
|
||||
TWITCH_CLIENT_ID: '${{ inputs.twitch-client-id }}'
|
||||
TWITCH_CLIENT_SECRET: '${{ inputs.twitch-client-secret }}'
|
||||
WHEELMAP_TOKEN: '${{ inputs.wheelmap-token }}'
|
||||
YOUTUBE_API_KEY: '${{ inputs.youtube-api-key }}'
|
||||
|
||||
- name: Write Markdown Summary
|
||||
if: always()
|
||||
run: |
|
||||
if test -f 'reports/service-tests.json'; then
|
||||
echo '# Services' >> $GITHUB_STEP_SUMMARY
|
||||
sed -e 's/^/- /' pull-request-services.log >> $GITHUB_STEP_SUMMARY
|
||||
node scripts/mocha2md.js Report reports/service-tests.json >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo 'No services found. Nothing to do.' >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
shell: bash
|
||||
36
.github/actions/setup/action.yml
vendored
36
.github/actions/setup/action.yml
vendored
@@ -1,36 +0,0 @@
|
||||
name: 'Set up project'
|
||||
description: 'Set up project'
|
||||
inputs:
|
||||
node-version:
|
||||
description: 'Version Spec of the version to use. Examples: 12.x, 10.15.1, >=10.15.0.'
|
||||
required: true
|
||||
cypress:
|
||||
description: 'Install Cypress binary (boolean)'
|
||||
type: boolean
|
||||
# https://docs.cypress.io/guides/getting-started/installing-cypress.html#Skipping-installation
|
||||
# We don't need to install the Cypress binary in jobs that aren't actually running Cypress.
|
||||
required: false
|
||||
default: false
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Install Node JS ${{ inputs.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
if: ${{ inputs.cypress == 'false' }}
|
||||
env:
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
run: |
|
||||
echo "skipping cypress binary"
|
||||
npm ci
|
||||
shell: bash
|
||||
|
||||
- name: Install dependencies (including cypress binary)
|
||||
if: ${{ inputs.cypress == 'true' }}
|
||||
run: |
|
||||
echo "installing cypress binary"
|
||||
npm ci
|
||||
shell: bash
|
||||
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
|
||||
|
||||
10
.github/workflows/build-docker-image.yml
vendored
10
.github/workflows/build-docker-image.yml
vendored
@@ -3,22 +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
|
||||
with:
|
||||
version: v0.9.1
|
||||
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@v4
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
|
||||
13
.github/workflows/create-release.yml
vendored
13
.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,18 +31,16 @@ jobs:
|
||||
tag: server-${{ steps.date.outputs.date }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
version: v0.9.1
|
||||
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@v4
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
|
||||
29
.github/workflows/danger.yml
vendored
29
.github/workflows/danger.yml
vendored
@@ -1,29 +0,0 @@
|
||||
name: Danger
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
|
||||
permissions:
|
||||
checks: write
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
|
||||
jobs:
|
||||
danger:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor != 'dependabot[bot]' && github.actor != 'repo-ranger[bot]'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Danger
|
||||
run: npm run danger ci
|
||||
env:
|
||||
# https://github.com/gatsbyjs/gatsby/pull/11555
|
||||
NODE_ENV: test
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
26
.github/workflows/deploy-docs.yml
vendored
26
.github/workflows/deploy-docs.yml
vendored
@@ -3,30 +3,24 @@ 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
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Build
|
||||
run: npm run build-docs
|
||||
run: |
|
||||
npm ci
|
||||
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@v3
|
||||
uses: actions/dependency-review-action@v1
|
||||
|
||||
12
.github/workflows/publish-docker-next.yml
vendored
12
.github/workflows/publish-docker-next.yml
vendored
@@ -5,19 +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
|
||||
with:
|
||||
version: v0.9.1
|
||||
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 }}
|
||||
@@ -26,7 +24,7 @@ jobs:
|
||||
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
|
||||
52
.github/workflows/test-e2e.yml
vendored
52
.github/workflows/test-e2e.yml
vendored
@@ -1,52 +0,0 @@
|
||||
name: E2E
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
- 'dependabot/**'
|
||||
|
||||
jobs:
|
||||
test-e2e:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Cache Cypress binary
|
||||
id: cache-cypress
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-cypress
|
||||
with:
|
||||
path: ~/.cache/Cypress
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: 16
|
||||
cypress: true
|
||||
|
||||
- name: Frontend build
|
||||
run: GATSBY_BASE_URL=http://localhost:8080 npm run build
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
run: npm run e2e-on-build
|
||||
|
||||
- name: Archive videos
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: videos
|
||||
path: cypress/videos
|
||||
|
||||
- name: Archive screenshots
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: screenshots
|
||||
path: cypress/screenshots
|
||||
26
.github/workflows/test-frontend.yml
vendored
26
.github/workflows/test-frontend.yml
vendored
@@ -1,26 +0,0 @@
|
||||
name: Frontend
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
- 'dependabot/**'
|
||||
|
||||
jobs:
|
||||
test-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Frontend tests
|
||||
uses: ./.github/actions/frontend-tests
|
||||
|
||||
- name: Frontend build
|
||||
run: npm run build
|
||||
61
.github/workflows/test-integration-17.yml
vendored
61
.github/workflows/test-integration-17.yml
vendored
@@ -1,61 +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
|
||||
postgres:
|
||||
image: postgres
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: ci_test
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
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 }}'
|
||||
59
.github/workflows/test-integration.yml
vendored
59
.github/workflows/test-integration.yml
vendored
@@ -1,59 +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
|
||||
postgres:
|
||||
image: postgres
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: ci_test
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
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
|
||||
40
.github/workflows/test-services-17.yml
vendored
40
.github/workflows/test-services-17.yml
vendored
@@ -1,40 +0,0 @@
|
||||
name: Services@node 17
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
|
||||
jobs:
|
||||
test-services-17:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: 17
|
||||
env:
|
||||
NPM_CONFIG_ENGINE_STRICT: 'false'
|
||||
|
||||
- name: Service tests (triggered from local branch)
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: ./.github/actions/service-tests
|
||||
with:
|
||||
github-token: '${{ secrets.GH_PAT }}'
|
||||
librariesio-tokens: '${{ secrets.SERVICETESTS_LIBRARIESIO_TOKENS }}'
|
||||
obs-user: '${{ secrets.SERVICETESTS_OBS_USER }}'
|
||||
obs-pass: '${{ secrets.SERVICETESTS_OBS_PASS }}'
|
||||
sl-insight-user-uuid: '${{ secrets.SERVICETESTS_SL_INSIGHT_USER_UUID }}'
|
||||
sl-insight-api-token: '${{ secrets.SERVICETESTS_SL_INSIGHT_API_TOKEN }}'
|
||||
twitch-client-id: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_ID }}'
|
||||
twitch-client-secret: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_SECRET }}'
|
||||
wheelmap-token: '${{ secrets.SERVICETESTS_WHEELMAP_TOKEN }}'
|
||||
youtube-api-key: '${{ secrets.SERVICETESTS_YOUTUBE_API_KEY }}'
|
||||
|
||||
- name: Service tests (triggered from fork)
|
||||
if: github.event.pull_request.head.repo.full_name != github.repository
|
||||
uses: ./.github/actions/service-tests
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
38
.github/workflows/test-services.yml
vendored
38
.github/workflows/test-services.yml
vendored
@@ -1,38 +0,0 @@
|
||||
name: Services
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
|
||||
jobs:
|
||||
test-services:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Service tests (triggered from local branch)
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: ./.github/actions/service-tests
|
||||
with:
|
||||
github-token: '${{ secrets.GH_PAT }}'
|
||||
librariesio-tokens: '${{ secrets.SERVICETESTS_LIBRARIESIO_TOKENS }}'
|
||||
obs-user: '${{ secrets.SERVICETESTS_OBS_USER }}'
|
||||
obs-pass: '${{ secrets.SERVICETESTS_OBS_PASS }}'
|
||||
sl-insight-user-uuid: '${{ secrets.SERVICETESTS_SL_INSIGHT_USER_UUID }}'
|
||||
sl-insight-api-token: '${{ secrets.SERVICETESTS_SL_INSIGHT_API_TOKEN }}'
|
||||
twitch-client-id: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_ID }}'
|
||||
twitch-client-secret: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_SECRET }}'
|
||||
wheelmap-token: '${{ secrets.SERVICETESTS_WHEELMAP_TOKEN }}'
|
||||
youtube-api-key: '${{ secrets.SERVICETESTS_YOUTUBE_API_KEY }}'
|
||||
|
||||
- name: Service tests (triggered from fork)
|
||||
if: github.event.pull_request.head.repo.full_name != github.repository
|
||||
uses: ./.github/actions/service-tests
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
33
.github/workflows/update-github-api.yml
vendored
33
.github/workflows/update-github-api.yml
vendored
@@ -1,33 +0,0 @@
|
||||
name: Update GitHub API Version
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 7 * * 6'
|
||||
# At 07:00 on Saturday
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update-github-api:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Check for new GitHub API version
|
||||
run: node scripts/update-github-api.js
|
||||
|
||||
- name: Create Pull Request if config has changed
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
commit-message: Update GitHub API Version
|
||||
title: Update [GitHub] API Version
|
||||
branch-suffix: random
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -117,6 +117,3 @@ service-definitions.yml
|
||||
|
||||
# Flamebearer
|
||||
flamegraph.html
|
||||
|
||||
# config file for node-pg-migrate
|
||||
migrations-config.json
|
||||
|
||||
148
CHANGELOG.md
148
CHANGELOG.md
@@ -4,154 +4,6 @@ Note: this changelog is for the shields.io server. The changelog for the badge-m
|
||||
|
||||
---
|
||||
|
||||
## server-2023-03-01
|
||||
|
||||
**Deprecation:** For users who need to maintain a Github Token pool, storage has been provided via the `RedisTokenPersistence` and `REDIS_URL` settings. As of this release, the `RedisTokenPersistence` backend is now deprecated and will be removed in a future release. If you are using this feature, you will need to migrate to using the `SQLTokenPersistence` backend for storage and provide a postgres connection string via the `POSTGRES_URL` setting. [#8922](https://github.com/badges/shields/issues/8922)
|
||||
|
||||
- fix: for crates.io versions, use max_stable_version if it exists [#8687](https://github.com/badges/shields/issues/8687)
|
||||
- don't autofocus search [#8927](https://github.com/badges/shields/issues/8927)
|
||||
- Add [Vcpkg] version service [#8923](https://github.com/badges/shields/issues/8923)
|
||||
- fix: Set uid/gid in docker image to 0 [#8908](https://github.com/badges/shields/issues/8908)
|
||||
- expose port 443 in Dockerfile [#8889](https://github.com/badges/shields/issues/8889)
|
||||
- Dependency updates
|
||||
|
||||
## server-2023-02-01
|
||||
|
||||
- replace [twitter] badge with static fallback [#8842](https://github.com/badges/shields/issues/8842)
|
||||
- Add various [Polymart] badges [#8811](https://github.com/badges/shields/issues/8811)
|
||||
- update [githubpipenv] tests/examples [#8797](https://github.com/badges/shields/issues/8797)
|
||||
- deprecate [apm] service [#8773](https://github.com/badges/shields/issues/8773)
|
||||
- deprecate lgtm [#8771](https://github.com/badges/shields/issues/8771)
|
||||
- Dependency updates
|
||||
|
||||
## server-2023-01-01
|
||||
|
||||
- Breaking change: Routes for GitHub workflows badge have changed. See https://github.com/badges/shields/issues/8671 for more details
|
||||
- Behaviour change: In this release we fixed a long standing bug. GitHub badges were previously not reading the base URL from the `config.service.baseUri`.
|
||||
This release fixes that bug, bringing the code into line with the documented behaviour. This should not cause a behaviour change for most users,
|
||||
but users who had previously set a value in `config.service.baseUri` which was previously ignored could see this now have an effect.
|
||||
Users who configure their instance using env vars rather than yaml should see no change.
|
||||
- Send `X-GitHub-Api-Version` when calling [GitHub] v3 API [#8669](https://github.com/badges/shields/issues/8669)
|
||||
- add [VpmVersion] badge [#8755](https://github.com/badges/shields/issues/8755)
|
||||
- Add [modrinth] game versions [#8673](https://github.com/badges/shields/issues/8673)
|
||||
- fix debug logging of undefined query params [#8540](https://github.com/badges/shields/issues/8540), [#8757](https://github.com/badges/shields/issues/8757)
|
||||
- fall back to classifiers if [pypi] license text is really long [#8690](https://github.com/badges/shields/issues/8690)
|
||||
- allow passing key to [stackexchange] [#8539](https://github.com/badges/shields/issues/8539)
|
||||
- Dependency updates
|
||||
|
||||
## server-2022-12-01
|
||||
|
||||
- fix: support logoColor to shield icons. [#8263](https://github.com/badges/shields/issues/8263)
|
||||
- handle missing properties array in [VisualStudioMarketplaceVersion] [#8603](https://github.com/badges/shields/issues/8603)
|
||||
- deprecate [wercker] service [#8642](https://github.com/badges/shields/issues/8642)
|
||||
- Add [Coincap] Cryptocurrency badges [#8623](https://github.com/badges/shields/issues/8623)
|
||||
- Add [modrinth] version [#8604](https://github.com/badges/shields/issues/8604)
|
||||
- [factorio-mod-portal] services [#8625](https://github.com/badges/shields/issues/8625)
|
||||
- [Coveralls] for GitLab [#8584](https://github.com/badges/shields/issues/8584), [#8644](https://github.com/badges/shields/issues/8644)
|
||||
- Remove 'suggest badges' feature [#8311](https://github.com/badges/shields/issues/8311)
|
||||
- Add [modrinth] followers [#8601](https://github.com/badges/shields/issues/8601)
|
||||
- Update the [modrinth] API to v2 [#8600](https://github.com/badges/shields/issues/8600)
|
||||
- tidy up [GitHubGist] routes [#8510](https://github.com/badges/shields/issues/8510)
|
||||
- fix [flathub] version error handling [#8500](https://github.com/badges/shields/issues/8500)
|
||||
- Dependency updates
|
||||
|
||||
## server-2022-11-01
|
||||
|
||||
- [Ansible] Add collection badge [#8578](https://github.com/badges/shields/issues/8578)
|
||||
- [VisualStudioMarketplace] Add support to prerelease extensions version (Issue #8207) [#8561](https://github.com/badges/shields/issues/8561)
|
||||
- feat: add [GitlabLastCommit] service [#8508](https://github.com/badges/shields/issues/8508)
|
||||
- fix [swagger] service tests (allow 0 items in array) [#8564](https://github.com/badges/shields/issues/8564)
|
||||
- fix codecov badge for non-default branch [#8565](https://github.com/badges/shields/issues/8565)
|
||||
- Add [GitHubLastCommit] by committer badge [#8537](https://github.com/badges/shields/issues/8537)
|
||||
- [GitHubReleaseDate] - published_at field [#8543](https://github.com/badges/shields/issues/8543)
|
||||
- Fix [Testspace] with new "untested" value in case_counts array [#8544](https://github.com/badges/shields/issues/8544)
|
||||
- fix: Support WAITING status for GitHub deployments [#8521](https://github.com/badges/shields/issues/8521)
|
||||
- [Whatpulse] badge for a user and for a team [#8466](https://github.com/badges/shields/issues/8466)
|
||||
- deprecate [pkgreview] service [#8499](https://github.com/badges/shields/issues/8499)
|
||||
- Dependency updates
|
||||
|
||||
## server-2022-10-08
|
||||
|
||||
- deprecate [criterion] service [#8501](https://github.com/badges/shields/issues/8501)
|
||||
- fix formatRelativeDate error handling; run [date] [#8497](https://github.com/badges/shields/issues/8497)
|
||||
- allow/validate bitbucket_username / bitbucket_password in private config schema [#8472](https://github.com/badges/shields/issues/8472)
|
||||
- fix [pub] points badge test and example [#8498](https://github.com/badges/shields/issues/8498)
|
||||
- feat: add [GitlabLanguageCount] service [#8377](https://github.com/badges/shields/issues/8377)
|
||||
- [GitHubGistStars] add GitHub Gist Stars [#8471](https://github.com/badges/shields/issues/8471)
|
||||
- fix display/search of CII badge examples [#8473](https://github.com/badges/shields/issues/8473)
|
||||
- feat: add 2022 support to GitHub Hacktoberfest [#8468](https://github.com/badges/shields/issues/8468)
|
||||
- fix [GitLabCoverage] subgroup bug [#8401](https://github.com/badges/shields/issues/8401)
|
||||
- implement ruby gems-specific version sort/color functions [#8434](https://github.com/badges/shields/issues/8434)
|
||||
- Add `rc` to pre-release identifiers [#8435](https://github.com/badges/shields/issues/8435)
|
||||
- add [GitHub] Number of commits between branches/tags/commits [#8394](https://github.com/badges/shields/issues/8394)
|
||||
- add [Packagist] dependency version [#8371](https://github.com/badges/shields/issues/8371)
|
||||
- fix Docker build status invalid response data bug [#8392](https://github.com/badges/shields/issues/8392)
|
||||
- Dependency updates
|
||||
|
||||
## server-2022-09-04
|
||||
|
||||
- fix frontend compile for users running on Windows [#8350](https://github.com/badges/shields/issues/8350)
|
||||
- [DockerSize] Docker image size multi arch [#8290](https://github.com/badges/shields/issues/8290)
|
||||
- upgrade gatsby [#8334](https://github.com/badges/shields/issues/8334)
|
||||
- Custom domains for [JitPack] artifacts [#8333](https://github.com/badges/shields/issues/8333)
|
||||
- fix [dockerstars] service [#8316](https://github.com/badges/shields/issues/8316)
|
||||
- [BountySource] Fix: Broken Badge generation for decimal activity values [#8315](https://github.com/badges/shields/issues/8315)
|
||||
- feat: add [gitlabmergerequests] service [#8166](https://github.com/badges/shields/issues/8166)
|
||||
- Fix terminology for [ROS] version service [#8292](https://github.com/badges/shields/issues/8292)
|
||||
- feat: add [GitlabStars] service [#8209](https://github.com/badges/shields/issues/8209)
|
||||
- Fix invalid `rst` format when `alt` or `target` is present [#8275](https://github.com/badges/shields/issues/8275)
|
||||
- [GithubGistLastCommit] GitHub gist last commit [#8272](https://github.com/badges/shields/issues/8272)
|
||||
- [GitHub] GitHub file size for a specific branch [#8262](https://github.com/badges/shields/issues/8262)
|
||||
- Dependency updates
|
||||
|
||||
## server-2022-08-01
|
||||
|
||||
- [pypi] Add Framework Version Badges support [#8261](https://github.com/badges/shields/issues/8261)
|
||||
- feat: add [GitlabForks] server [#8208](https://github.com/badges/shields/issues/8208)
|
||||
- Update PyPI api according to https://warehouse.pypa.io/api-reference/json.html [#8251](https://github.com/badges/shields/issues/8251)
|
||||
- Add [galaxytoolshed] Activity [#8164](https://github.com/badges/shields/issues/8164)
|
||||
- [greasyfork] Add Greasy Fork rating badges [#8087](https://github.com/badges/shields/issues/8087)
|
||||
- refactor(deps): Replace moment with dayjs [#8192](https://github.com/badges/shields/issues/8192)
|
||||
- add spaces round pipe in [conda] badge [#8189](https://github.com/badges/shields/issues/8189)
|
||||
- Add [ROS] version service [#8169](https://github.com/badges/shields/issues/8169)
|
||||
- feat: add [gitlabissues] service [#8108](https://github.com/badges/shields/issues/8108)
|
||||
- Dependency updates
|
||||
|
||||
## server-2022-07-03
|
||||
|
||||
- Add [galaxytoolshed] services [#8114](https://github.com/badges/shields/issues/8114)
|
||||
- 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
|
||||
|
||||
@@ -30,8 +30,8 @@ LABEL fly.version=$version
|
||||
ENV NODE_ENV production
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY --from=Builder --chown=0:0 /usr/src/app /usr/src/app
|
||||
COPY --from=Builder /usr/src/app /usr/src/app
|
||||
|
||||
CMD node server
|
||||
|
||||
EXPOSE 80 443
|
||||
EXPOSE 80
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
<a href="https://coveralls.io/github/badges/shields">
|
||||
<img src="https://img.shields.io/coveralls/github/badges/shields"
|
||||
alt="coverage"></a>
|
||||
<a href="https://lgtm.com/projects/g/badges/shields/alerts/">
|
||||
<img src="https://img.shields.io/lgtm/alerts/g/badges/shields"
|
||||
alt="Total alerts"/></a>
|
||||
<a href="https://discord.gg/HjJCwm5">
|
||||
<img src="https://img.shields.io/discord/308323056592486420?logo=discord"
|
||||
alt="chat on Discord"></a>
|
||||
|
||||
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": {
|
||||
|
||||
@@ -94,12 +94,10 @@ private:
|
||||
obs_user: 'OBS_USER'
|
||||
obs_pass: 'OBS_PASS'
|
||||
redis_url: 'REDIS_URL'
|
||||
postgres_url: 'POSTGRES_URL'
|
||||
sentry_dsn: 'SENTRY_DSN'
|
||||
sl_insight_userUuid: 'SL_INSIGHT_USER_UUID'
|
||||
sl_insight_apiToken: 'SL_INSIGHT_API_TOKEN'
|
||||
sonarqube_token: 'SONARQUBE_TOKEN'
|
||||
stackapps_api_key: 'STACKAPPS_API_KEY'
|
||||
teamcity_user: 'TEAMCITY_USER'
|
||||
teamcity_pass: 'TEAMCITY_PASS'
|
||||
twitch_client_id: 'TWITCH_CLIENT_ID'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
public:
|
||||
bind:
|
||||
address: '::'
|
||||
|
||||
metrics:
|
||||
prometheus:
|
||||
enabled: false
|
||||
@@ -11,26 +12,33 @@ public:
|
||||
intervalSeconds: 15
|
||||
ssl:
|
||||
isSecure: false
|
||||
|
||||
cors:
|
||||
allowedOrigin: []
|
||||
|
||||
services:
|
||||
github:
|
||||
baseUri: 'https://api.github.com'
|
||||
baseUri: 'https://api.github.com/'
|
||||
debug:
|
||||
enabled: false
|
||||
intervalSeconds: 200
|
||||
restApiVersion: '2022-11-28'
|
||||
obs:
|
||||
authorizedOrigins: 'https://api.opensuse.org'
|
||||
weblate:
|
||||
authorizedOrigins: 'https://hosted.weblate.org'
|
||||
trace: false
|
||||
|
||||
cacheHeaders:
|
||||
defaultCacheLengthSeconds: 120
|
||||
|
||||
handleInternalErrors: true
|
||||
|
||||
fetchLimit: '10MB'
|
||||
userAgentBase: 'shields (self-hosted)'
|
||||
|
||||
requestTimeoutSeconds: 120
|
||||
requestTimeoutMaxAgeSeconds: 30
|
||||
|
||||
requireCloudflare: false
|
||||
|
||||
private: {}
|
||||
|
||||
@@ -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
|
||||
|
||||
18
core/badge-urls/make-badge-url.d.ts
vendored
18
core/badge-urls/make-badge-url.d.ts
vendored
@@ -14,6 +14,24 @@ export function badgeUrlFromPath({
|
||||
longCache?: boolean
|
||||
}): string
|
||||
|
||||
export function badgeUrlFromPattern({
|
||||
baseUrl,
|
||||
pattern,
|
||||
namedParams,
|
||||
queryParams,
|
||||
style,
|
||||
format,
|
||||
longCache,
|
||||
}: {
|
||||
baseUrl?: string
|
||||
pattern: string
|
||||
namedParams: { [k: string]: string }
|
||||
queryParams: { [k: string]: string | number | boolean }
|
||||
style?: string
|
||||
format?: string
|
||||
longCache?: boolean
|
||||
}): string
|
||||
|
||||
export function encodeField(s: string): string
|
||||
|
||||
export function staticBadgeUrl({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Avoid "Attempted import error: 'URL' is not exported from 'url'" in frontend.
|
||||
import url from 'url'
|
||||
import queryString from 'query-string'
|
||||
import { compile } from 'path-to-regexp'
|
||||
|
||||
function badgeUrlFromPath({
|
||||
baseUrl = '',
|
||||
@@ -22,6 +23,33 @@ function badgeUrlFromPath({
|
||||
return `${baseUrl}${path}${outExt}${suffix}`
|
||||
}
|
||||
|
||||
function badgeUrlFromPattern({
|
||||
baseUrl = '',
|
||||
pattern,
|
||||
namedParams,
|
||||
queryParams,
|
||||
style,
|
||||
format = '',
|
||||
longCache = false,
|
||||
}) {
|
||||
const toPath = compile(pattern, {
|
||||
strict: true,
|
||||
sensitive: true,
|
||||
encode: encodeURIComponent,
|
||||
})
|
||||
|
||||
const path = toPath(namedParams)
|
||||
|
||||
return badgeUrlFromPath({
|
||||
baseUrl,
|
||||
path,
|
||||
queryParams,
|
||||
style,
|
||||
format,
|
||||
longCache,
|
||||
})
|
||||
}
|
||||
|
||||
function encodeField(s) {
|
||||
return encodeURIComponent(s.replace(/-/g, '--').replace(/_/g, '__'))
|
||||
}
|
||||
@@ -126,6 +154,7 @@ function rasterRedirectUrl({ rasterUrl }, badgeUrl) {
|
||||
|
||||
export {
|
||||
badgeUrlFromPath,
|
||||
badgeUrlFromPattern,
|
||||
encodeField,
|
||||
staticBadgeUrl,
|
||||
queryStringStaticBadgeUrl,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { test, given } from 'sazerac'
|
||||
import {
|
||||
badgeUrlFromPath,
|
||||
badgeUrlFromPattern,
|
||||
encodeField,
|
||||
staticBadgeUrl,
|
||||
queryStringStaticBadgeUrl,
|
||||
@@ -19,6 +20,18 @@ describe('Badge URL generation functions', function () {
|
||||
)
|
||||
})
|
||||
|
||||
test(badgeUrlFromPattern, () => {
|
||||
given({
|
||||
baseUrl: 'http://example.com',
|
||||
pattern: '/npm/v/:packageName',
|
||||
namedParams: { packageName: 'gh-badges' },
|
||||
style: 'flat-square',
|
||||
longCache: true,
|
||||
}).expect(
|
||||
'http://example.com/npm/v/gh-badges?cacheSeconds=2592000&style=flat-square'
|
||||
)
|
||||
})
|
||||
|
||||
test(encodeField, () => {
|
||||
given('foo').expect('foo')
|
||||
given('').expect('')
|
||||
|
||||
@@ -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'
|
||||
@@ -221,14 +227,8 @@ class BaseService {
|
||||
const logTrace = (...args) => trace.logTrace('fetch', ...args)
|
||||
let logUrl = url
|
||||
const logOptions = Object.assign({}, options)
|
||||
if ('searchParams' in options && options.searchParams != null) {
|
||||
const params = new URLSearchParams(
|
||||
Object.fromEntries(
|
||||
Object.entries(options.searchParams).filter(
|
||||
([k, v]) => v !== undefined
|
||||
)
|
||||
)
|
||||
)
|
||||
if ('searchParams' in options) {
|
||||
const params = new URLSearchParams(options.searchParams)
|
||||
logUrl = `${url}?${params.toString()}`
|
||||
delete logOptions.searchParams
|
||||
}
|
||||
@@ -429,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,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -440,21 +429,14 @@ describe('BaseService', function () {
|
||||
)
|
||||
|
||||
const url = 'some-url'
|
||||
const options = {
|
||||
headers: { Cookie: 'some-cookie' },
|
||||
searchParams: { param1: 'foobar', param2: undefined },
|
||||
}
|
||||
const options = { headers: { Cookie: 'some-cookie' } }
|
||||
await serviceInstance._request({ url, options })
|
||||
|
||||
expect(trace.logTrace).to.be.calledWithMatch(
|
||||
'fetch',
|
||||
sinon.match.string,
|
||||
'Request',
|
||||
`${url}?param1=foobar\n${JSON.stringify(
|
||||
{ headers: options.headers },
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
`${url}\n${JSON.stringify(options, null, 2)}`
|
||||
)
|
||||
expect(trace.logTrace).to.be.calledWithMatch(
|
||||
'fetch',
|
||||
@@ -581,9 +563,7 @@ describe('BaseService', function () {
|
||||
},
|
||||
private: {},
|
||||
},
|
||||
{
|
||||
namedParamA: 'bar.bar.bar',
|
||||
}
|
||||
{ namedParamA: 'bar.bar.bar' }
|
||||
)
|
||||
).to.deep.equal({
|
||||
color: 'lightgray',
|
||||
|
||||
@@ -16,15 +16,16 @@ import toArray from './to-array.js'
|
||||
//
|
||||
// Logos are resolved in this manner:
|
||||
//
|
||||
// 1. When `?logo=` contains a named logo or the name of one of the Shields
|
||||
// logos or contains base64-encoded SVG, that logo is used. When a
|
||||
// `&logoColor=` is specified, that color is used (except for the
|
||||
// base64-encoded logos). Otherwise the default color is used. If the color
|
||||
// is specified for a multicolor Shield logo, the named logo will be used and
|
||||
// colored. The appearance of the logo can be customized using `logoWidth`,
|
||||
// and in the case of the popout badge, `logoPosition`. When `?logo=` is
|
||||
// specified, any logo-related parameters specified dynamically by the
|
||||
// service, or by default in the service, are ignored.
|
||||
// 1. When `?logo=` contains the name of one of the Shields logos, or contains
|
||||
// base64-encoded SVG, that logo is used. In the case of a named logo, when
|
||||
// a `&logoColor=` is specified, that color is used. Otherwise the default
|
||||
// color is used. `logoColor` will not be applied to a custom
|
||||
// (base64-encoded) logo; if a custom color is desired the logo should be
|
||||
// recolored prior to making the request. The appearance of the logo can be
|
||||
// customized using `logoWidth`, and in the case of the popout badge,
|
||||
// `logoPosition`. When `?logo=` is specified, any logo-related parameters
|
||||
// specified dynamically by the service, or by default in the service, are
|
||||
// ignored.
|
||||
// 2. The second precedence is the dynamic logo returned by a service. This is
|
||||
// used only by the Endpoint badge. The `logoColor` can be overridden by the
|
||||
// query string.
|
||||
|
||||
@@ -153,18 +153,10 @@ describe('coalesceBadge', function () {
|
||||
).and.not.to.be.empty
|
||||
})
|
||||
|
||||
it('applies the named monochrome logo with color', function () {
|
||||
expect(
|
||||
coalesceBadge({}, { namedLogo: 'dependabot', logoColor: 'blue' }, {})
|
||||
.logo
|
||||
).to.equal(getShieldsIcon({ name: 'dependabot', color: 'blue' })).and.not
|
||||
.to.be.empty
|
||||
})
|
||||
|
||||
it('applies the named multicolored logo with color', function () {
|
||||
it('applies the named logo with color', function () {
|
||||
expect(
|
||||
coalesceBadge({}, { namedLogo: 'npm', logoColor: 'blue' }, {}).logo
|
||||
).to.equal(getSimpleIcon({ name: 'npm', color: 'blue' })).and.not.to.be
|
||||
).to.equal(getShieldsIcon({ name: 'npm', color: 'blue' })).and.not.to.be
|
||||
.empty
|
||||
})
|
||||
|
||||
@@ -174,25 +166,15 @@ describe('coalesceBadge', function () {
|
||||
).to.equal(getShieldsIcon({ name: 'npm' })).and.not.be.empty
|
||||
})
|
||||
|
||||
it('overrides the monochrome logo with a color', function () {
|
||||
expect(
|
||||
coalesceBadge(
|
||||
{ logo: 'dependabot', logoColor: 'blue' },
|
||||
{ namedLogo: 'appveyor' },
|
||||
{}
|
||||
).logo
|
||||
).to.equal(getShieldsIcon({ name: 'dependabot', color: 'blue' })).and.not
|
||||
.be.empty
|
||||
})
|
||||
|
||||
it('overrides multicolored logo with a color', function () {
|
||||
it('overrides the logo with a color', function () {
|
||||
expect(
|
||||
coalesceBadge(
|
||||
{ logo: 'npm', logoColor: 'blue' },
|
||||
{ namedLogo: 'appveyor' },
|
||||
{}
|
||||
).logo
|
||||
).to.equal(getSimpleIcon({ name: 'npm', color: 'blue' })).and.not.be.empty
|
||||
).to.equal(getShieldsIcon({ name: 'npm', color: 'blue' })).and.not.be
|
||||
.empty
|
||||
})
|
||||
|
||||
it("when the logo is overridden, it ignores the service's logo color, position, and width", function () {
|
||||
@@ -210,25 +192,15 @@ describe('coalesceBadge', function () {
|
||||
).to.equal(getShieldsIcon({ name: 'npm' })).and.not.be.empty
|
||||
})
|
||||
|
||||
it("overrides the service monochome logo's color", function () {
|
||||
expect(
|
||||
coalesceBadge(
|
||||
{ logoColor: 'blue' },
|
||||
{ namedLogo: 'dependabot', logoColor: 'red' },
|
||||
{}
|
||||
).logo
|
||||
).to.equal(getShieldsIcon({ name: 'dependabot', color: 'blue' })).and.not
|
||||
.be.empty
|
||||
})
|
||||
|
||||
it("overrides the service multicolored logo's color", function () {
|
||||
it("overrides the service logo's color", function () {
|
||||
expect(
|
||||
coalesceBadge(
|
||||
{ logoColor: 'blue' },
|
||||
{ namedLogo: 'npm', logoColor: 'red' },
|
||||
{}
|
||||
).logo
|
||||
).to.equal(getSimpleIcon({ name: 'npm', color: 'blue' })).and.not.be.empty
|
||||
).to.equal(getShieldsIcon({ name: 'npm', color: 'blue' })).and.not.be
|
||||
.empty
|
||||
})
|
||||
|
||||
// https://github.com/badges/shields/issues/2998
|
||||
|
||||
@@ -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)
|
||||
@@ -27,13 +20,9 @@ class InvalidService extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
function getServicePaths(pattern) {
|
||||
return glob.sync(toUnixPath(path.join(serviceDir, '**', pattern)))
|
||||
}
|
||||
|
||||
async function loadServiceClasses(servicePaths) {
|
||||
if (!servicePaths) {
|
||||
servicePaths = getServicePaths('*.service.js')
|
||||
servicePaths = glob.sync(path.join(serviceDir, '**', '*.service.js'))
|
||||
}
|
||||
|
||||
const serviceClasses = []
|
||||
@@ -53,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)
|
||||
}
|
||||
@@ -104,16 +93,15 @@ async function collectDefinitions() {
|
||||
|
||||
async function loadTesters() {
|
||||
return Promise.all(
|
||||
getServicePaths('*.tester.js').map(
|
||||
async path => await import(`file://${path}`)
|
||||
)
|
||||
glob
|
||||
.sync(path.join(serviceDir, '**', '*.tester.js'))
|
||||
.map(async path => await import(`file://${path}`))
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
InvalidService,
|
||||
loadServiceClasses,
|
||||
getServicePaths,
|
||||
checkNames,
|
||||
collectDefinitions,
|
||||
loadTesters,
|
||||
|
||||
@@ -2,11 +2,7 @@ import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import chai from 'chai'
|
||||
import chaiAsPromised from 'chai-as-promised'
|
||||
import {
|
||||
loadServiceClasses,
|
||||
getServicePaths,
|
||||
InvalidService,
|
||||
} from './loader.js'
|
||||
import { loadServiceClasses, InvalidService } from './loader.js'
|
||||
chai.use(chaiAsPromised)
|
||||
|
||||
const { expect } = chai
|
||||
@@ -69,15 +65,3 @@ describe('loadServiceClasses function', function () {
|
||||
).to.eventually.have.length(5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getServicePaths', function () {
|
||||
// these tests just make sure we discover a
|
||||
// plausibly large number of .service and .tester files
|
||||
it('finds a non-zero number of services in the project', function () {
|
||||
expect(getServicePaths('*.service.js')).to.have.length.above(400)
|
||||
})
|
||||
|
||||
it('finds a non-zero number of testers in the project', function () {
|
||||
expect(getServicePaths('*.tester.js')).to.have.length.above(400)
|
||||
})
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
@@ -14,16 +14,14 @@ let resourceCache = Object.create(null)
|
||||
/**
|
||||
* Make a HTTP request using an in-memory cache
|
||||
*
|
||||
* @async
|
||||
* @param {object} attrs - Refer to individual attrs
|
||||
* @param {string} attrs.url - URL to request
|
||||
* @param {number} attrs.ttl - Number of milliseconds to keep cached value for
|
||||
* @param {boolean} [attrs.json=true] - True if we expect to parse the response as JSON
|
||||
* @param {Function} [attrs.scraper=buffer => buffer] - Function to extract value from the response
|
||||
* @param {object} [attrs.options={}] - Options to pass to got
|
||||
* @param {Function} [attrs.requestFetcher=fetch] - Custom fetch function
|
||||
* @throws {InvalidResponse} - Error if unable to parse response
|
||||
* @returns {Promise<*>} Promise that resolves to parsed response
|
||||
* @param {object} attrs Refer to individual attrs
|
||||
* @param {string} attrs.url URL to request
|
||||
* @param {number} attrs.ttl Number of milliseconds to keep cached value for
|
||||
* @param {boolean} [attrs.json=true] True if we expect to parse the response as JSON
|
||||
* @param {Function} [attrs.scraper=buffer => buffer] Function to extract value from the response
|
||||
* @param {object} [attrs.options={}] Options to pass to got
|
||||
* @param {Function} [attrs.requestFetcher=fetch] Custom fetch function
|
||||
* @returns {*} Parsed response
|
||||
*/
|
||||
async function getCachedResource({
|
||||
url,
|
||||
|
||||
@@ -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,18 +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 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'
|
||||
@@ -112,9 +114,6 @@ const publicConfigSchema = Joi.object({
|
||||
redirectUrl: optionalUrl,
|
||||
rasterUrl: optionalUrl,
|
||||
cors: {
|
||||
// This doesn't actually do anything
|
||||
// TODO: maybe remove in future?
|
||||
// https://github.com/badges/shields/pull/8311#discussion_r945337530
|
||||
allowedOrigin: Joi.array().items(optionalUrl).required(),
|
||||
},
|
||||
services: Joi.object({
|
||||
@@ -126,7 +125,6 @@ const publicConfigSchema = Joi.object({
|
||||
enabled: Joi.boolean().required(),
|
||||
intervalSeconds: Joi.number().integer().min(1).required(),
|
||||
},
|
||||
restApiVersion: Joi.date().raw().required(),
|
||||
},
|
||||
gitlab: defaultService,
|
||||
jira: defaultService,
|
||||
@@ -143,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(),
|
||||
@@ -172,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()),
|
||||
@@ -183,12 +181,10 @@ const privateConfigSchema = Joi.object({
|
||||
obs_user: Joi.string(),
|
||||
obs_pass: Joi.string(),
|
||||
redis_url: Joi.string().uri({ scheme: ['redis', 'rediss'] }),
|
||||
postgres_url: Joi.string().uri({ scheme: 'postgresql' }),
|
||||
sentry_dsn: Joi.string(),
|
||||
sl_insight_userUuid: Joi.string(),
|
||||
sl_insight_apiToken: Joi.string(),
|
||||
sonarqube_token: Joi.string(),
|
||||
stackapps_api_key: Joi.string(),
|
||||
teamcity_user: Joi.string(),
|
||||
teamcity_pass: Joi.string(),
|
||||
twitch_client_id: Joi.string(),
|
||||
@@ -204,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 {
|
||||
/**
|
||||
@@ -313,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`,
|
||||
@@ -359,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()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -405,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,
|
||||
@@ -485,14 +469,18 @@ 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 },
|
||||
cors: { allowedOrigin },
|
||||
requireCloudflare,
|
||||
} = this.config.public
|
||||
|
||||
@@ -500,62 +488,65 @@ 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()
|
||||
}
|
||||
}
|
||||
|
||||
const { apiProvider: githubApiProvider } = this.githubConstellation
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -567,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()
|
||||
|
||||
@@ -60,14 +60,12 @@ describe('The server', function () {
|
||||
})
|
||||
|
||||
it('should serve badges with custom maxAge', async function () {
|
||||
const { headers } = await got(`${baseUrl}badge/foo-bar-blue`)
|
||||
expect(headers['cache-control']).to.equal('max-age=86400, s-maxage=86400')
|
||||
const { headers } = await got(`${baseUrl}npm/l/express`)
|
||||
expect(headers['cache-control']).to.equal('max-age=3600, s-maxage=3600')
|
||||
})
|
||||
|
||||
it('should return cors header for the request', async function () {
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}badge/foo-bar-blue.svg`
|
||||
)
|
||||
const { statusCode, headers } = await got(`${baseUrl}npm/v/express.svg`)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(headers['access-control-allow-origin']).to.equal('*')
|
||||
})
|
||||
@@ -75,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(
|
||||
@@ -86,15 +82,12 @@ describe('The server', function () {
|
||||
})
|
||||
|
||||
it('should redirect modern PNG badges as configured', async function () {
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}badge/foo-bar-blue.png`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
)
|
||||
const { statusCode, headers } = await got(`${baseUrl}npm/v/express.png`, {
|
||||
followRedirect: false,
|
||||
})
|
||||
expect(statusCode).to.equal(301)
|
||||
expect(headers.location).to.equal(
|
||||
'http://raster.example.test/badge/foo-bar-blue.png'
|
||||
'http://raster.example.test/npm/v/express.png'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -103,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')
|
||||
})
|
||||
|
||||
@@ -117,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()
|
||||
@@ -202,14 +197,10 @@ describe('The server', function () {
|
||||
})
|
||||
|
||||
it('should return the 410 badge for obsolete formats', async function () {
|
||||
const { statusCode, body } = await got(
|
||||
`${baseUrl}badge/foo-bar-blue.jpg`,
|
||||
{
|
||||
throwHttpErrors: false,
|
||||
}
|
||||
)
|
||||
// TODO It would be nice if this were 404 or 410.
|
||||
expect(statusCode).to.equal(200)
|
||||
const { statusCode, body } = await got(`${baseUrl}npm/v/express.jpg`, {
|
||||
throwHttpErrors: false,
|
||||
})
|
||||
expect(statusCode).to.equal(410)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('410')
|
||||
@@ -247,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))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -275,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 () {
|
||||
|
||||
100
core/service-test-runner/infer-pull-request.js
Normal file
100
core/service-test-runner/infer-pull-request.js
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { URL, format as urlFormat } from 'url'
|
||||
|
||||
function formatSlug(owner, repo, pullRequest) {
|
||||
return `${owner}/${repo}#${pullRequest}`
|
||||
}
|
||||
|
||||
function parseGithubPullRequestUrl(url, options = {}) {
|
||||
const { verifyBaseUrl } = options
|
||||
|
||||
const parsed = new URL(url)
|
||||
const components = parsed.pathname.substr(1).split('/')
|
||||
if (components[2] !== 'pull' || components.length !== 4) {
|
||||
throw Error(`Invalid GitHub pull request URL: ${url}`)
|
||||
}
|
||||
const [owner, repo, , pullRequest] = components
|
||||
|
||||
parsed.pathname = ''
|
||||
const baseUrl = urlFormat(parsed, {
|
||||
auth: false,
|
||||
fragment: false,
|
||||
search: false,
|
||||
}).replace(/\/$/, '')
|
||||
|
||||
if (verifyBaseUrl && baseUrl !== verifyBaseUrl) {
|
||||
throw Error(`Expected base URL to be ${verifyBaseUrl} but got ${baseUrl}`)
|
||||
}
|
||||
|
||||
return {
|
||||
baseUrl,
|
||||
owner,
|
||||
repo,
|
||||
pullRequest: +pullRequest,
|
||||
slug: formatSlug(owner, repo, pullRequest),
|
||||
}
|
||||
}
|
||||
|
||||
function parseGithubRepoSlug(slug) {
|
||||
const components = slug.split('/')
|
||||
if (components.length !== 2) {
|
||||
throw Error(`Invalid GitHub repo slug: ${slug}`)
|
||||
}
|
||||
const [owner, repo] = components
|
||||
return { owner, repo }
|
||||
}
|
||||
|
||||
function _inferPullRequestFromTravisEnv(env) {
|
||||
const { owner, repo } = parseGithubRepoSlug(env.TRAVIS_REPO_SLUG)
|
||||
const pullRequest = +env.TRAVIS_PULL_REQUEST
|
||||
return {
|
||||
owner,
|
||||
repo,
|
||||
pullRequest,
|
||||
slug: formatSlug(owner, repo, pullRequest),
|
||||
}
|
||||
}
|
||||
|
||||
function _inferPullRequestFromCircleEnv(env) {
|
||||
return parseGithubPullRequestUrl(env.CI_PULL_REQUEST)
|
||||
}
|
||||
|
||||
/**
|
||||
* When called inside a CI build, infer the details
|
||||
* of a pull request from the environment variables.
|
||||
*
|
||||
* @param {object} [env=process.env] Environment variables
|
||||
* @returns {module:core/service-test-runner/infer-pull-request~PullRequest}
|
||||
* Pull Request
|
||||
*/
|
||||
function inferPullRequest(env = process.env) {
|
||||
if (env.TRAVIS) {
|
||||
return _inferPullRequestFromTravisEnv(env)
|
||||
} else if (env.CIRCLECI) {
|
||||
return _inferPullRequestFromCircleEnv(env)
|
||||
} else if (env.CI) {
|
||||
throw Error(
|
||||
'Unsupported CI system. Unable to obtain pull request information from the environment.'
|
||||
)
|
||||
} else {
|
||||
throw Error(
|
||||
'Unable to obtain pull request information from the environment. Is this running in CI?'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull Request
|
||||
*
|
||||
* @typedef PullRequest
|
||||
* @property {string} pr.baseUrl (returned for travis CI only)
|
||||
* @property {string} owner
|
||||
* @property {string} repo
|
||||
* @property {string} pullRequest PR/issue number
|
||||
* @property {string} slug owner/repo/#pullRequest
|
||||
*/
|
||||
|
||||
export { parseGithubPullRequestUrl, parseGithubRepoSlug, inferPullRequest }
|
||||
48
core/service-test-runner/infer-pull-request.spec.js
Normal file
48
core/service-test-runner/infer-pull-request.spec.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import { test, given, forCases } from 'sazerac'
|
||||
import {
|
||||
parseGithubPullRequestUrl,
|
||||
inferPullRequest,
|
||||
} from './infer-pull-request.js'
|
||||
|
||||
describe('Pull request inference', function () {
|
||||
test(parseGithubPullRequestUrl, () => {
|
||||
forCases([
|
||||
given('https://github.com/badges/shields/pull/1234'),
|
||||
given('https://github.com/badges/shields/pull/1234', {
|
||||
verifyBaseUrl: 'https://github.com',
|
||||
}),
|
||||
]).expect({
|
||||
baseUrl: 'https://github.com',
|
||||
owner: 'badges',
|
||||
repo: 'shields',
|
||||
pullRequest: 1234,
|
||||
slug: 'badges/shields#1234',
|
||||
})
|
||||
|
||||
given('https://github.com/badges/shields/pull/1234', {
|
||||
verifyBaseUrl: 'https://example.com',
|
||||
}).expectError(
|
||||
'Expected base URL to be https://example.com but got https://github.com'
|
||||
)
|
||||
})
|
||||
|
||||
test(inferPullRequest, () => {
|
||||
const expected = {
|
||||
owner: 'badges',
|
||||
repo: 'shields',
|
||||
pullRequest: 1234,
|
||||
slug: 'badges/shields#1234',
|
||||
}
|
||||
|
||||
given({
|
||||
CIRCLECI: '1',
|
||||
CI_PULL_REQUEST: 'https://github.com/badges/shields/pull/1234',
|
||||
}).expect(Object.assign({ baseUrl: 'https://github.com' }, expected))
|
||||
|
||||
given({
|
||||
TRAVIS: '1',
|
||||
TRAVIS_REPO_SLUG: 'badges/shields',
|
||||
TRAVIS_PULL_REQUEST: '1234',
|
||||
}).expect(expected)
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
// Derive a list of service tests to run based on
|
||||
// space-separated service names in the PR title.
|
||||
// Infer the current PR from the Travis environment, and look for bracketed,
|
||||
// space-separated service names in the pull request title.
|
||||
//
|
||||
// Output the list of services.
|
||||
//
|
||||
@@ -8,26 +8,54 @@
|
||||
// Output:
|
||||
// travis
|
||||
// sonar
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// TRAVIS=1 TRAVIS_REPO_SLUG=badges/shields TRAVIS_PULL_REQUEST=1108 npm run test:services:pr:prepare
|
||||
|
||||
import got from 'got'
|
||||
import { inferPullRequest } from './infer-pull-request.js'
|
||||
import servicesForTitle from './services-for-title.js'
|
||||
|
||||
let title
|
||||
async function getTitle(owner, repo, pullRequest) {
|
||||
const {
|
||||
body: { title },
|
||||
} = await got(
|
||||
`https://api.github.com/repos/${owner}/${repo}/pulls/${pullRequest}`,
|
||||
{
|
||||
headers: {
|
||||
'User-Agent': 'badges/shields',
|
||||
Authorization: `token ${process.env.GITHUB_TOKEN}`,
|
||||
},
|
||||
responseType: 'json',
|
||||
}
|
||||
)
|
||||
return title
|
||||
}
|
||||
|
||||
try {
|
||||
if (process.argv.length < 3) {
|
||||
throw new Error()
|
||||
async function main() {
|
||||
const { owner, repo, pullRequest, slug } = inferPullRequest()
|
||||
console.error(`PR: ${slug}`)
|
||||
|
||||
const title = await getTitle(owner, repo, pullRequest)
|
||||
|
||||
console.error(`Title: ${title}\n`)
|
||||
const services = servicesForTitle(title)
|
||||
if (services.length === 0) {
|
||||
console.error('No services found. Nothing to do.')
|
||||
} else {
|
||||
console.error(
|
||||
`Services: (${services.length} found) ${services.join(', ')}\n`
|
||||
)
|
||||
console.log(services.join('\n'))
|
||||
}
|
||||
title = process.argv[2]
|
||||
} catch (e) {
|
||||
console.error('Error processing arguments')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.error(`Title: ${title}\n`)
|
||||
const services = servicesForTitle(title)
|
||||
if (services.length === 0) {
|
||||
console.error('No services found. Nothing to do.')
|
||||
} else {
|
||||
console.error(`Services: (${services.length} found) ${services.join(', ')}\n`)
|
||||
console.log(services.join('\n'))
|
||||
}
|
||||
;(async () => {
|
||||
try {
|
||||
await main()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
process.exit(1)
|
||||
}
|
||||
})()
|
||||
|
||||
@@ -84,6 +84,7 @@ describe('Redis token persistence', function () {
|
||||
const toRemove = expected.pop()
|
||||
|
||||
await persistence.initialize()
|
||||
|
||||
await persistence.noteTokenRemoved(toRemove)
|
||||
|
||||
const savedTokens = await redis.smembers(key)
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import pg from 'pg'
|
||||
import { expect } from 'chai'
|
||||
import configModule from 'config'
|
||||
import SqlTokenPersistence from './sql-token-persistence.js'
|
||||
|
||||
const config = configModule.util.toObject()
|
||||
const postgresUrl = config?.private?.postgres_url
|
||||
const tableName = 'token_persistence_integration_test'
|
||||
|
||||
describe('SQL token persistence', function () {
|
||||
let pool
|
||||
let persistence
|
||||
|
||||
before('Mock db connection and load app', async function () {
|
||||
// Create a new pool with a connection limit of 1
|
||||
pool = new pg.Pool({
|
||||
connectionString: postgresUrl,
|
||||
|
||||
// Reuse the connection to make sure we always hit the same pg_temp schema
|
||||
max: 1,
|
||||
|
||||
// Disable auto-disconnection of idle clients to make sure we always hit the same pg_temp schema
|
||||
idleTimeoutMillis: 0,
|
||||
})
|
||||
persistence = new SqlTokenPersistence({
|
||||
url: postgresUrl,
|
||||
table: tableName,
|
||||
})
|
||||
})
|
||||
after(async function () {
|
||||
if (persistence) {
|
||||
await persistence.stop()
|
||||
persistence = undefined
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach('Create temporary table', async function () {
|
||||
await pool.query(
|
||||
`CREATE TEMPORARY TABLE ${tableName} (LIKE github_user_tokens INCLUDING ALL);`
|
||||
)
|
||||
})
|
||||
afterEach('Drop temporary table', async function () {
|
||||
await pool.query(`DROP TABLE IF EXISTS pg_temp.${tableName};`)
|
||||
})
|
||||
|
||||
context('when the key does not exist', function () {
|
||||
it('does nothing', async function () {
|
||||
const tokens = await persistence.initialize(pool)
|
||||
expect(tokens).to.deep.equal([])
|
||||
})
|
||||
})
|
||||
|
||||
context('when the key exists', function () {
|
||||
const initialTokens = ['a', 'b', 'c'].map(char => char.repeat(40))
|
||||
|
||||
beforeEach(async function () {
|
||||
initialTokens.forEach(async token => {
|
||||
await pool.query(
|
||||
`INSERT INTO pg_temp.${tableName} (token) VALUES ($1::text);`,
|
||||
[token]
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('loads the contents', async function () {
|
||||
const tokens = await persistence.initialize(pool)
|
||||
expect(tokens.sort()).to.deep.equal(initialTokens)
|
||||
})
|
||||
|
||||
context('when tokens are added', function () {
|
||||
it('saves the change', async function () {
|
||||
const newToken = 'e'.repeat(40)
|
||||
const expected = initialTokens.slice()
|
||||
expected.push(newToken)
|
||||
|
||||
await persistence.initialize(pool)
|
||||
await persistence.noteTokenAdded(newToken)
|
||||
|
||||
const result = await pool.query(
|
||||
`SELECT token FROM pg_temp.${tableName};`
|
||||
)
|
||||
const savedTokens = result.rows.map(row => row.token)
|
||||
expect(savedTokens.sort()).to.deep.equal(expected)
|
||||
})
|
||||
})
|
||||
|
||||
context('when tokens are removed', function () {
|
||||
it('saves the change', async function () {
|
||||
const expected = Array.from(initialTokens)
|
||||
const toRemove = expected.pop()
|
||||
|
||||
await persistence.initialize(pool)
|
||||
await persistence.noteTokenRemoved(toRemove)
|
||||
|
||||
const result = await pool.query(
|
||||
`SELECT token FROM pg_temp.${tableName};`
|
||||
)
|
||||
const savedTokens = result.rows.map(row => row.token)
|
||||
expect(savedTokens.sort()).to.deep.equal(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,55 +0,0 @@
|
||||
import pg from 'pg'
|
||||
import log from '../server/log.js'
|
||||
|
||||
export default class SqlTokenPersistence {
|
||||
constructor({ url, table }) {
|
||||
this.url = url
|
||||
this.table = table
|
||||
this.noteTokenAdded = this.noteTokenAdded.bind(this)
|
||||
this.noteTokenRemoved = this.noteTokenRemoved.bind(this)
|
||||
}
|
||||
|
||||
async initialize(pool) {
|
||||
if (pool) {
|
||||
this.pool = pool
|
||||
} else {
|
||||
this.pool = new pg.Pool({ connectionString: this.url })
|
||||
}
|
||||
const result = await this.pool.query(`SELECT token FROM ${this.table};`)
|
||||
return result.rows.map(row => row.token)
|
||||
}
|
||||
|
||||
async stop() {
|
||||
await this.pool.end()
|
||||
}
|
||||
|
||||
async onTokenAdded(token) {
|
||||
return await this.pool.query(
|
||||
`INSERT INTO ${this.table} (token) VALUES ($1::text) ON CONFLICT (token) DO NOTHING;`,
|
||||
[token]
|
||||
)
|
||||
}
|
||||
|
||||
async onTokenRemoved(token) {
|
||||
return await this.pool.query(
|
||||
`DELETE FROM ${this.table} WHERE token=$1::text;`,
|
||||
[token]
|
||||
)
|
||||
}
|
||||
|
||||
async noteTokenAdded(token) {
|
||||
try {
|
||||
await this.onTokenAdded(token)
|
||||
} catch (e) {
|
||||
log.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
async noteTokenRemoved(token) {
|
||||
try {
|
||||
await this.onTokenRemoved(token)
|
||||
} catch (e) {
|
||||
log.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,61 +0,0 @@
|
||||
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"]'
|
||||
|
||||
function expectBadgeExample(title, previewUrl, pattern) {
|
||||
cy.contains('tr', `${title}:`).find('code').should('have.text', pattern)
|
||||
cy.contains('tr', `${title}:`)
|
||||
.find('img')
|
||||
.should('have.attr', 'src', previewUrl)
|
||||
}
|
||||
|
||||
function visitAndWait(page) {
|
||||
cy.visit(page)
|
||||
cy.waitForStableDOM({ pollInterval: 1000, timeout: 10000 })
|
||||
}
|
||||
|
||||
it('Search for badges', function () {
|
||||
visitAndWait('/')
|
||||
|
||||
cy.get(SEARCH_INPUT).type('pypi')
|
||||
|
||||
cy.contains('PyPI - License')
|
||||
})
|
||||
|
||||
it('Shows badge from category', function () {
|
||||
visitAndWait('/category/chat')
|
||||
|
||||
expectBadgeExample(
|
||||
'Discourse status',
|
||||
'http://localhost:8080/badge/discourse-online-brightgreen',
|
||||
'/discourse/status?server=https%3A%2F%2Fmeta.discourse.org'
|
||||
)
|
||||
})
|
||||
|
||||
it('Customizate badges', function () {
|
||||
visitAndWait('/')
|
||||
|
||||
cy.get(SEARCH_INPUT).type('issues')
|
||||
|
||||
cy.contains('/github/issues/:user/:repo').click()
|
||||
|
||||
cy.get('input[name="user"]').type('badges')
|
||||
cy.get('input[name="repo"]').type('shields')
|
||||
cy.get('table input[name="color"]').type('orange')
|
||||
|
||||
cy.get(`img[src='${backendUrl}/github/issues/badges/shields?color=orange']`)
|
||||
})
|
||||
|
||||
it('Do not duplicate example parameters', function () {
|
||||
visitAndWait('/category/funding')
|
||||
|
||||
cy.contains('GitHub Sponsors').click()
|
||||
cy.get('[name="style"]').should($style => {
|
||||
expect($style).to.have.length(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
72
cypress/integration/main-page.spec.js
Normal file
72
cypress/integration/main-page.spec.js
Normal file
@@ -0,0 +1,72 @@
|
||||
describe('Main page', function () {
|
||||
const backendUrl = Cypress.env('backend_url')
|
||||
const SEARCH_INPUT = 'input[placeholder="search / project URL"]'
|
||||
|
||||
function expectBadgeExample(title, previewUrl, pattern) {
|
||||
cy.contains('tr', `${title}:`).find('code').should('have.text', pattern)
|
||||
cy.contains('tr', `${title}:`)
|
||||
.find('img')
|
||||
.should('have.attr', 'src', previewUrl)
|
||||
}
|
||||
|
||||
it('Search for badges', function () {
|
||||
cy.visit('/')
|
||||
|
||||
cy.get(SEARCH_INPUT).type('pypi')
|
||||
|
||||
cy.contains('PyPI - License')
|
||||
})
|
||||
|
||||
it('Shows badge from category', function () {
|
||||
cy.visit('/category/chat')
|
||||
|
||||
expectBadgeExample(
|
||||
'Discourse status',
|
||||
'http://localhost:8080/badge/discourse-online-brightgreen',
|
||||
'/discourse/status?server=https%3A%2F%2Fmeta.discourse.org'
|
||||
)
|
||||
})
|
||||
|
||||
it('Suggest badges', function () {
|
||||
const badgeUrl = `${backendUrl}/github/issues/badges/shields`
|
||||
cy.visit('/')
|
||||
|
||||
cy.get(SEARCH_INPUT).type('https://github.com/badges/shields')
|
||||
cy.contains('Suggest badges').click()
|
||||
|
||||
expectBadgeExample('GitHub issues', badgeUrl, badgeUrl)
|
||||
})
|
||||
|
||||
it('Customization form is filled with suggested badge details', function () {
|
||||
const badgeUrl = `${backendUrl}/github/issues/badges/shields`
|
||||
cy.visit('/')
|
||||
cy.get(SEARCH_INPUT).type('https://github.com/badges/shields')
|
||||
cy.contains('Suggest badges').click()
|
||||
|
||||
cy.contains(badgeUrl).click()
|
||||
|
||||
cy.get('input[name="user"]').should('have.value', 'badges')
|
||||
cy.get('input[name="repo"]').should('have.value', 'shields')
|
||||
})
|
||||
|
||||
it('Customizate suggested badge', function () {
|
||||
const badgeUrl = `${backendUrl}/github/issues/badges/shields`
|
||||
cy.visit('/')
|
||||
cy.get(SEARCH_INPUT).type('https://github.com/badges/shields')
|
||||
cy.contains('Suggest badges').click()
|
||||
cy.contains(badgeUrl).click()
|
||||
|
||||
cy.get('table input[name="color"]').type('orange')
|
||||
|
||||
cy.get(`img[src='${backendUrl}/github/issues/badges/shields?color=orange']`)
|
||||
})
|
||||
|
||||
it('Do not duplicate example parameters', function () {
|
||||
cy.visit('/category/funding')
|
||||
|
||||
cy.contains('GitHub Sponsors').click()
|
||||
cy.get('[name="style"]').should($style => {
|
||||
expect($style).to.have.length(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -11,6 +11,7 @@
|
||||
// DANGER_GITHUB_API_TOKEN=your-github-api-token npm run danger -- pr https://github.com/badges/shields/pull/2665
|
||||
|
||||
const { danger, fail, message, warn } = require('danger')
|
||||
const { default: noTestShortcuts } = require('danger-plugin-no-test-shortcuts')
|
||||
const { fileMatch } = danger.git
|
||||
|
||||
const documentation = fileMatch(
|
||||
@@ -113,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('')
|
||||
@@ -172,3 +173,11 @@ affectedServices.forEach(service => {
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Prevent merging exclusive services tests.
|
||||
noTestShortcuts({
|
||||
testFilePredicate: filePath => filePath.endsWith('.tester.js'),
|
||||
patterns: {
|
||||
only: ['only()'],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# Badges Requiring Authentication
|
||||
|
||||
There are two patterns for how shields.io can interact with APIs that require auth:
|
||||
|
||||
1. We can store one token at the service level which allows us to read public data for everyone's projects, or lift a rate limit. If you are looking for information on configuring credentials for a self-hosted instance see https://github.com/badges/shields/blob/master/doc/server-secrets.md
|
||||
2. If every user needs to provide their own token, that has to be a token which can be passed to us as a query param in the badge URL. This means it must be possible to generate a key or token that can be exposed in a public github README public with no negative consequences. (i.e: that key or token only exposes public metrics).
|
||||
|
||||
If every user would need to supply their own token for some particular service and it is only possible to generate a key or token which allows access to sensitive data or allows write access to resources, we can't provide an integration for this service.
|
||||
@@ -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:
|
||||
|
||||
@@ -20,6 +20,8 @@ The Shields codebase is divided into several parts:
|
||||
1. `*.js` in the root of [`services`][services]
|
||||
7. The services themselves (about 80% of the code)
|
||||
1. `*.js` in the folders of [`services`][services]
|
||||
8. The badge suggestion endpoint (Note: it's tested as if it’s a service.)
|
||||
1. [`lib/suggest.js`][suggest]
|
||||
|
||||
[frontend]: https://github.com/badges/shields/tree/master/frontend
|
||||
[badge-maker]: https://github.com/badges/shields/tree/master/badge-maker
|
||||
@@ -27,6 +29,7 @@ The Shields codebase is divided into several parts:
|
||||
[server]: https://github.com/badges/shields/tree/master/core/server
|
||||
[token-pooling]: https://github.com/badges/shields/tree/master/core/token-pooling
|
||||
[services]: https://github.com/badges/shields/tree/master/services
|
||||
[suggest]: https://github.com/badges/shields/tree/master/lib/suggest.js
|
||||
|
||||
The tests are also divided into several parts:
|
||||
|
||||
@@ -55,7 +58,7 @@ The tests are also divided into several parts:
|
||||
[redis-token-persistence.integration]: https://github.com/badges/shields/blob/master/core/token-pooling/redis-token-persistence.integration.js
|
||||
[github-api-provider.integration]: https://github.com/badges/shields/blob/master/services/github/github-api-provider.integration.js
|
||||
|
||||
Our goal is to reach 100% coverage of the code in the
|
||||
Our goal is for the core code is to reach 100% coverage of the code in the
|
||||
frontend, core, and service helper functions when the unit and functional
|
||||
tests are run.
|
||||
|
||||
@@ -77,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
|
||||
@@ -116,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
|
||||
@@ -143,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
|
||||
@@ -151,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.
|
||||
|
||||
|
||||
14
doc/logos.md
14
doc/logos.md
@@ -22,18 +22,6 @@ Any custom logo can be passed in a URL parameter by base64 encoding it. e.g:
|
||||
|
||||
 - https://img.shields.io/badge/play-station-blue.svg?logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEiIHdpZHRoPSI2MDAiIGhlaWdodD0iNjAwIj48cGF0aCBkPSJNMTI5IDExMWMtNTUgNC05MyA2Ni05MyA3OEwwIDM5OGMtMiA3MCAzNiA5MiA2OSA5MWgxYzc5IDAgODctNTcgMTMwLTEyOGgyMDFjNDMgNzEgNTAgMTI4IDEyOSAxMjhoMWMzMyAxIDcxLTIxIDY5LTkxbC0zNi0yMDljMC0xMi00MC03OC05OC03OGgtMTBjLTYzIDAtOTIgMzUtOTIgNDJIMjM2YzAtNy0yOS00Mi05Mi00MmgtMTV6IiBmaWxsPSIjZmZmIi8+PC9zdmc+
|
||||
|
||||
### logoColor parameter
|
||||
|
||||
The `logoColor` param can be used to set the color of the logo. Hex, rgb, rgba, hsl, hsla and css named colors can all be used. For SimpleIcons named logos (which are monochrome), the color will be applied to the SimpleIcons logo.
|
||||
|
||||
-  - https://img.shields.io/badge/logo-javascript-blue?logo=javascript
|
||||
-  - https://img.shields.io/badge/logo-javascript-blue?logo=javascript&logoColor=f5f5f5
|
||||
|
||||
In the case where Shields hosts a custom multi-colored logo, if the `logoColor` param is passed, the corresponding SimpleIcons logo will be substituted and colored.
|
||||
|
||||
-  - https://img.shields.io/badge/logo-gitlab-blue?logo=gitlab
|
||||
-  - https://img.shields.io/badge/logo-gitlab-blue?logo=gitlab&logoColor=white
|
||||
|
||||
## Contributing Logos
|
||||
|
||||
Our preferred way to consume icons is via the SimpleIcons logo. As a first port of call, we encourage you to contribute logos to [the SimpleIcons project][simple-icons github]. Please review their [guidance](https://github.com/simple-icons/simple-icons/blob/develop/CONTRIBUTING.md) before contributing.
|
||||
@@ -52,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`.
|
||||
@@ -153,6 +153,15 @@ Then copy the contents of the `build/` folder to your static hosting / CDN.
|
||||
|
||||
There are also a couple settings you should configure on the server.
|
||||
|
||||
If you want to use server suggestions, you should also set `ALLOWED_ORIGIN`:
|
||||
|
||||
```sh
|
||||
ALLOWED_ORIGIN=http://my-custom-shields.s3.amazonaws.com,https://my-custom-shields.s3.amazonaws.com
|
||||
```
|
||||
|
||||
This should be a comma-separated list of allowed origin headers. They should
|
||||
not have paths or trailing slashes.
|
||||
|
||||
To help out users, you can make the Shields server redirect the server root.
|
||||
Set the `REDIRECT_URI` environment variable:
|
||||
|
||||
|
||||
@@ -125,17 +125,11 @@ Because of GitHub rate limits, you will need to provide a token, or else badges
|
||||
will stop working once you hit 60 requests per hour, the
|
||||
[unauthenticated rate limit][github rate limit].
|
||||
|
||||
You can [create a personal access token][personal access tokens] (PATs) through the
|
||||
You can [create a personal access token][personal access tokens] through the
|
||||
GitHub website. When you create the token, you can choose to give read access
|
||||
to your repositories. If you do that, your self-hosted Shields installation
|
||||
will have access to your private repositories.
|
||||
|
||||
For most users we recommend using a classic PAT as opposed to a [fine-grained PAT][fine-grained pat].
|
||||
It is possible to request a fairly large subset of the GitHub badge suite using a
|
||||
fine-grained PAT for authentication but there are also some badges that won't work.
|
||||
This is because some of our badges make use of GitHub's v4 GraphQL API and the
|
||||
GraphQL API only supports authentication with a classic PAT.
|
||||
|
||||
When a `gh_token` is specified, it is used in place of the Shields token
|
||||
rotation logic.
|
||||
|
||||
@@ -145,7 +139,6 @@ token, though it's not required.
|
||||
|
||||
[github rate limit]: https://developer.github.com/v3/#rate-limiting
|
||||
[personal access tokens]: https://github.com/settings/tokens
|
||||
[fine-grained pat]: https://github.blog/2022-10-18-introducing-fine-grained-personal-access-tokens-for-github/
|
||||
|
||||
- `GH_CLIENT_ID` (yml: `private.gh_client_id`)
|
||||
- `GH_CLIENT_SECRET` (yml: `private.gh_client_secret`)
|
||||
@@ -251,17 +244,6 @@ Create an account, sign in and obtain a uuid and token from your
|
||||
to give your self-hosted Shields installation access to a
|
||||
private SonarQube instance or private project on a public instance.
|
||||
|
||||
### StackApps (for StackExchange and StackOverflow)
|
||||
|
||||
- `STACKAPPS_API_KEY`: (yml: `private.stackapps_api_key`)
|
||||
|
||||
Anonymous requests to the stackexchange API are limited to 300 calls per day.
|
||||
To increase your quota to 10,000 calls per day, create an account at
|
||||
[StackApps](https://stackapps.com/) and
|
||||
[register an OAuth app](https://stackapps.com/apps/oauth/register). Having registered
|
||||
an OAuth app, you'll be granted a key which can be used to increase your request quota.
|
||||
It is not necessary to performa full OAuth Flow to gain an access token.
|
||||
|
||||
### TeamCity
|
||||
|
||||
- `TEAMCITY_ORIGINS` (yml: `public.services.teamcity.authorizedOrigins`)
|
||||
|
||||
@@ -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"`.
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# Static Badges
|
||||
|
||||
It is possible to use shields.io to make a wide variety of badges displaying static text and/or logos. For example:
|
||||
|
||||
-  - https://img.shields.io/badge/any%20text-you%20like-blue
|
||||
-  - https://img.shields.io/badge/just%20the%20message-8A2BE2
|
||||
-  - https://img.shields.io/badge/%27for%20the%20badge%27%20style-20B2AA?style=for-the-badge
|
||||
-  - https://img.shields.io/badge/with%20a%20logo-grey?style=for-the-badge&logo=javascript
|
||||
|
||||
Full documentation of styles and parameters: https://shields.io/#styles
|
||||
|
||||
More documentation on logos: https://github.com/badges/shields/blob/master/doc/logos.md
|
||||
@@ -2,11 +2,13 @@ import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import {
|
||||
badgeUrlFromPath,
|
||||
badgeUrlFromPattern,
|
||||
staticBadgeUrl,
|
||||
} from '../../core/badge-urls/make-badge-url'
|
||||
import { removeRegexpFromPattern } from '../lib/pattern-helpers'
|
||||
import {
|
||||
Example as ExampleData,
|
||||
Suggestion,
|
||||
RenderableExample,
|
||||
} from '../lib/service-definitions'
|
||||
import { Badge } from './common'
|
||||
@@ -34,34 +36,49 @@ function Example({
|
||||
baseUrl,
|
||||
onClick,
|
||||
exampleData,
|
||||
isBadgeSuggestion,
|
||||
}: {
|
||||
baseUrl?: string
|
||||
onClick: (example: RenderableExample) => void
|
||||
onClick: (example: RenderableExample, isSuggestion: boolean) => void
|
||||
exampleData: RenderableExample
|
||||
isBadgeSuggestion: boolean
|
||||
}): JSX.Element {
|
||||
const handleClick = React.useCallback(
|
||||
function (): void {
|
||||
onClick(exampleData)
|
||||
onClick(exampleData, isBadgeSuggestion)
|
||||
},
|
||||
[exampleData, onClick]
|
||||
[exampleData, isBadgeSuggestion, onClick]
|
||||
)
|
||||
|
||||
const {
|
||||
example: { pattern, queryParams },
|
||||
preview: { label, message, color, style, namedLogo },
|
||||
} = exampleData as ExampleData
|
||||
const previewUrl = staticBadgeUrl({
|
||||
baseUrl,
|
||||
label: label || '',
|
||||
message,
|
||||
color,
|
||||
style,
|
||||
namedLogo,
|
||||
})
|
||||
const exampleUrl = badgeUrlFromPath({
|
||||
path: removeRegexpFromPattern(pattern),
|
||||
queryParams,
|
||||
})
|
||||
let exampleUrl, previewUrl
|
||||
if (isBadgeSuggestion) {
|
||||
const {
|
||||
example: { pattern, namedParams, queryParams },
|
||||
} = exampleData as Suggestion
|
||||
exampleUrl = previewUrl = badgeUrlFromPattern({
|
||||
baseUrl,
|
||||
pattern,
|
||||
namedParams,
|
||||
queryParams,
|
||||
})
|
||||
} else {
|
||||
const {
|
||||
example: { pattern, queryParams },
|
||||
preview: { label, message, color, style, namedLogo },
|
||||
} = exampleData as ExampleData
|
||||
previewUrl = staticBadgeUrl({
|
||||
baseUrl,
|
||||
label: label || '',
|
||||
message,
|
||||
color,
|
||||
style,
|
||||
namedLogo,
|
||||
})
|
||||
exampleUrl = badgeUrlFromPath({
|
||||
path: removeRegexpFromPattern(pattern),
|
||||
queryParams,
|
||||
})
|
||||
}
|
||||
|
||||
const { title } = exampleData
|
||||
return (
|
||||
@@ -84,12 +101,14 @@ function Example({
|
||||
|
||||
export function BadgeExamples({
|
||||
examples,
|
||||
areBadgeSuggestions,
|
||||
baseUrl,
|
||||
onClick,
|
||||
}: {
|
||||
examples: RenderableExample[]
|
||||
areBadgeSuggestions: boolean
|
||||
baseUrl?: string
|
||||
onClick: (exampleData: RenderableExample) => void
|
||||
onClick: (exampleData: RenderableExample, isSuggestion: boolean) => void
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<ExampleTable>
|
||||
@@ -98,6 +117,7 @@ export function BadgeExamples({
|
||||
<Example
|
||||
baseUrl={baseUrl}
|
||||
exampleData={exampleData}
|
||||
isBadgeSuggestion={areBadgeSuggestions}
|
||||
key={`${exampleData.title} ${exampleData.example.pattern}`}
|
||||
onClick={onClick}
|
||||
/>
|
||||
|
||||
@@ -18,6 +18,8 @@ export default function Customizer({
|
||||
exampleNamedParams,
|
||||
exampleQueryParams,
|
||||
initialStyle,
|
||||
isPrefilled,
|
||||
link = '',
|
||||
}: {
|
||||
baseUrl: string
|
||||
title: string
|
||||
@@ -25,6 +27,8 @@ export default function Customizer({
|
||||
exampleNamedParams: { [k: string]: string }
|
||||
exampleQueryParams: { [k: string]: string }
|
||||
initialStyle?: string
|
||||
isPrefilled: boolean
|
||||
link?: string
|
||||
}): JSX.Element {
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/35572
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/28884#issuecomment-471341041
|
||||
@@ -71,6 +75,7 @@ export default function Customizer({
|
||||
const builtBadgeUrl = generateBuiltBadgeUrl()
|
||||
const markup = generateMarkup({
|
||||
badgeUrl: builtBadgeUrl,
|
||||
link,
|
||||
title,
|
||||
markupFormat,
|
||||
})
|
||||
@@ -88,7 +93,7 @@ export default function Customizer({
|
||||
indicatorRef.current.trigger()
|
||||
}
|
||||
},
|
||||
[generateBuiltBadgeUrl, title, setMessage, setMarkup]
|
||||
[generateBuiltBadgeUrl, link, title, setMessage, setMarkup]
|
||||
)
|
||||
|
||||
function renderMarkupAndLivePreview(): JSX.Element {
|
||||
@@ -142,6 +147,7 @@ export default function Customizer({
|
||||
<form action="">
|
||||
<PathBuilder
|
||||
exampleParams={exampleNamedParams}
|
||||
isPrefilled={isPrefilled}
|
||||
onChange={handlePathChange}
|
||||
pattern={pattern}
|
||||
/>
|
||||
|
||||
@@ -112,6 +112,7 @@ export default function PathBuilder({
|
||||
pattern,
|
||||
exampleParams,
|
||||
onChange,
|
||||
isPrefilled,
|
||||
}: {
|
||||
pattern: string
|
||||
exampleParams: { [k: string]: string }
|
||||
@@ -122,19 +123,22 @@ export default function PathBuilder({
|
||||
path: string
|
||||
isComplete: boolean
|
||||
}) => void
|
||||
isPrefilled: boolean
|
||||
}): JSX.Element {
|
||||
const [tokens] = useState(() => parse(pattern))
|
||||
const [namedParams, setNamedParams] = useState(() =>
|
||||
// `pathToRegexp.parse()` returns a mixed array of strings for literals
|
||||
// and objects for parameters. Filter out the literals and work with the
|
||||
// objects.
|
||||
tokens
|
||||
.filter(t => typeof t !== 'string')
|
||||
.map(t => t as Key)
|
||||
.reduce((accum, { name }) => {
|
||||
accum[name] = ''
|
||||
return accum
|
||||
}, {} as { [k: string]: string })
|
||||
isPrefilled
|
||||
? exampleParams
|
||||
: // `pathToRegexp.parse()` returns a mixed array of strings for literals
|
||||
// and objects for parameters. Filter out the literals and work with the
|
||||
// objects.
|
||||
tokens
|
||||
.filter(t => typeof t !== 'string')
|
||||
.map(t => t as Key)
|
||||
.reduce((accum, { name }) => {
|
||||
accum[name] = ''
|
||||
return accum
|
||||
}, {} as { [k: string]: string })
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -191,11 +195,11 @@ export default function PathBuilder({
|
||||
onChange={handleTokenChange}
|
||||
value={value}
|
||||
>
|
||||
<option key="empty" value="">
|
||||
<option disabled={isPrefilled} key="empty" value="">
|
||||
{' '}
|
||||
</option>
|
||||
{options.map(option => (
|
||||
<option key={option} value={option}>
|
||||
<option disabled={isPrefilled} key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
@@ -204,6 +208,7 @@ export default function PathBuilder({
|
||||
} else {
|
||||
return (
|
||||
<NamedParamInput
|
||||
disabled={isPrefilled}
|
||||
name={name}
|
||||
onChange={handleTokenChange}
|
||||
type="text"
|
||||
@@ -234,9 +239,11 @@ export default function PathBuilder({
|
||||
{optional ? <BuilderLabel>(optional)</BuilderLabel> : null}
|
||||
</NamedParamLabelContainer>
|
||||
{renderNamedParamInput(token)}
|
||||
<NamedParamCaption>
|
||||
{namedParamIndex === 0 ? `e.g. ${exampleValue}` : exampleValue}
|
||||
</NamedParamCaption>
|
||||
{!isPrefilled && (
|
||||
<NamedParamCaption>
|
||||
{namedParamIndex === 0 ? `e.g. ${exampleValue}` : exampleValue}
|
||||
</NamedParamCaption>
|
||||
)}
|
||||
</PathBuilderColumn>
|
||||
</React.Fragment>
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ import React, {
|
||||
} from 'react'
|
||||
import styled from 'styled-components'
|
||||
import humanizeString from 'humanize-string'
|
||||
import qs from 'query-string'
|
||||
import { stringify as stringifyQueryString } from 'query-string'
|
||||
import { advertisedStyles } from '../../lib/supported-features'
|
||||
import { noAutocorrect, StyledInput } from '../common'
|
||||
import {
|
||||
@@ -94,7 +94,7 @@ function getQueryString({
|
||||
}
|
||||
})
|
||||
|
||||
const queryString = qs.stringify(outQuery)
|
||||
const queryString = stringifyQueryString(outQuery)
|
||||
|
||||
return { queryString, isComplete }
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import ServiceDefinitionSetHelper from '../lib/service-definitions/service-defin
|
||||
import { getBaseUrl } from '../constants'
|
||||
import Meta from './meta'
|
||||
import Header from './header'
|
||||
import Search from './search'
|
||||
import SuggestionAndSearch from './suggestion-and-search'
|
||||
import DonateBox from './donate'
|
||||
import { MarkupModal } from './markup-modal'
|
||||
import Usage from './usage'
|
||||
@@ -49,6 +49,8 @@ export default function Main({
|
||||
[k: string]: ServiceDefinition[]
|
||||
}>()
|
||||
const [selectedExample, setSelectedExample] = useState<RenderableExample>()
|
||||
const [selectedExampleIsSuggestion, setSelectedExampleIsSuggestion] =
|
||||
useState(false)
|
||||
const searchTimeout = useRef(0)
|
||||
const baseUrl = getBaseUrl()
|
||||
|
||||
@@ -90,6 +92,14 @@ export default function Main({
|
||||
[setSearchIsInProgress, performSearch]
|
||||
)
|
||||
|
||||
const exampleClicked = React.useCallback(
|
||||
function (example: RenderableExample, isSuggestion: boolean): void {
|
||||
setSelectedExample(example)
|
||||
setSelectedExampleIsSuggestion(isSuggestion)
|
||||
},
|
||||
[setSelectedExample, setSelectedExampleIsSuggestion]
|
||||
)
|
||||
|
||||
const dismissMarkupModal = React.useCallback(
|
||||
function (): void {
|
||||
setSelectedExample(undefined)
|
||||
@@ -113,6 +123,7 @@ export default function Main({
|
||||
<div>
|
||||
<CategoryHeading category={category} />
|
||||
<BadgeExamples
|
||||
areBadgeSuggestions={false}
|
||||
baseUrl={baseUrl}
|
||||
examples={flattened}
|
||||
onClick={setSelectedExample}
|
||||
@@ -171,10 +182,15 @@ export default function Main({
|
||||
<MarkupModal
|
||||
baseUrl={baseUrl}
|
||||
example={selectedExample}
|
||||
isBadgeSuggestion={selectedExampleIsSuggestion}
|
||||
onRequestClose={dismissMarkupModal}
|
||||
/>
|
||||
<section>
|
||||
<Search queryChanged={searchQueryChanged} />
|
||||
<SuggestionAndSearch
|
||||
baseUrl={baseUrl}
|
||||
onBadgeClick={exampleClicked}
|
||||
queryChanged={searchQueryChanged}
|
||||
/>
|
||||
<DonateBox />
|
||||
</section>
|
||||
{renderMain()}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user