Compare commits

..

25 Commits

Author SHA1 Message Date
Paul Melnikow
fc68b88a04 Merge branch 'master' into express 2022-04-23 21:55:55 -04:00
Paul Melnikow
ca6ae88504 Tweak style 2022-04-22 15:26:25 -07:00
Paul Melnikow
fc13e8e90a Merge branch 'master' into express 2022-04-22 15:19:21 -07:00
Paul Melnikow
1761aab020 Wee bit of cleanup 2022-04-22 15:09:39 -07:00
Paul Melnikow
416ef3920a Don't alias this 2022-04-22 13:09:50 -04:00
Paul Melnikow
25ab4806c0 Clean up makeBadge tests 2022-04-22 13:01:58 -04:00
Paul Melnikow
9d6b3e0985 Clean lint 2022-04-22 12:54:21 -04:00
Paul Melnikow
18c76e392f Clean up acceptor test 2022-04-22 12:49:04 -04:00
Paul Melnikow
78b886c010 Merge branch 'master' into express 2022-04-22 12:36:39 -04:00
Paul Melnikow
b19ca203e8 Update docs and suggest test 2022-04-22 12:35:08 -04:00
Paul Melnikow
00df3e1136 Clean diff 2022-04-22 12:23:04 -04:00
Paul Melnikow
b4f7ec383e Update integration tests 2022-04-22 12:20:25 -04:00
Paul Melnikow
6d77534709 Progress / cleanup 2022-04-22 12:12:37 -04:00
Paul Melnikow
e8f59a2645 More fixes 2022-04-17 11:10:15 -04:00
Paul Melnikow
8cbc8cb926 More tests passing 2022-04-17 10:46:10 -04:00
Paul Melnikow
be9d49083d More tests passing 2022-04-17 10:41:33 -04:00
Paul Melnikow
aa046cb510 Fixes 2022-04-17 01:55:57 -04:00
Paul Melnikow
903bef2a4c Finish rename 2022-04-17 01:40:27 -04:00
Paul Melnikow
15043dfc92 Remove more obsolete code 2022-04-17 01:39:42 -04:00
Paul Melnikow
cf6b5b14c7 Remove obsolete code 2022-04-17 01:38:33 -04:00
Paul Melnikow
aeebfaa51f Renames 2022-04-17 01:38:28 -04:00
Paul Melnikow
c3097aad0d Fix / remove obsolete tests 2022-04-17 01:27:38 -04:00
Paul Melnikow
6e6cec9b2b Move one test 2022-04-17 01:16:16 -04:00
Paul Melnikow
7c071e352e Core tests passing 2022-04-17 01:10:05 -04:00
Paul Melnikow
6d72fd68e8 Begin to replace scoutcamp with express 2022-04-16 23:29:11 -04:00
675 changed files with 40326 additions and 41435 deletions

385
.circleci/config.yml Normal file
View 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\/.*/

View File

@@ -3,7 +3,6 @@ shields.env
.git/ .git/
.gitignore .gitignore
.vscode/ .vscode/
fly.toml
# Improve layer cacheability. # Improve layer cacheability.
Dockerfile Dockerfile

View File

@@ -2,6 +2,7 @@ extends:
- standard - standard
- standard-jsx - standard-jsx
- standard-react - standard-react
- plugin:@typescript-eslint/recommended
- prettier - prettier
- eslint:recommended - eslint:recommended
@@ -17,13 +18,12 @@ settings:
react: react:
version: '16.8' version: '16.8'
jsdoc: jsdoc:
mode: typescript mode: jsdoc
plugins: plugins:
- chai-friendly - chai-friendly
- jsdoc - jsdoc
- mocha - mocha
- icedfrisby
- no-extension-in-require - no-extension-in-require
- sort-class-members - sort-class-members
- import - import
@@ -35,34 +35,40 @@ overrides:
# list of rules, even if they only apply to certain files. That way the # list of rules, even if they only apply to certain files. That way the
# rules listed here are only ones which conflict. # rules listed here are only ones which conflict.
- files:
- 'badge-maker/**/*.js'
- '**/*.cjs'
env:
node: true
es6: true
- files: - files:
- '**/*.js' - '**/*.js'
- '!frontend/**/*.js' - '!frontend/**/*.js'
- '!badge-maker/**/*.js'
env: env:
node: true node: true
es6: true es6: true
rules:
no-console: 'off'
'@typescript-eslint/explicit-module-boundary-types': 'off'
- files:
- '**/*.@(ts|tsx)'
parserOptions: parserOptions:
sourceType: 'module' sourceType: 'module'
parser: '@typescript-eslint/parser' parser: '@typescript-eslint/parser'
rules: rules:
no-console: 'off' # Argh.
'@typescript-eslint/explicit-function-return-type':
['error', { 'allowExpressions': true }]
'@typescript-eslint/no-empty-function': 'error'
'@typescript-eslint/no-var-requires': 'error'
'@typescript-eslint/no-object-literal-type-assertion': 'off'
'@typescript-eslint/no-explicit-any': 'error'
'@typescript-eslint/ban-ts-ignore': 'off'
'@typescript-eslint/explicit-module-boundary-types': 'off'
- files: - files:
- '**/*.ts' - core/**/*.ts
parserOptions: parserOptions:
sourceType: 'module' sourceType: 'module'
parser: '@typescript-eslint/parser' parser: '@typescript-eslint/parser'
- files: - files:
- 'frontend/**/*.js' - gatsby-browser.js
- 'frontend/**/*.@(js|ts|tsx)'
parserOptions: parserOptions:
sourceType: 'module' sourceType: 'module'
env: env:
@@ -107,20 +113,21 @@ overrides:
mocha: true mocha: true
rules: rules:
mocha/no-exclusive-tests: 'error' mocha/no-exclusive-tests: 'error'
mocha/no-skipped-tests: 'error'
mocha/no-mocha-arrows: 'error' mocha/no-mocha-arrows: 'error'
mocha/prefer-arrow-callback: 'error' mocha/prefer-arrow-callback: 'error'
- files:
- 'services/**/*.tester.js'
rules:
icedfrisby/no-exclusive-tests: 'error'
icedfrisby/no-skipped-tests: 'error'
rules: rules:
# Disable some rules from eslint:recommended. # Disable some rules from eslint:recommended.
no-empty: ['error', { 'allowEmptyCatch': true }] no-empty: ['error', { 'allowEmptyCatch': true }]
# Allow unused parameters. In callbacks, removing them seems to obscure
# what the functions are doing.
'@typescript-eslint/no-unused-vars': ['error', { 'args': 'none' }]
no-unused-vars: 'off'
'@typescript-eslint/no-var-requires': 'off'
'@typescript-eslint/no-use-before-define': 'error'
no-use-before-define: 'off' no-use-before-define: 'off'
# These should be disabled by eslint-config-prettier, but are not. # These should be disabled by eslint-config-prettier, but are not.
@@ -137,8 +144,6 @@ rules:
func-style: ['error', 'declaration', { 'allowArrowFunctions': true }] func-style: ['error', 'declaration', { 'allowArrowFunctions': true }]
new-cap: ['error', { 'capIsNew': true }] new-cap: ['error', { 'capIsNew': true }]
import/order: ['error', { 'newlines-between': 'never' }] import/order: ['error', { 'newlines-between': 'never' }]
quotes:
['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }]
# Account for destructuring responses from upstream services, # Account for destructuring responses from upstream services,
# many of which do not follow camelcase # many of which do not follow camelcase
@@ -171,7 +176,7 @@ rules:
jsdoc/check-tag-names: 'error' jsdoc/check-tag-names: 'error'
jsdoc/check-types: 'error' jsdoc/check-types: 'error'
jsdoc/implements-on-classes: 'error' jsdoc/implements-on-classes: 'error'
jsdoc/tag-lines: ['error', 'any', { 'startLines': 1 }] jsdoc/newline-after-description: 'error'
jsdoc/require-param: 'error' jsdoc/require-param: 'error'
jsdoc/require-param-description: 'error' jsdoc/require-param-description: 'error'
jsdoc/require-param-name: 'error' jsdoc/require-param-name: 'error'
@@ -182,7 +187,11 @@ rules:
jsdoc/require-returns-type: 'error' jsdoc/require-returns-type: 'error'
jsdoc/valid-types: 'error' jsdoc/valid-types: 'error'
react/prop-types: 'off' # Disable some from TypeScript.
'@typescript-eslint/camelcase': off
'@typescript-eslint/explicit-function-return-type': 'off'
'@typescript-eslint/no-empty-function': 'off'
react/jsx-sort-props: 'error' react/jsx-sort-props: 'error'
react-hooks/rules-of-hooks: 'error' react-hooks/rules-of-hooks: 'error'
react-hooks/exhaustive-deps: 'error' react-hooks/exhaustive-deps: 'error'

View File

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

View File

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

View File

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

View File

@@ -35,6 +35,7 @@ function allChangelogLinesAreVersionBump(changelogLines) {
function isPointlessVersionBump(body) { function isPointlessVersionBump(body) {
const pointlessBumpLinks = [ const pointlessBumpLinks = [
'https://github.com/gatsbyjs/gatsby',
'https://github.com/typescript-eslint/typescript-eslint', 'https://github.com/typescript-eslint/typescript-eslint',
] ]

View File

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

View File

@@ -9,36 +9,35 @@
"version": "0.0.0", "version": "0.0.0",
"license": "CC0", "license": "CC0",
"dependencies": { "dependencies": {
"@actions/core": "^1.10.0", "@actions/core": "^1.6.0",
"@actions/github": "^5.1.1" "@actions/github": "^5.0.1"
} }
}, },
"node_modules/@actions/core": { "node_modules/@actions/core": {
"version": "1.10.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz", "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.6.0.tgz",
"integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==", "integrity": "sha512-NB1UAZomZlCV/LmJqkLhNTqtKfFXJZAUPcfl/zqG7EfsQdeUJtaWO98SGbuQ3pydJ3fHl2CvI/51OKYlCYYcaw==",
"dependencies": { "dependencies": {
"@actions/http-client": "^2.0.1", "@actions/http-client": "^1.0.11"
"uuid": "^8.3.2"
} }
}, },
"node_modules/@actions/github": { "node_modules/@actions/github": {
"version": "5.1.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-5.1.1.tgz", "resolved": "https://registry.npmjs.org/@actions/github/-/github-5.0.1.tgz",
"integrity": "sha512-Nk59rMDoJaV+mHCOJPXuvB1zIbomlKS0dmSIqPGxd0enAXBnOfn4VWF+CGtRCwXZG9Epa54tZA7VIRlJDS8A6g==", "integrity": "sha512-JZGyPM9ektb8NVTTI/2gfJ9DL7Rk98tQ7OVyTlgTuaQroariRBsOnzjy0I2EarX4xUZpK88YyO503fhmjFdyAg==",
"dependencies": { "dependencies": {
"@actions/http-client": "^2.0.1", "@actions/http-client": "^1.0.11",
"@octokit/core": "^3.6.0", "@octokit/core": "^3.6.0",
"@octokit/plugin-paginate-rest": "^2.17.0", "@octokit/plugin-paginate-rest": "^2.17.0",
"@octokit/plugin-rest-endpoint-methods": "^5.13.0" "@octokit/plugin-rest-endpoint-methods": "^5.13.0"
} }
}, },
"node_modules/@actions/http-client": { "node_modules/@actions/http-client": {
"version": "2.0.1", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", "integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
"dependencies": { "dependencies": {
"tunnel": "^0.0.6" "tunnel": "0.0.6"
} }
}, },
"node_modules/@octokit/auth-token": { "node_modules/@octokit/auth-token": {
@@ -205,14 +204,6 @@
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" "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": { "node_modules/webidl-conversions": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
@@ -235,31 +226,30 @@
}, },
"dependencies": { "dependencies": {
"@actions/core": { "@actions/core": {
"version": "1.10.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz", "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.6.0.tgz",
"integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==", "integrity": "sha512-NB1UAZomZlCV/LmJqkLhNTqtKfFXJZAUPcfl/zqG7EfsQdeUJtaWO98SGbuQ3pydJ3fHl2CvI/51OKYlCYYcaw==",
"requires": { "requires": {
"@actions/http-client": "^2.0.1", "@actions/http-client": "^1.0.11"
"uuid": "^8.3.2"
} }
}, },
"@actions/github": { "@actions/github": {
"version": "5.1.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-5.1.1.tgz", "resolved": "https://registry.npmjs.org/@actions/github/-/github-5.0.1.tgz",
"integrity": "sha512-Nk59rMDoJaV+mHCOJPXuvB1zIbomlKS0dmSIqPGxd0enAXBnOfn4VWF+CGtRCwXZG9Epa54tZA7VIRlJDS8A6g==", "integrity": "sha512-JZGyPM9ektb8NVTTI/2gfJ9DL7Rk98tQ7OVyTlgTuaQroariRBsOnzjy0I2EarX4xUZpK88YyO503fhmjFdyAg==",
"requires": { "requires": {
"@actions/http-client": "^2.0.1", "@actions/http-client": "^1.0.11",
"@octokit/core": "^3.6.0", "@octokit/core": "^3.6.0",
"@octokit/plugin-paginate-rest": "^2.17.0", "@octokit/plugin-paginate-rest": "^2.17.0",
"@octokit/plugin-rest-endpoint-methods": "^5.13.0" "@octokit/plugin-rest-endpoint-methods": "^5.13.0"
} }
}, },
"@actions/http-client": { "@actions/http-client": {
"version": "2.0.1", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", "integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
"requires": { "requires": {
"tunnel": "^0.0.6" "tunnel": "0.0.6"
} }
}, },
"@octokit/auth-token": { "@octokit/auth-token": {
@@ -403,11 +393,6 @@
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" "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": { "webidl-conversions": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",

View File

@@ -10,7 +10,7 @@
"author": "chris48s", "author": "chris48s",
"license": "CC0", "license": "CC0",
"dependencies": { "dependencies": {
"@actions/core": "^1.10.0", "@actions/core": "^1.6.0",
"@actions/github": "^5.1.1" "@actions/github": "^5.0.1"
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,6 @@ updates:
day: friday day: friday
time: '12:00' time: '12:00'
open-pull-requests-limit: 99 open-pull-requests-limit: 99
rebase-strategy: disabled
ignore: ignore:
# https://github.com/badges/shields/issues/7324 # https://github.com/badges/shields/issues/7324
# https://github.com/badges/shields/issues/7447 # https://github.com/badges/shields/issues/7447
@@ -28,7 +27,6 @@ updates:
day: friday day: friday
time: '12:00' time: '12:00'
open-pull-requests-limit: 99 open-pull-requests-limit: 99
rebase-strategy: disabled
# close-bot package dependencies # close-bot package dependencies
- package-ecosystem: npm - package-ecosystem: npm
@@ -38,12 +36,3 @@ updates:
day: friday day: friday
time: '12:00' time: '12:00'
open-pull-requests-limit: 99 open-pull-requests-limit: 99
rebase-strategy: disabled
# GH actions
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: weekly
open-pull-requests-limit: 99
rebase-strategy: disabled

View File

@@ -1,10 +0,0 @@
#!/bin/bash
set -euxo pipefail
apps=$(flyctl apps list --json | jq -r .[].ID | grep -E "pr-[0-9]+-badges-shields") || exit 0
for app in $apps
do
flyctl apps destroy "$app" -y
done

View File

@@ -1,35 +0,0 @@
#!/bin/bash
set -euxo pipefail
app="pr-$PR_NUMBER-badges-shields"
region="ewr"
org="shields-io"
# Get PR JSON from the API
# This will fail if $PR_NUMBER is not a valid PR
pr_json=$(curl --fail "https://api.github.com/repos/badges/shields/pulls/$PR_NUMBER")
# Attempt to apply the PR diff to the target branch
# This will fail if it does not merge cleanly
git config user.name "actions[bot]"
git config user.email "actions@users.noreply.github.com"
git fetch origin "pull/$PR_NUMBER/head:pr-$PR_NUMBER"
git merge "pr-$PR_NUMBER"
# If the app does not already exist, create it
if ! flyctl status --app "$app"; then
flyctl launch --no-deploy --copy-config --name "$app" --region "$region" --org "$org"
echo $SECRETS | tr " " "\n" | flyctl secrets import --app "$app"
fi
# Deploy
flyctl deploy --app "$app" --region "$region"
# Post a comment on the PR
app_url=$(flyctl status --app "$app" --json | jq -r .Hostname)
comment_url=$(echo "$pr_json" | jq .comments_url -r)
curl "$comment_url" \
-X POST \
-H "Authorization: token $GITHUB_TOKEN" \
--data "{\"body\":\"🚀 Updated review app: https://$app_url\"}"

View File

@@ -1,18 +1,16 @@
name: Auto close name: Auto close
on: on: pull_request_target
pull_request_target:
types: [opened]
permissions: permissions:
pull-requests: write pull-requests: write
jobs: jobs:
auto-close: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]' if: github.actor == 'dependabot[bot]'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Install action dependencies - name: Install action dependencies
run: cd .github/actions/close-bot && npm ci run: cd .github/actions/close-bot && npm ci

View File

@@ -3,25 +3,23 @@ on:
pull_request: pull_request:
jobs: jobs:
build-docker-image: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v1
with:
version: v0.9.1
- name: Set Git Short SHA - name: Set Git Short SHA
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
- name: Build - name: Build
uses: docker/build-push-action@v4 uses: docker/build-push-action@v2
with: with:
context: . context: .
push: false push: false
tags: ghcr.io/badges/shields:pr-validation tags: shieldsio/shields:pr-validation
build-args: | build-args: |
version=${{ env.SHORT_SHA }} version=${{ env.SHORT_SHA }}

View File

@@ -1,24 +0,0 @@
name: Cleanup Review Apps
on:
schedule:
- cron: '0 7 * * *'
# At 07:00, daily
workflow_dispatch:
jobs:
cleanup-review-apps:
runs-on: ubuntu-latest
environment: 'Review Apps'
steps:
- uses: actions/checkout@v3
- uses: superfly/flyctl-actions/setup-flyctl@master
- name: install jq
run: |
sudo apt-get -qq update
sudo apt-get install -y jq
- run: .github/scripts/cleanup-review-apps.sh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

View File

@@ -4,10 +4,6 @@ on:
pull_request: pull_request:
types: [closed] types: [closed]
permissions:
contents: write
packages: write
jobs: jobs:
create-release: create-release:
if: | if: |
@@ -24,7 +20,7 @@ jobs:
run: echo "::set-output name=date::$(date --rfc-3339=date)" run: echo "::set-output name=date::$(date --rfc-3339=date)"
- name: Checkout branch "master" - name: Checkout branch "master"
uses: actions/checkout@v3 uses: actions/checkout@v2
with: with:
ref: 'master' ref: 'master'
@@ -35,37 +31,19 @@ jobs:
tag: server-${{ steps.date.outputs.date }} tag: server-${{ steps.date.outputs.date }}
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v1
with:
version: v0.9.1
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v2 uses: docker/login-action@v1
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push snapshot release to DockerHub - name: Build and push snapshot release to DockerHub
uses: docker/build-push-action@v4 uses: docker/build-push-action@v2
with: with:
context: . context: .
push: true push: true
tags: shieldsio/shields:server-${{ steps.date.outputs.date }} tags: shieldsio/shields:server-${{ steps.date.outputs.date }}
build-args: | build-args: |
version=server-${{ steps.date.outputs.date }} version=server-${{ steps.date.outputs.date }}
- name: Login to GHCR
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push snapshot release to GHCR
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ghcr.io/badges/shields:server-${{ steps.date.outputs.date }}
build-args: |
version=server-${{ steps.date.outputs.date }}

View File

@@ -1,29 +0,0 @@
name: Danger
on:
pull_request_target:
types: [opened, 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 }}'

View File

@@ -3,30 +3,24 @@ on:
push: push:
branches: branches:
- master - master
permissions:
contents: write
jobs: jobs:
deploy-docs: build-and-deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v2.3.1
with: with:
persist-credentials: false persist-credentials: false
- name: Setup
uses: ./.github/actions/setup
with:
node-version: 16
- name: Build - name: Build
run: npm run build-docs run: |
npm ci
npm run build-docs
- name: Deploy - name: Deploy
uses: JamesIves/github-pages-deploy-action@v4 uses: JamesIves/github-pages-deploy-action@3.7.1
with: with:
branch: gh-pages GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
folder: api-docs BRANCH: gh-pages
clean: true FOLDER: api-docs
CLEAN: true

View File

@@ -1,43 +0,0 @@
name: Create/Update Review App
on:
workflow_dispatch:
inputs:
pr_number:
description: 'PR Number to deploy e.g: 1234'
required: true
permissions:
pull-requests: write
jobs:
deploy-review-app:
runs-on: ubuntu-latest
environment: 'Review Apps'
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: superfly/flyctl-actions/setup-flyctl@master
- name: install jq
run: |
sudo apt-get -qq update
sudo apt-get install -y jq
- run: .github/scripts/deploy-review-app.sh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
PR_NUMBER: ${{ github.event.inputs.pr_number }}
# credentials to set when we create the review app
SECRETS: |
GH_TOKEN=${{ secrets.GH_PAT }}
LIBRARIESIO_TOKENS=${{ secrets.SERVICETESTS_LIBRARIESIO_TOKENS }}
OBS_USER=${{ secrets.SERVICETESTS_OBS_USER }}
OBS_PASS=${{ secrets.SERVICETESTS_OBS_PASS }}
SL_INSIGHT_API_TOKEN=${{ secrets.SERVICETESTS_SL_INSIGHT_USER_UUID }}
SL_INSIGHT_USER_UUID=${{ 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 }}

View File

@@ -5,16 +5,12 @@ on:
# At 01:00 on the first day of every month # At 01:00 on the first day of every month
workflow_dispatch: workflow_dispatch:
permissions:
pull-requests: write
contents: write
jobs: jobs:
draft-release: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Draft Release - name: Draft Release
uses: ./.github/actions/draft-release uses: ./.github/actions/draft-release

View File

@@ -1,13 +1,14 @@
name: 'Dependency Review' name: 'Dependency Review'
on: on: [pull_request]
pull_request:
types: [opened, reopened, synchronize] permissions:
contents: read
jobs: jobs:
enforce-dependency-review: dependency-review:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 'Checkout Repository' - name: 'Checkout Repository'
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: 'Dependency Review' - name: 'Dependency Review'
uses: actions/dependency-review-action@v3 uses: actions/dependency-review-action@v1

View File

@@ -4,23 +4,18 @@ on:
branches: branches:
- master - master
permissions:
packages: write
jobs: jobs:
publish-docker-next: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v1
with:
version: v0.9.1
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v2 uses: docker/login-action@v1
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -28,27 +23,11 @@ jobs:
- name: Set Git Short SHA - name: Set Git Short SHA
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
- name: Build and push to DockerHub - name: Build and push
uses: docker/build-push-action@v4 uses: docker/build-push-action@v2
with: with:
context: . context: .
push: true push: true
tags: shieldsio/shields:next tags: shieldsio/shields:next
build-args: | build-args: |
version=${{ env.SHORT_SHA }} version=${{ env.SHORT_SHA }}
- name: Login to GHCR
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push to GHCR
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ghcr.io/badges/shields:next
build-args: |
version=${{ env.SHORT_SHA }}

View File

@@ -1,49 +0,0 @@
name: E2E
on:
pull_request:
types: [opened, 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: Run tests
env:
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
run: npm run e2e
- name: Archive videos
if: always()
uses: actions/upload-artifact@v3
with:
name: videos
path: cypress/videos
- name: Archive screenshots
if: always()
uses: actions/upload-artifact@v3
with:
name: screenshots
path: cypress/screenshots

View File

@@ -1,52 +0,0 @@
name: Integration@node 17
on:
pull_request:
types: [opened, reopened, synchronize]
push:
branches-ignore:
- 'gh-pages'
- 'dependabot/**'
jobs:
test-integration-17:
runs-on: ubuntu-latest
env:
PAT_EXISTS: ${{ secrets.GH_PAT != '' }}
services:
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 }}'

View File

@@ -1,50 +0,0 @@
name: Integration
on:
pull_request:
types: [opened, reopened, synchronize]
push:
branches-ignore:
- 'gh-pages'
- 'dependabot/**'
jobs:
test-integration:
runs-on: ubuntu-latest
env:
PAT_EXISTS: ${{ secrets.GH_PAT != '' }}
services:
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 }}'

View File

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

View File

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

View File

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

View File

@@ -1,46 +0,0 @@
name: Package CLI
on:
pull_request:
types: [opened, reopened, synchronize]
push:
branches-ignore:
- 'gh-pages'
- 'dependabot/**'
# Smoke test (render a badge with the CLI) with only the package
# dependencies installed.
jobs:
test-package-cli:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- node: '14'
engine-strict: 'false'
- node: '16'
engine-strict: 'false'
- node: '18'
engine-strict: 'true'
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Node JS ${{ inputs.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
- name: Install dependencies
env:
CYPRESS_INSTALL_BINARY: 0
NPM_CONFIG_ENGINE_STRICT: ${{ matrix.engine-strict }}
run: |
cd badge-maker
npm install
npm link
- name: Render a badge with the CLI
run: |
cd badge-maker
badge cactus grown :green @flat

View File

@@ -1,34 +0,0 @@
name: Package Library
on:
pull_request:
types: [opened, reopened, synchronize]
push:
branches-ignore:
- 'gh-pages'
- 'dependabot/**'
jobs:
test-package-lib:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- node: '14'
engine-strict: 'false'
- node: '16'
engine-strict: 'true'
- node: '18'
engine-strict: 'false'
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup
uses: ./.github/actions/setup
with:
node-version: ${{ matrix.node }}
env:
NPM_CONFIG_ENGINE_STRICT: ${{ matrix.engine-strict }}
- name: Package tests
uses: ./.github/actions/package-tests

View File

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

View File

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

View File

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

18
.gitignore vendored
View File

@@ -92,7 +92,10 @@ typings/
# Temporary build artifacts. # Temporary build artifacts.
/build /build
frontend/categories/*.yaml .next
badge-examples.json
supported-features.json
service-definitions.yml
# Local runtime configuration. # Local runtime configuration.
/config/local*.yml /config/local*.yml
@@ -100,6 +103,11 @@ frontend/categories/*.yaml
# Template for the local runtime configuration. # Template for the local runtime configuration.
!/config/local*.template.yml !/config/local*.template.yml
# Gatsby
/frontend/.cache
/frontend/public
/public
# Cypress # Cypress
/cypress/videos/ /cypress/videos/
/cypress/screenshots/ /cypress/screenshots/
@@ -109,11 +117,3 @@ frontend/categories/*.yaml
# Flamebearer # Flamebearer
flamegraph.html flamegraph.html
# config file for node-pg-migrate
migrations-config.json
# Frontend/Docusaurus
frontend/.docusaurus
frontend/.cache-loader
/public

5
.mocharc-frontend.yml Normal file
View File

@@ -0,0 +1,5 @@
reporter: mocha-env-reporter
require:
- '@babel/polyfill'
- '@babel/register'
- mocha-yaml-loader

2
.npmrc
View File

@@ -1,2 +0,0 @@
engine-strict=true
strict-peer-deps=true

10
.nycrc-frontend.json Normal file
View File

@@ -0,0 +1,10 @@
{
"reporter": ["lcov"],
"all": false,
"silent": true,
"clean": false,
"sourceMap": false,
"instrument": false,
"include": ["frontend/**/*.js"],
"exclude": ["**/*.spec.js", "**/mocha-*.js"]
}

View File

@@ -10,5 +10,5 @@ public
private/*.json private/*.json
/.nyc_output /.nyc_output
analytics.json analytics.json
frontend/.docusaurus supported-features.json
frontend/categories service-definitions.yml

View File

@@ -4,183 +4,6 @@ Note: this changelog is for the shields.io server. The changelog for the badge-m
--- ---
## server-2023-06-01
- feat: Add total commits to [GitHubCommitActivity] [#9196](https://github.com/badges/shields/issues/9196)
- set a custom error on 429 [#9159](https://github.com/badges/shields/issues/9159)
- deprecate [travis].org badges [#9171](https://github.com/badges/shields/issues/9171)
- count private sponsors on [GithubSponsors] badge [#9170](https://github.com/badges/shields/issues/9170)
- Dependency updates
## server-2023-05-01
** Removal:** For users who need to maintain a Github Token pool, storage has been provided via the `RedisTokenPersistence` and `REDIS_URL` settings. This feature was deprecated in `server-2023-03-01`. As of this release, the `RedisTokenPersistence` backend is now removed. 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)
- fail to start server if there are duplicate service names [#9099](https://github.com/badges/shields/issues/9099)
- [SourceForge] Added badges for SourceForge [#9078](https://github.com/badges/shields/issues/9078) [#9102](https://github.com/badges/shields/issues/9102)
- crates: Use `?include=` to reduce crates.io backend load [#9081](https://github.com/badges/shields/issues/9081)
- Dependency updates
## server-2023-04-02
- [JenkinsCoverage] Update Jenkins Code Coverage API for new plugin version [#9010](https://github.com/badges/shields/issues/9010)
- [CTAN] fallback to date if version is empty [#9036](https://github.com/badges/shields/issues/9036)
- Update to [CTAN] API version 2.0 [#9016](https://github.com/badges/shields/issues/9016)
- handle missing statistics array in [VisualStudioMarketplace] badges [#8985](https://github.com/badges/shields/issues/8985)
- [Netlify] upgrade colors for SVG parsing [#8971](https://github.com/badges/shields/issues/8971)
- Fix [Vcpkg] version service for different version fields [#8945](https://github.com/badges/shields/issues/8945)
- only try to close pool if one exists [#8947](https://github.com/badges/shields/issues/8947)
- misc minor fixes to [githubsize node pypi] [#8946](https://github.com/badges/shields/issues/8946)
- Dependency updates
## 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 ## server-2022-04-03
- Breaking change: This release updates ioredis from v4 to v5. - Breaking change: This release updates ioredis from v4 to v5.

View File

@@ -134,19 +134,9 @@ 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 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. When changing other code, please add unit tests.
The integration tests are not run by default. For most contributions it is OK to skip these unless you're working directly on the code for storing the GitHub token pool in postgres. 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
To run the integration tests: start the server automatically.
- You must have PostgreSQL installed. Use `brew install postgresql`, `apt-get install postgresql`, etc.
- Set a connection string either with an env var `POSTGRES_URL=postgresql://user:pass@127.0.0.1:5432/db_name` or by using
```yaml
private:
postgres_url: 'postgresql://user:pass@127.0.0.1:5432/db_name'
```
in a yaml config file.
- Run `npm run migrate up` to apply DB migrations
- Run `npm run test:integration` to run the tests
[service-tests]: https://github.com/badges/shields/blob/master/doc/service-tests.md [service-tests]: https://github.com/badges/shields/blob/master/doc/service-tests.md

View File

@@ -9,7 +9,7 @@ COPY package.json package-lock.json /usr/src/app/
COPY badge-maker /usr/src/app/badge-maker/ COPY badge-maker /usr/src/app/badge-maker/
RUN apk add python3 make g++ 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. # 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 RUN NODE_ENV=development CYPRESS_INSTALL_BINARY=0 npm ci
@@ -30,8 +30,8 @@ LABEL fly.version=$version
ENV NODE_ENV production ENV NODE_ENV production
WORKDIR /usr/src/app 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 CMD node server
EXPOSE 80 443 EXPOSE 80

View File

@@ -19,6 +19,9 @@
<a href="https://coveralls.io/github/badges/shields"> <a href="https://coveralls.io/github/badges/shields">
<img src="https://img.shields.io/coveralls/github/badges/shields" <img src="https://img.shields.io/coveralls/github/badges/shields"
alt="coverage"></a> 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"> <a href="https://discord.gg/HjJCwm5">
<img src="https://img.shields.io/discord/308323056592486420?logo=discord" <img src="https://img.shields.io/discord/308323056592486420?logo=discord"
alt="chat on Discord"></a> alt="chat on Discord"></a>
@@ -107,7 +110,7 @@ You can read a [tutorial on how to add a badge][tutorial].
When server source files change, the badge server should automatically restart When server source files change, the badge server should automatically restart
itself (using [nodemon][]). When the frontend files change, the frontend dev itself (using [nodemon][]). When the frontend files change, the frontend dev
server (`docusaurus start`) should also automatically reload. However the badge server (`gatsby dev`) should also automatically reload. However the badge
definitions are built only before the server first starts. To regenerate those, definitions are built only before the server first starts. To regenerate those,
either run `npm run defs` or manually restart the server. either run `npm run defs` or manually restart the server.

View File

@@ -35,16 +35,6 @@
"WEBLATE_API_KEY": { "WEBLATE_API_KEY": {
"description": "Configure the API key to be used for the Weblate service.", "description": "Configure the API key to be used for the Weblate service.",
"required": false "required": false
},
"METRICS_INFLUX_ENABLED": {
"description": "Disable influx metrics",
"value": "false",
"required": false
},
"REQUIRE_CLOUDFLARE": {
"description": "Allow direct traffic",
"value": "false",
"required": false
} }
}, },
"formation": { "formation": {

View File

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

View File

@@ -1,6 +1,7 @@
'use strict' 'use strict'
const path = require('path') const path = require('path')
const isSvg = require('is-svg')
const { spawn } = require('child-process-promise') const { spawn } = require('child-process-promise')
const { expect, use } = require('chai') const { expect, use } = require('chai')
use(require('chai-string')) use(require('chai-string'))
@@ -19,7 +20,6 @@ describe('The CLI', function () {
}) })
it('should produce default badges', async function () { it('should produce default badges', async function () {
const { default: isSvg } = await import('is-svg')
const { stdout } = await runCli(['cactus', 'grown']) const { stdout } = await runCli(['cactus', 'grown'])
expect(stdout) expect(stdout)
.to.satisfy(isSvg) .to.satisfy(isSvg)
@@ -28,13 +28,11 @@ describe('The CLI', function () {
}) })
it('should produce colorschemed badges', async function () { it('should produce colorschemed badges', async function () {
const { default: isSvg } = await import('is-svg')
const { stdout } = await runCli(['cactus', 'grown', ':green']) const { stdout } = await runCli(['cactus', 'grown', ':green'])
expect(stdout).to.satisfy(isSvg) expect(stdout).to.satisfy(isSvg)
}) })
it('should produce right-color badges', async function () { it('should produce right-color badges', async function () {
const { default: isSvg } = await import('is-svg')
const { stdout } = await runCli(['cactus', 'grown', '#abcdef']) const { stdout } = await runCli(['cactus', 'grown', '#abcdef'])
expect(stdout).to.satisfy(isSvg).and.to.include('#abcdef') expect(stdout).to.satisfy(isSvg).and.to.include('#abcdef')
}) })

View File

@@ -1,11 +1,11 @@
'use strict' 'use strict'
const { expect } = require('chai') const { expect } = require('chai')
const isSvg = require('is-svg')
const { makeBadge, ValidationError } = require('.') const { makeBadge, ValidationError } = require('.')
describe('makeBadge function', function () { describe('makeBadge function', function () {
it('should produce badge with valid input', async function () { it('should produce badge with valid input', function () {
const { default: isSvg } = await import('is-svg')
expect( expect(
makeBadge({ makeBadge({
label: 'build', label: 'build',

View File

@@ -1,6 +1,6 @@
'use strict' 'use strict'
const { normalizeColor, toSvgColor } = require('./color') const { toSvgColor } = require('./color')
const badgeRenderers = require('./badge-renderers') const badgeRenderers = require('./badge-renderers')
const { stripXmlWhitespace } = require('./xml') 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 it is likely this will impact on the package's public interface in index.js
*/ */
module.exports = function makeBadge({ module.exports = function makeBadge({
format,
style = 'flat', style = 'flat',
label, label,
message, message,
@@ -24,22 +23,6 @@ module.exports = function makeBadge({
label = `${label}`.trim() label = `${label}`.trim()
message = `${message}`.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] const render = badgeRenderers[style]
if (!render) { if (!render) {
throw new Error(`Unknown badge style: '${style}'`) throw new Error(`Unknown badge style: '${style}'`)

View File

@@ -1,144 +1,48 @@
'use strict' 'use strict'
const { test, given, forCases } = require('sazerac')
const { expect } = require('chai') const { expect } = require('chai')
const snapshot = require('snap-shot-it') const snapshot = require('snap-shot-it')
const isSvg = require('is-svg')
const prettier = require('prettier') const prettier = require('prettier')
const makeBadge = require('./make-badge') const makeBadge = require('./make-badge')
function expectBadgeToMatchSnapshot(format) { function expectBadgeToMatchSnapshot(badgeData) {
snapshot(prettier.format(makeBadge(format), { parser: 'html' })) snapshot(prettier.format(makeBadge(badgeData), { parser: 'html' }))
}
function testColor(color = '', colorAttr = 'color') {
return JSON.parse(
makeBadge({
label: 'name',
message: 'Bob',
[colorAttr]: color,
format: 'json',
})
).color
} }
describe('The badge generator', function () { 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 () { describe('SVG', function () {
it('should produce SVG', async function () { it('should produce SVG', function () {
const { default: isSvg } = await import('is-svg') expect(makeBadge({ label: 'cactus', message: 'grown' }))
expect(makeBadge({ label: 'cactus', message: 'grown', format: 'svg' }))
.to.satisfy(isSvg) .to.satisfy(isSvg)
.and.to.include('cactus') .and.to.include('cactus')
.and.to.include('grown') .and.to.include('grown')
}) })
it('should match snapshot', function () { it('should match snapshot', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({ label: 'cactus', message: 'grown' })
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/'],
})
}) })
it('should replace undefined svg badge style with "flat"', async function () { it('should replace undefined svg badge style with "flat"', function () {
const { default: isSvg } = await import('is-svg') expect(
const jsonBadgeWithUnknownStyle = makeBadge({ makeBadge({
label: 'name', label: 'name',
message: 'Bob', message: 'Bob',
format: 'svg', })
}) )
const jsonBadgeWithDefaultStyle = makeBadge({ .to.satisfy(isSvg)
label: 'name', .and.to.equal(
message: 'Bob', makeBadge({
format: 'svg', label: 'name',
style: 'flat', message: 'Bob',
}) style: 'flat',
expect(jsonBadgeWithUnknownStyle) })
.to.equal(jsonBadgeWithDefaultStyle) )
.and.to.satisfy(isSvg)
}) })
it('should fail with unknown svg badge style', function () { it('should fail with unknown svg badge style', function () {
expect(() => expect(() =>
makeBadge({ makeBadge({ label: 'name', message: 'Bob', style: 'unknown_style' })
label: 'name',
message: 'Bob',
format: 'svg',
style: 'unknown_style',
})
).to.throw(Error, "Unknown badge style: 'unknown_style'") ).to.throw(Error, "Unknown badge style: 'unknown_style'")
}) })
}) })
@@ -148,7 +52,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: 'cactus', label: 'cactus',
message: 'grown', message: 'grown',
format: 'svg',
style: 'flat', style: 'flat',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
@@ -159,7 +62,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: 'cactus', label: 'cactus',
message: 'grown', message: 'grown',
format: 'svg',
style: 'flat', style: 'flat',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
@@ -171,7 +73,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: '', label: '',
message: 'grown', message: 'grown',
format: 'svg',
style: 'flat', style: 'flat',
color: '#b3e', color: '#b3e',
}) })
@@ -181,7 +82,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: '', label: '',
message: 'grown', message: 'grown',
format: 'svg',
style: 'flat', style: 'flat',
color: '#b3e', color: '#b3e',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu', logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
@@ -192,7 +92,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: '', label: '',
message: 'grown', message: 'grown',
format: 'svg',
style: 'flat', style: 'flat',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
@@ -204,7 +103,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: 'cactus', label: 'cactus',
message: 'grown', message: 'grown',
format: 'svg',
style: 'flat', style: 'flat',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
@@ -216,7 +114,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: 'cactus', label: 'cactus',
message: 'grown', message: 'grown',
format: 'svg',
style: 'flat', style: 'flat',
color: '#000', color: '#000',
labelColor: '#f3f3f3', labelColor: '#f3f3f3',
@@ -227,7 +124,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: 'cactus', label: 'cactus',
message: 'grown', message: 'grown',
format: 'svg',
style: 'flat', style: 'flat',
color: '#e2ffe1', color: '#e2ffe1',
labelColor: '#000', labelColor: '#000',
@@ -240,7 +136,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: 'cactus', label: 'cactus',
message: 'grown', message: 'grown',
format: 'svg',
style: 'flat-square', style: 'flat-square',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
@@ -251,7 +146,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: 'cactus', label: 'cactus',
message: 'grown', message: 'grown',
format: 'svg',
style: 'flat-square', style: 'flat-square',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
@@ -263,7 +157,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: '', label: '',
message: 'grown', message: 'grown',
format: 'svg',
style: 'flat-square', style: 'flat-square',
color: '#b3e', color: '#b3e',
}) })
@@ -273,7 +166,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: '', label: '',
message: 'grown', message: 'grown',
format: 'svg',
style: 'flat-square', style: 'flat-square',
color: '#b3e', color: '#b3e',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu', logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
@@ -284,7 +176,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: '', label: '',
message: 'grown', message: 'grown',
format: 'svg',
style: 'flat-square', style: 'flat-square',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
@@ -296,7 +187,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: 'cactus', label: 'cactus',
message: 'grown', message: 'grown',
format: 'svg',
style: 'flat-square', style: 'flat-square',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
@@ -308,7 +198,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: 'cactus', label: 'cactus',
message: 'grown', message: 'grown',
format: 'svg',
style: 'flat-square', style: 'flat-square',
color: '#000', color: '#000',
labelColor: '#f3f3f3', labelColor: '#f3f3f3',
@@ -319,7 +208,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: 'cactus', label: 'cactus',
message: 'grown', message: 'grown',
format: 'svg',
style: 'flat-square', style: 'flat-square',
color: '#e2ffe1', color: '#e2ffe1',
labelColor: '#000', labelColor: '#000',
@@ -332,7 +220,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: 'cactus', label: 'cactus',
message: 'grown', message: 'grown',
format: 'svg',
style: 'plastic', style: 'plastic',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
@@ -343,7 +230,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: 'cactus', label: 'cactus',
message: 'grown', message: 'grown',
format: 'svg',
style: 'plastic', style: 'plastic',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
@@ -355,7 +241,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: '', label: '',
message: 'grown', message: 'grown',
format: 'svg',
style: 'plastic', style: 'plastic',
color: '#b3e', color: '#b3e',
}) })
@@ -365,7 +250,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: '', label: '',
message: 'grown', message: 'grown',
format: 'svg',
style: 'plastic', style: 'plastic',
color: '#b3e', color: '#b3e',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu', logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
@@ -376,7 +260,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: '', label: '',
message: 'grown', message: 'grown',
format: 'svg',
style: 'plastic', style: 'plastic',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
@@ -388,7 +271,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: 'cactus', label: 'cactus',
message: 'grown', message: 'grown',
format: 'svg',
style: 'plastic', style: 'plastic',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
@@ -400,7 +282,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: 'cactus', label: 'cactus',
message: 'grown', message: 'grown',
format: 'svg',
style: 'plastic', style: 'plastic',
color: '#000', color: '#000',
labelColor: '#f3f3f3', labelColor: '#f3f3f3',
@@ -411,7 +292,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: 'cactus', label: 'cactus',
message: 'grown', message: 'grown',
format: 'svg',
style: 'plastic', style: 'plastic',
color: '#e2ffe1', color: '#e2ffe1',
labelColor: '#000', labelColor: '#000',
@@ -426,7 +306,6 @@ describe('The badge generator', function () {
makeBadge({ makeBadge({
label: 1998, label: 1998,
message: 1999, message: 1999,
format: 'svg',
style: 'for-the-badge', style: 'for-the-badge',
}) })
) )
@@ -439,7 +318,6 @@ describe('The badge generator', function () {
makeBadge({ makeBadge({
label: 'Label', label: 'Label',
message: '1 string', message: '1 string',
format: 'svg',
style: 'for-the-badge', style: 'for-the-badge',
}) })
) )
@@ -451,7 +329,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: 'cactus', label: 'cactus',
message: 'grown', message: 'grown',
format: 'svg',
style: 'for-the-badge', style: 'for-the-badge',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
@@ -462,7 +339,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: 'cactus', label: 'cactus',
message: 'grown', message: 'grown',
format: 'svg',
style: 'for-the-badge', style: 'for-the-badge',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
@@ -474,7 +350,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: '', label: '',
message: 'grown', message: 'grown',
format: 'svg',
style: 'for-the-badge', style: 'for-the-badge',
color: '#b3e', color: '#b3e',
}) })
@@ -484,7 +359,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: '', label: '',
message: 'grown', message: 'grown',
format: 'svg',
style: 'for-the-badge', style: 'for-the-badge',
color: '#b3e', color: '#b3e',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu', logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
@@ -495,7 +369,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: '', label: '',
message: 'grown', message: 'grown',
format: 'svg',
style: 'for-the-badge', style: 'for-the-badge',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
@@ -507,7 +380,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: 'cactus', label: 'cactus',
message: 'grown', message: 'grown',
format: 'svg',
style: 'for-the-badge', style: 'for-the-badge',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
@@ -519,7 +391,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: 'cactus', label: 'cactus',
message: 'grown', message: 'grown',
format: 'svg',
style: 'for-the-badge', style: 'for-the-badge',
color: '#000', color: '#000',
labelColor: '#f3f3f3', labelColor: '#f3f3f3',
@@ -530,7 +401,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: 'cactus', label: 'cactus',
message: 'grown', message: 'grown',
format: 'svg',
style: 'for-the-badge', style: 'for-the-badge',
color: '#e2ffe1', color: '#e2ffe1',
labelColor: '#000', labelColor: '#000',
@@ -544,7 +414,6 @@ describe('The badge generator', function () {
makeBadge({ makeBadge({
label: 'some-key', label: 'some-key',
message: 'some-value', message: 'some-value',
format: 'svg',
style: 'social', style: 'social',
}) })
) )
@@ -558,11 +427,10 @@ describe('The badge generator', function () {
makeBadge({ makeBadge({
label: '', label: '',
message: 'some-value', message: 'some-value',
format: 'json',
style: 'social', style: 'social',
}) })
) )
.to.include('""') .to.include('></text>')
.and.to.include('some-value') .and.to.include('some-value')
}) })
@@ -570,7 +438,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: 'cactus', label: 'cactus',
message: 'grown', message: 'grown',
format: 'svg',
style: 'social', style: 'social',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
@@ -581,7 +448,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: 'cactus', label: 'cactus',
message: 'grown', message: 'grown',
format: 'svg',
style: 'social', style: 'social',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
@@ -593,7 +459,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: '', label: '',
message: 'grown', message: 'grown',
format: 'svg',
style: 'social', style: 'social',
color: '#b3e', color: '#b3e',
}) })
@@ -603,7 +468,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: '', label: '',
message: 'grown', message: 'grown',
format: 'svg',
style: 'social', style: 'social',
color: '#b3e', color: '#b3e',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu', logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
@@ -614,7 +478,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: '', label: '',
message: 'grown', message: 'grown',
format: 'svg',
style: 'social', style: 'social',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
@@ -626,7 +489,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: 'cactus', label: 'cactus',
message: 'grown', message: 'grown',
format: 'svg',
style: 'social', style: 'social',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
@@ -640,7 +502,6 @@ describe('The badge generator', function () {
expectBadgeToMatchSnapshot({ expectBadgeToMatchSnapshot({
label: 'label', label: 'label',
message: 'message', message: 'message',
format: 'svg',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu', logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
}) })
}) })

View File

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

View File

@@ -94,12 +94,10 @@ private:
obs_user: 'OBS_USER' obs_user: 'OBS_USER'
obs_pass: 'OBS_PASS' obs_pass: 'OBS_PASS'
redis_url: 'REDIS_URL' redis_url: 'REDIS_URL'
postgres_url: 'POSTGRES_URL'
sentry_dsn: 'SENTRY_DSN' sentry_dsn: 'SENTRY_DSN'
sl_insight_userUuid: 'SL_INSIGHT_USER_UUID' sl_insight_userUuid: 'SL_INSIGHT_USER_UUID'
sl_insight_apiToken: 'SL_INSIGHT_API_TOKEN' sl_insight_apiToken: 'SL_INSIGHT_API_TOKEN'
sonarqube_token: 'SONARQUBE_TOKEN' sonarqube_token: 'SONARQUBE_TOKEN'
stackapps_api_key: 'STACKAPPS_API_KEY'
teamcity_user: 'TEAMCITY_USER' teamcity_user: 'TEAMCITY_USER'
teamcity_pass: 'TEAMCITY_PASS' teamcity_pass: 'TEAMCITY_PASS'
twitch_client_id: 'TWITCH_CLIENT_ID' twitch_client_id: 'TWITCH_CLIENT_ID'

View File

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

View File

@@ -4,6 +4,7 @@ private:
gh_client_id: ... gh_client_id: ...
gh_client_secret: ... gh_client_secret: ...
gitlab_token: ... gitlab_token: ...
redis_url: ...
sentry_dsn: ... sentry_dsn: ...
shields_secret: ... shields_secret: ...
sl_insight_userUuid: ... sl_insight_userUuid: ...

View File

@@ -6,20 +6,13 @@ public:
enabled: true enabled: true
url: https://metrics.shields.io/telegraf url: https://metrics.shields.io/telegraf
instanceIdFrom: env-var instanceIdFrom: env-var
instanceIdEnvVarName: FLY_ALLOC_ID instanceIdEnvVarName: HEROKU_DYNO_ID
envLabel: shields-production envLabel: shields-production
ssl: ssl:
isSecure: false isSecure: true
cors: cors:
allowedOrigin: ['http://shields.io', 'https://shields.io'] allowedOrigin: ['http://shields.io', 'https://shields.io']
services:
gitlab:
authorizedOrigins: 'https://gitlab.com'
rasterUrl: 'https://raster.shields.io' rasterUrl: 'https://raster.shields.io'
userAgentBase: 'Shields.io'
requireCloudflare: true
requestTimeoutSeconds: 20

112
core/badge-urls/make-badge-url.d.ts vendored Normal file
View File

@@ -0,0 +1,112 @@
export function badgeUrlFromPath({
baseUrl,
path,
queryParams,
style,
format,
longCache,
}: {
baseUrl?: string
path: string
queryParams: { [k: string]: string | number | boolean }
style?: string
format?: string
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({
baseUrl,
label,
message,
labelColor,
color,
style,
namedLogo,
format,
links,
}: {
baseUrl?: string
label: string
message: string
labelColor?: string
color?: string
style?: string
namedLogo?: string
format?: string
links?: string[]
}): string
export function queryStringStaticBadgeUrl({
baseUrl,
label,
message,
color,
labelColor,
style,
namedLogo,
logoColor,
logoWidth,
logoPosition,
format,
}: {
baseUrl?: string
label: string
message: string
color?: string
labelColor?: string
style?: string
namedLogo?: string
logoColor?: string
logoWidth?: number
logoPosition?: number
format?: string
}): string
export function dynamicBadgeUrl({
baseUrl,
datatype,
label,
dataUrl,
query,
prefix,
suffix,
color,
style,
format,
}: {
baseUrl?: string
datatype: string
label: string
dataUrl: string
query: string
prefix: string
suffix: string
color?: string
style?: string
format?: string
}): string
export function rasterRedirectUrl(
{ rasterUrl }: { rasterUrl: string },
badgeUrl: string
): string

View File

@@ -1,5 +1,147 @@
// Avoid "Attempted import error: 'URL' is not exported from 'url'" in frontend. // Avoid "Attempted import error: 'URL' is not exported from 'url'" in frontend.
import url from 'url' import url from 'url'
import queryString from 'query-string'
import { compile } from 'path-to-regexp'
function badgeUrlFromPath({
baseUrl = '',
path,
queryParams,
style,
format = '',
longCache = false,
}) {
const outExt = format.length ? `.${format}` : ''
const outQueryString = queryString.stringify({
cacheSeconds: longCache ? '2592000' : undefined,
style,
...queryParams,
})
const suffix = outQueryString ? `?${outQueryString}` : ''
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, '__'))
}
function staticBadgeUrl({
baseUrl = '',
label,
message,
labelColor,
color = 'lightgray',
style,
namedLogo,
format = '',
links = [],
}) {
const path = [label, message, color].map(encodeField).join('-')
const outQueryString = queryString.stringify({
labelColor,
style,
logo: namedLogo,
link: links,
})
const outExt = format.length ? `.${format}` : ''
const suffix = outQueryString ? `?${outQueryString}` : ''
return `${baseUrl}/badge/${path}${outExt}${suffix}`
}
function queryStringStaticBadgeUrl({
baseUrl = '',
label,
message,
color,
labelColor,
style,
namedLogo,
logoColor,
logoWidth,
logoPosition,
format = '',
}) {
// schemaVersion could be a parameter if we iterate on it,
// for now it's hardcoded to the only supported version.
const schemaVersion = '1'
const suffix = `?${queryString.stringify({
label,
message,
color,
labelColor,
style,
logo: namedLogo,
logoColor,
logoWidth,
logoPosition,
})}`
const outExt = format.length ? `.${format}` : ''
return `${baseUrl}/static/v${schemaVersion}${outExt}${suffix}`
}
function dynamicBadgeUrl({
baseUrl,
datatype,
label,
dataUrl,
query,
prefix,
suffix,
color,
style,
format = '',
}) {
const outExt = format.length ? `.${format}` : ''
const queryParams = {
label,
url: dataUrl,
query,
style,
}
if (color) {
queryParams.color = color
}
if (prefix) {
queryParams.prefix = prefix
}
if (suffix) {
queryParams.suffix = suffix
}
const outQueryString = queryString.stringify(queryParams)
return `${baseUrl}/badge/dynamic/${datatype}${outExt}?${outQueryString}`
}
function rasterRedirectUrl({ rasterUrl }, badgeUrl) { function rasterRedirectUrl({ rasterUrl }, badgeUrl) {
// Ensure we're always using the `rasterUrl` by using just the path from // Ensure we're always using the `rasterUrl` by using just the path from
@@ -10,4 +152,12 @@ function rasterRedirectUrl({ rasterUrl }, badgeUrl) {
return result return result
} }
export { rasterRedirectUrl } export {
badgeUrlFromPath,
badgeUrlFromPattern,
encodeField,
staticBadgeUrl,
queryStringStaticBadgeUrl,
dynamicBadgeUrl,
rasterRedirectUrl,
}

View File

@@ -0,0 +1,155 @@
import { test, given } from 'sazerac'
import {
badgeUrlFromPath,
badgeUrlFromPattern,
encodeField,
staticBadgeUrl,
queryStringStaticBadgeUrl,
dynamicBadgeUrl,
} from './make-badge-url.js'
describe('Badge URL generation functions', function () {
test(badgeUrlFromPath, () => {
given({
baseUrl: 'http://example.com',
path: '/npm/v/gh-badges',
style: 'flat-square',
longCache: true,
}).expect(
'http://example.com/npm/v/gh-badges?cacheSeconds=2592000&style=flat-square'
)
})
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('')
given('happy go lucky').expect('happy%20go%20lucky')
given('do-right').expect('do--right')
given('it_is_a_snake').expect('it__is__a__snake')
})
test(staticBadgeUrl, () => {
given({
label: 'foo',
message: 'bar',
color: 'blue',
style: 'flat-square',
}).expect('/badge/foo-bar-blue?style=flat-square')
given({
label: 'foo',
message: 'bar',
color: 'blue',
style: 'flat-square',
format: 'png',
namedLogo: 'github',
}).expect('/badge/foo-bar-blue.png?logo=github&style=flat-square')
given({
label: 'Hello World',
message: 'Привет Мир',
color: '#aabbcc',
}).expect(
'/badge/Hello%20World-%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80-%23aabbcc'
)
given({
label: '123-123',
message: 'abc-abc',
color: 'blue',
}).expect('/badge/123--123-abc--abc-blue')
given({
label: '123-123',
message: '',
color: 'blue',
style: 'social',
}).expect('/badge/123--123--blue?style=social')
given({
label: '',
message: 'blue',
color: 'blue',
}).expect('/badge/-blue-blue')
})
test(queryStringStaticBadgeUrl, () => {
// the query-string library sorts parameters by name
given({
label: 'foo',
message: 'bar',
color: 'blue',
style: 'flat-square',
}).expect('/static/v1?color=blue&label=foo&message=bar&style=flat-square')
given({
label: 'foo Bar',
message: 'bar Baz',
color: 'blue',
style: 'flat-square',
format: 'png',
namedLogo: 'github',
}).expect(
'/static/v1.png?color=blue&label=foo%20Bar&logo=github&message=bar%20Baz&style=flat-square'
)
given({
label: 'Hello World',
message: 'Привет Мир',
color: '#aabbcc',
}).expect(
'/static/v1?color=%23aabbcc&label=Hello%20World&message=%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80'
)
})
test(dynamicBadgeUrl, () => {
const dataUrl = 'http://example.com/foo.json'
const query = '$.bar'
const prefix = 'value: '
given({
baseUrl: 'http://img.example.com',
datatype: 'json',
label: 'foo',
dataUrl,
query,
prefix,
style: 'plastic',
}).expect(
[
'http://img.example.com/badge/dynamic/json',
'?label=foo',
`&prefix=${encodeURIComponent(prefix)}`,
`&query=${encodeURIComponent(query)}`,
'&style=plastic',
`&url=${encodeURIComponent(dataUrl)}`,
].join('')
)
const suffix = '<- value'
const color = 'blue'
given({
baseUrl: 'http://img.example.com',
datatype: 'json',
label: 'foo',
dataUrl,
query,
suffix,
color,
style: 'plastic',
}).expect(
[
'http://img.example.com/badge/dynamic/json',
'?color=blue',
'&label=foo',
`&query=${encodeURIComponent(query)}`,
'&style=plastic',
`&suffix=${encodeURIComponent(suffix)}`,
`&url=${encodeURIComponent(dataUrl)}`,
].join('')
)
})
})

View File

@@ -44,12 +44,6 @@ class BaseGraphqlService extends BaseService {
* and custom error messages e.g: `{ 404: 'package not found' }`. * and custom error messages e.g: `{ 404: 'package not found' }`.
* This can be used to extend or override the * This can be used to extend or override the
* [default](https://github.com/badges/shields/blob/master/core/base-service/check-error-response.js#L5) * [default](https://github.com/badges/shields/blob/master/core/base-service/check-error-response.js#L5)
* @param {object} [attrs.systemErrors={}] Key-value map of got network exception codes
* and an object of params to pass when we construct an Inaccessible exception object
* e.g: `{ ECONNRESET: { prettyMessage: 'connection reset' } }`.
* See {@link https://github.com/sindresorhus/got/blob/main/documentation/7-retry.md#errorcodes got error codes}
* for allowed keys
* and {@link module:core/base-service/errors~RuntimeErrorProps} for allowed values
* @param {Function} [attrs.transformJson=data => data] Function which takes the raw json and transforms it before * @param {Function} [attrs.transformJson=data => data] Function which takes the raw json and transforms it before
* further procesing. In case of multiple query in a single graphql call and few of them * further procesing. In case of multiple query in a single graphql call and few of them
* throw error, partial data might be used ignoring the error. * throw error, partial data might be used ignoring the error.
@@ -68,7 +62,6 @@ class BaseGraphqlService extends BaseService {
variables = {}, variables = {},
options = {}, options = {},
httpErrorMessages = {}, httpErrorMessages = {},
systemErrors = {},
transformJson = data => data, transformJson = data => data,
transformErrors = defaultTransformErrors, transformErrors = defaultTransformErrors,
}) { }) {
@@ -81,8 +74,7 @@ class BaseGraphqlService extends BaseService {
const { buffer } = await this._request({ const { buffer } = await this._request({
url, url,
options: mergedOptions, options: mergedOptions,
httpErrors: httpErrorMessages, errorMessages: httpErrorMessages,
systemErrors,
}) })
const json = transformJson(this._parseJson(buffer)) const json = transformJson(this._parseJson(buffer))
if (json.errors) { if (json.errors) {

View File

@@ -30,26 +30,14 @@ class BaseJsonService extends BaseService {
* @param {string} attrs.url URL to request * @param {string} attrs.url URL to request
* @param {object} [attrs.options={}] Options to pass to got. See * @param {object} [attrs.options={}] Options to pass to got. See
* [documentation](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md) * [documentation](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md)
* @param {object} [attrs.httpErrors={}] Key-value map of status codes * @param {object} [attrs.errorMessages={}] Key-value map of status codes
* and custom error messages e.g: `{ 404: 'package not found' }`. * and custom error messages e.g: `{ 404: 'package not found' }`.
* This can be used to extend or override the * This can be used to extend or override the
* [default](https://github.com/badges/shields/blob/master/core/base-service/check-error-response.js#L5) * [default](https://github.com/badges/shields/blob/master/core/base-service/check-error-response.js#L5)
* @param {object} [attrs.systemErrors={}] Key-value map of got network exception codes
* and an object of params to pass when we construct an Inaccessible exception object
* e.g: `{ ECONNRESET: { prettyMessage: 'connection reset' } }`.
* See {@link https://github.com/sindresorhus/got/blob/main/documentation/7-retry.md#errorcodes got error codes}
* for allowed keys
* and {@link module:core/base-service/errors~RuntimeErrorProps} for allowed values
* @returns {object} Parsed response * @returns {object} Parsed response
* @see https://github.com/sindresorhus/got/blob/main/documentation/2-options.md * @see https://github.com/sindresorhus/got/blob/main/documentation/2-options.md
*/ */
async _requestJson({ async _requestJson({ schema, url, options = {}, errorMessages = {} }) {
schema,
url,
options = {},
httpErrors = {},
systemErrors = {},
}) {
const mergedOptions = { const mergedOptions = {
...{ headers: { Accept: 'application/json' } }, ...{ headers: { Accept: 'application/json' } },
...options, ...options,
@@ -57,8 +45,7 @@ class BaseJsonService extends BaseService {
const { buffer } = await this._request({ const { buffer } = await this._request({
url, url,
options: mergedOptions, options: mergedOptions,
httpErrors, errorMessages,
systemErrors,
}) })
const json = this._parseJson(buffer) const json = this._parseJson(buffer)
return this.constructor._validate(json, schema) return this.constructor._validate(json, schema)

View File

@@ -1,58 +1,29 @@
import makeBadge from '../../badge-maker/lib/make-badge.js'
import BaseService from './base.js' import BaseService from './base.js'
import { import {
serverHasBeenUpSinceResourceCached, serverHasBeenUpSinceResourceCached,
setCacheHeadersForStaticResource, setCacheHeadersForStaticResource,
} from './cache-headers.js' } from './cache-headers.js'
import { makeSend } from './legacy-result-sender.js' import { prepareRoute } from './route.js'
import { MetricHelper } from './metric-helper.js'
import coalesceBadge from './coalesce-badge.js'
import { prepareRoute, namedParamsForMatch } from './route.js'
export default class BaseStaticService extends BaseService { export default class BaseStaticService extends BaseService {
static register({ camp, metricInstance }, serviceConfig) { static _applyCacheHeaders({ res }) {
const { regex, captureNames } = prepareRoute(this.route) setCacheHeadersForStaticResource(res)
}
const metricHelper = MetricHelper.create({ static register({ app, ...serviceContext }, serviceConfig) {
metricInstance, const { regex } = prepareRoute(this.route)
ServiceClass: this, app.get(
}) regex,
(req, res, next) => {
camp.route(regex, async (queryParams, match, end, ask) => { if (serverHasBeenUpSinceResourceCached(req)) {
if (serverHasBeenUpSinceResourceCached(ask.req)) { // Send Not Modified.
// Send Not Modified. res.status(304)
ask.res.statusCode = 304 res.end()
ask.res.end() } else {
return next()
} }
},
const metricHandle = metricHelper.startRequest() this.makeExpressHandler(serviceContext, serviceConfig)
)
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()
})
} }
} }

View File

@@ -53,16 +53,10 @@ class BaseSvgScrapingService extends BaseService {
* @param {string} attrs.url URL to request * @param {string} attrs.url URL to request
* @param {object} [attrs.options={}] Options to pass to got. See * @param {object} [attrs.options={}] Options to pass to got. See
* [documentation](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md) * [documentation](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md)
* @param {object} [attrs.httpErrors={}] Key-value map of status codes * @param {object} [attrs.errorMessages={}] Key-value map of status codes
* and custom error messages e.g: `{ 404: 'package not found' }`. * and custom error messages e.g: `{ 404: 'package not found' }`.
* This can be used to extend or override the * This can be used to extend or override the
* [default](https://github.com/badges/shields/blob/master/core/base-service/check-error-response.js#L5) * [default](https://github.com/badges/shields/blob/master/core/base-service/check-error-response.js#L5)
* @param {object} [attrs.systemErrors={}] Key-value map of got network exception codes
* and an object of params to pass when we construct an Inaccessible exception object
* e.g: `{ ECONNRESET: { prettyMessage: 'connection reset' } }`.
* See {@link https://github.com/sindresorhus/got/blob/main/documentation/7-retry.md#errorcodes got error codes}
* for allowed keys
* and {@link module:core/base-service/errors~RuntimeErrorProps} for allowed values
* @returns {object} Parsed response * @returns {object} Parsed response
* @see https://github.com/sindresorhus/got/blob/main/documentation/2-options.md * @see https://github.com/sindresorhus/got/blob/main/documentation/2-options.md
*/ */
@@ -71,8 +65,7 @@ class BaseSvgScrapingService extends BaseService {
valueMatcher, valueMatcher,
url, url,
options = {}, options = {},
httpErrors = {}, errorMessages = {},
systemErrors = {},
}) { }) {
const logTrace = (...args) => trace.logTrace('fetch', ...args) const logTrace = (...args) => trace.logTrace('fetch', ...args)
const mergedOptions = { const mergedOptions = {
@@ -82,8 +75,7 @@ class BaseSvgScrapingService extends BaseService {
const { buffer } = await this._request({ const { buffer } = await this._request({
url, url,
options: mergedOptions, options: mergedOptions,
httpErrors, errorMessages,
systemErrors,
}) })
logTrace(emojic.dart, 'Response SVG', buffer) logTrace(emojic.dart, 'Response SVG', buffer)
const data = { const data = {

View File

@@ -24,16 +24,10 @@ class BaseXmlService extends BaseService {
* @param {string} attrs.url URL to request * @param {string} attrs.url URL to request
* @param {object} [attrs.options={}] Options to pass to got. See * @param {object} [attrs.options={}] Options to pass to got. See
* [documentation](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md) * [documentation](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md)
* @param {object} [attrs.httpErrors={}] Key-value map of status codes * @param {object} [attrs.errorMessages={}] Key-value map of status codes
* and custom error messages e.g: `{ 404: 'package not found' }`. * and custom error messages e.g: `{ 404: 'package not found' }`.
* This can be used to extend or override the * This can be used to extend or override the
* [default](https://github.com/badges/shields/blob/master/core/base-service/check-error-response.js#L5) * [default](https://github.com/badges/shields/blob/master/core/base-service/check-error-response.js#L5)
* @param {object} [attrs.systemErrors={}] Key-value map of got network exception codes
* and an object of params to pass when we construct an Inaccessible exception object
* e.g: `{ ECONNRESET: { prettyMessage: 'connection reset' } }`.
* See {@link https://github.com/sindresorhus/got/blob/main/documentation/7-retry.md#errorcodes got error codes}
* for allowed keys
* and {@link module:core/base-service/errors~RuntimeErrorProps} for allowed values
* @param {object} [attrs.parserOptions={}] Options to pass to fast-xml-parser. See * @param {object} [attrs.parserOptions={}] Options to pass to fast-xml-parser. See
* [documentation](https://github.com/NaturalIntelligence/fast-xml-parser#xml-to-json) * [documentation](https://github.com/NaturalIntelligence/fast-xml-parser#xml-to-json)
* @returns {object} Parsed response * @returns {object} Parsed response
@@ -44,8 +38,7 @@ class BaseXmlService extends BaseService {
schema, schema,
url, url,
options = {}, options = {},
httpErrors = {}, errorMessages = {},
systemErrors = {},
parserOptions = {}, parserOptions = {},
}) { }) {
const logTrace = (...args) => trace.logTrace('fetch', ...args) const logTrace = (...args) => trace.logTrace('fetch', ...args)
@@ -56,8 +49,7 @@ class BaseXmlService extends BaseService {
const { buffer } = await this._request({ const { buffer } = await this._request({
url, url,
options: mergedOptions, options: mergedOptions,
httpErrors, errorMessages,
systemErrors,
}) })
const validateResult = XMLValidator.validate(buffer) const validateResult = XMLValidator.validate(buffer)
if (validateResult !== true) { if (validateResult !== true) {

View File

@@ -23,16 +23,10 @@ class BaseYamlService extends BaseService {
* @param {string} attrs.url URL to request * @param {string} attrs.url URL to request
* @param {object} [attrs.options={}] Options to pass to got. See * @param {object} [attrs.options={}] Options to pass to got. See
* [documentation](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md) * [documentation](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md)
* @param {object} [attrs.httpErrors={}] Key-value map of status codes * @param {object} [attrs.errorMessages={}] Key-value map of status codes
* and custom error messages e.g: `{ 404: 'package not found' }`. * and custom error messages e.g: `{ 404: 'package not found' }`.
* This can be used to extend or override the * This can be used to extend or override the
* [default](https://github.com/badges/shields/blob/master/core/base-service/check-error-response.js#L5) * [default](https://github.com/badges/shields/blob/master/core/base-service/check-error-response.js#L5)
* @param {object} [attrs.systemErrors={}] Key-value map of got network exception codes
* and an object of params to pass when we construct an Inaccessible exception object
* e.g: `{ ECONNRESET: { prettyMessage: 'connection reset' } }`.
* See {@link https://github.com/sindresorhus/got/blob/main/documentation/7-retry.md#errorcodes got error codes}
* for allowed keys
* and {@link module:core/base-service/errors~RuntimeErrorProps} for allowed values
* @param {object} [attrs.encoding='utf8'] Character encoding * @param {object} [attrs.encoding='utf8'] Character encoding
* @returns {object} Parsed response * @returns {object} Parsed response
* @see https://github.com/sindresorhus/got/blob/main/documentation/2-options.md * @see https://github.com/sindresorhus/got/blob/main/documentation/2-options.md
@@ -41,8 +35,7 @@ class BaseYamlService extends BaseService {
schema, schema,
url, url,
options = {}, options = {},
httpErrors = {}, errorMessages = {},
systemErrors = {},
encoding = 'utf8', encoding = 'utf8',
}) { }) {
const logTrace = (...args) => trace.logTrace('fetch', ...args) const logTrace = (...args) => trace.logTrace('fetch', ...args)
@@ -58,8 +51,7 @@ class BaseYamlService extends BaseService {
const { buffer } = await this._request({ const { buffer } = await this._request({
url, url,
options: mergedOptions, options: mergedOptions,
httpErrors, errorMessages,
systemErrors,
}) })
let parsed let parsed
try { try {

View File

@@ -6,8 +6,13 @@
import emojic from 'emojic' import emojic from 'emojic'
import Joi from 'joi' import Joi from 'joi'
import log from '../server/log.js' import log from '../server/log.js'
import makeBadge from '../../badge-maker/lib/make-badge.js'
import { AuthHelper } from './auth-helper.js' import { AuthHelper } from './auth-helper.js'
import { MetricHelper, MetricNames } from './metric-helper.js' import { MetricHelper, MetricNames } from './metric-helper.js'
import {
coalesceCacheLength,
setHeadersForCacheLength,
} from './cache-headers.js'
import { assertValidCategory } from './categories.js' import { assertValidCategory } from './categories.js'
import checkErrorResponse from './check-error-response.js' import checkErrorResponse from './check-error-response.js'
import coalesceBadge from './coalesce-badge.js' import coalesceBadge from './coalesce-badge.js'
@@ -21,11 +26,12 @@ import {
} from './errors.js' } from './errors.js'
import { validateExample, transformExample } from './examples.js' import { validateExample, transformExample } from './examples.js'
import { fetch } from './got.js' import { fetch } from './got.js'
import { makeJsonBadge } from './make-json-badge.js'
import { import {
makeFullUrl, makeFullUrl,
assertValidRoute, assertValidRoute,
paramsForReq,
prepareRoute, prepareRoute,
namedParamsForMatch,
getQueryParamNames, getQueryParamNames,
} from './route.js' } from './route.js'
import { assertValidServiceDefinition } from './service-definitions.js' import { assertValidServiceDefinition } from './service-definitions.js'
@@ -140,15 +146,6 @@ class BaseService {
*/ */
static examples = [] static examples = []
/**
* Optional: an OpenAPI Paths Object describing this service's
* route or routes in OpenAPI format.
*
* @see https://swagger.io/specification/#paths-object
* @abstract
*/
static openApi = undefined
static get _cacheLength() { static get _cacheLength() {
const cacheLengths = { const cacheLengths = {
build: 30, build: 30,
@@ -192,7 +189,7 @@ class BaseService {
} }
static getDefinition() { static getDefinition() {
const { category, name, isDeprecated, openApi } = this const { category, name, isDeprecated } = this
const { base, format, pattern } = this.route const { base, format, pattern } = this.route
const queryParams = getQueryParamNames(this.route) const queryParams = getQueryParamNames(this.route)
@@ -209,7 +206,7 @@ class BaseService {
route = undefined route = undefined
} }
const result = { category, name, isDeprecated, route, examples, openApi } const result = { category, name, isDeprecated, route, examples }
assertValidServiceDefinition(result, `getDefinition() for ${this.name}`) assertValidServiceDefinition(result, `getDefinition() for ${this.name}`)
@@ -226,18 +223,12 @@ class BaseService {
this._metricHelper = metricHelper this._metricHelper = metricHelper
} }
async _request({ url, options = {}, httpErrors = {}, systemErrors = {} }) { async _request({ url, options = {}, errorMessages = {} }) {
const logTrace = (...args) => trace.logTrace('fetch', ...args) const logTrace = (...args) => trace.logTrace('fetch', ...args)
let logUrl = url let logUrl = url
const logOptions = Object.assign({}, options) const logOptions = Object.assign({}, options)
if ('searchParams' in options && options.searchParams != null) { if ('searchParams' in options) {
const params = new URLSearchParams( const params = new URLSearchParams(options.searchParams)
Object.fromEntries(
Object.entries(options.searchParams).filter(
([k, v]) => v !== undefined
)
)
)
logUrl = `${url}?${params.toString()}` logUrl = `${url}?${params.toString()}`
delete logOptions.searchParams delete logOptions.searchParams
} }
@@ -246,14 +237,10 @@ class BaseService {
'Request', 'Request',
`${logUrl}\n${JSON.stringify(logOptions, null, 2)}` `${logUrl}\n${JSON.stringify(logOptions, null, 2)}`
) )
const { res, buffer } = await this._requestFetcher( const { res, buffer } = await this._requestFetcher(url, options)
url,
options,
systemErrors
)
await this._meterResponse(res, buffer) await this._meterResponse(res, buffer)
logTrace(emojic.dart, 'Response status code', res.statusCode) logTrace(emojic.dart, 'Response status code', res.statusCode)
return checkErrorResponse(httpErrors)({ buffer, res }) return checkErrorResponse(errorMessages)({ buffer, res })
} }
static enabledMetrics = [] static enabledMetrics = []
@@ -332,15 +319,11 @@ class BaseService {
error instanceof Deprecated error instanceof Deprecated
) { ) {
trace.logTrace('outbound', emojic.noGoodWoman, 'Handled error', error) trace.logTrace('outbound', emojic.noGoodWoman, 'Handled error', error)
const serviceData = { return {
isError: true, isError: true,
message: error.prettyMessage, message: error.prettyMessage,
color: 'lightgray', color: 'lightgray',
} }
if (error.cacheSeconds !== undefined) {
serviceData.cacheSeconds = error.cacheSeconds
}
return serviceData
} else if (this._handleInternalErrors) { } else if (this._handleInternalErrors) {
if ( if (
!trace.logTrace( !trace.logTrace(
@@ -446,60 +429,90 @@ class BaseService {
return serviceData return serviceData
} }
static register( // `defaultCacheLengthSeconds` can be overridden by
{ // `serviceDefaultCacheLengthSeconds` (either by category or on a badge-
camp, // by-badge basis). Then in turn that can be overridden by
handleRequest, // `serviceOverrideCacheLengthSeconds` (which we expect to be used only in
githubApiProvider, // the dynamic badge) but only if `serviceOverrideCacheLengthSeconds` is
librariesIoApiProvider, // longer than `serviceDefaultCacheLengthSeconds` and then the `cacheSeconds`
metricInstance, // 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 serviceConfig
) { ) {
const { cacheHeaders: cacheHeaderConfig } = serviceConfig
const { regex, captureNames } = prepareRoute(this.route)
const queryParams = getQueryParamNames(this.route)
const metricHelper = MetricHelper.create({ const metricHelper = MetricHelper.create({
metricInstance, metricInstance,
ServiceClass: this, ServiceClass: this,
}) })
const { captureNames } = prepareRoute(this.route)
const { cacheHeaders: cacheHeaderConfig } = serviceConfig
camp.route( return async (req, res) => {
regex, const metricHandle = metricHelper.startRequest()
handleRequest(cacheHeaderConfig, {
queryParams,
handler: async (queryParams, match, sendBadge) => {
const metricHandle = metricHelper.startRequest()
const namedParams = namedParamsForMatch(captureNames, match, this) const { namedParams, format } = paramsForReq(captureNames, req, this)
const serviceData = await this.invoke( const serviceData = await this.invoke(
{ {
requestFetcher: fetch, requestFetcher: fetch,
githubApiProvider, githubApiProvider,
librariesIoApiProvider, librariesIoApiProvider,
metricHelper, 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()
}, },
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))
} }
} }

View File

@@ -1,9 +1,11 @@
import Joi from 'joi' import Joi from 'joi'
import chai from 'chai' import chai from 'chai'
import isSvg from 'is-svg'
import sinon from 'sinon' import sinon from 'sinon'
import prometheus from 'prom-client' import prometheus from 'prom-client'
import chaiAsPromised from 'chai-as-promised' import chaiAsPromised from 'chai-as-promised'
import PrometheusMetrics from '../server/prometheus-metrics.js' import PrometheusMetrics from '../server/prometheus-metrics.js'
import { ExpressTestHarness } from '../express-test-harness.js'
import trace from './trace.js' import trace from './trace.js'
import { import {
NotFound, NotFound,
@@ -15,6 +17,7 @@ import {
import BaseService from './base.js' import BaseService from './base.js'
import { MetricHelper, MetricNames } from './metric-helper.js' import { MetricHelper, MetricNames } from './metric-helper.js'
import '../register-chai-plugins.spec.js' import '../register-chai-plugins.spec.js'
const { expect } = chai const { expect } = chai
chai.use(chaiAsPromised) chai.use(chaiAsPromised)
@@ -59,9 +62,12 @@ class DummyServiceWithServiceResponseSizeMetricEnabled extends DummyService {
describe('BaseService', function () { describe('BaseService', function () {
const defaultConfig = { const defaultConfig = {
handleInternalErrors: false,
cacheHeaders: { defaultCacheLengthSeconds: 120 },
public: { public: {
handleInternalErrors: false, handleInternalErrors: false,
services: {}, services: {},
cacheHeaders: { defaultCacheLengthSeconds: 120 },
}, },
private: {}, private: {},
} }
@@ -321,62 +327,45 @@ describe('BaseService', function () {
}) })
}) })
describe('ScoutCamp integration', function () { describe('Express integration', function () {
// TODO Strangly, without the useless escape the regexes do not match in Node 12. let harness
// eslint-disable-next-line no-useless-escape beforeEach(async function () {
const expectedRouteRegex = /^\/foo(?:\/([^\/#\?]+?))(|\.svg|\.json)$/ harness = new ExpressTestHarness()
DummyService.register({ app: harness.app }, defaultConfig)
await harness.start()
})
let mockCamp afterEach(async function () {
let mockHandleRequest await harness.stop()
})
beforeEach(function () { it('fulfills the request for an SVG badge', async function () {
mockCamp = { const { headers, body } = await harness.get(
route: sinon.spy(), '/foo/bar.svg?queryParamA=%3F'
}
mockHandleRequest = sinon.spy()
DummyService.register(
{ camp: mockCamp, handleRequest: mockHandleRequest },
defaultConfig
) )
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 () { it('fulfills the request for a JSON badge', async function () {
expect(mockCamp.route).to.have.been.calledOnce const { headers, body } = await harness.get(
expect(mockCamp.route).to.have.been.calledWith(expectedRouteRegex) '/foo/bar.json?queryParamA=%3F',
}) { responseType: 'json' }
)
it('handles the request', async function () { expect(headers).to.include({
expect(mockHandleRequest).to.have.been.calledOnce 'content-type': 'application/json; charset=utf-8',
})
const { queryParams: serviceQueryParams, handler: requestHandler } = expect(body).to.include({
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, {
label: 'cat', label: 'cat',
message: 'Hello namedParamA: bar with queryParamA: ?', 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 url = 'some-url'
const options = { const options = { headers: { Cookie: 'some-cookie' } }
headers: { Cookie: 'some-cookie' },
searchParams: { param1: 'foobar', param2: undefined },
}
await serviceInstance._request({ url, options }) await serviceInstance._request({ url, options })
expect(trace.logTrace).to.be.calledWithMatch( expect(trace.logTrace).to.be.calledWithMatch(
'fetch', 'fetch',
sinon.match.string, sinon.match.string,
'Request', 'Request',
`${url}?param1=foobar\n${JSON.stringify( `${url}\n${JSON.stringify(options, null, 2)}`
{ headers: options.headers },
null,
2
)}`
) )
expect(trace.logTrace).to.be.calledWithMatch( expect(trace.logTrace).to.be.calledWithMatch(
'fetch', 'fetch',
@@ -581,9 +563,7 @@ describe('BaseService', function () {
}, },
private: {}, private: {},
}, },
{ { namedParamA: 'bar.bar.bar' }
namedParamA: 'bar.bar.bar',
}
) )
).to.deep.equal({ ).to.deep.equal({
color: 'lightgray', color: 'lightgray',

View File

@@ -39,7 +39,6 @@ function coalesceCacheLength({
assert(defaultCacheLengthSeconds !== undefined) assert(defaultCacheLengthSeconds !== undefined)
const cacheLength = coalesce( const cacheLength = coalesce(
serviceOverrideCacheLengthSeconds,
serviceDefaultCacheLengthSeconds, serviceDefaultCacheLengthSeconds,
defaultCacheLengthSeconds defaultCacheLengthSeconds
) )
@@ -47,6 +46,7 @@ function coalesceCacheLength({
// Overrides can apply _more_ caching, but not less. Query param overriding // Overrides can apply _more_ caching, but not less. Query param overriding
// can request more overriding than service override, but not less. // can request more overriding than service override, but not less.
const candidateOverrides = [ const candidateOverrides = [
serviceOverrideCacheLengthSeconds,
overrideCacheLengthFromQueryParams(queryParams), overrideCacheLengthFromQueryParams(queryParams),
].filter(x => x !== undefined) ].filter(x => x !== undefined)

View File

@@ -74,12 +74,12 @@ describe('Cache header functions', function () {
serviceDefaultCacheLengthSeconds: 900, serviceDefaultCacheLengthSeconds: 900,
serviceOverrideCacheLengthSeconds: 400, serviceOverrideCacheLengthSeconds: 400,
queryParams: {}, queryParams: {},
}).expect(400) }).expect(900)
given({ given({
cacheHeaderConfig, cacheHeaderConfig,
serviceOverrideCacheLengthSeconds: 400, serviceOverrideCacheLengthSeconds: 400,
queryParams: {}, queryParams: {},
}).expect(400) }).expect(777)
given({ given({
cacheHeaderConfig, cacheHeaderConfig,
serviceOverrideCacheLengthSeconds: 900, serviceOverrideCacheLengthSeconds: 900,

View File

@@ -2,22 +2,21 @@ import { NotFound, InvalidResponse, Inaccessible } from './errors.js'
const defaultErrorMessages = { const defaultErrorMessages = {
404: 'not found', 404: 'not found',
429: 'rate limited by upstream service',
} }
export default function checkErrorResponse(httpErrors = {}) { export default function checkErrorResponse(errorMessages = {}) {
return async function ({ buffer, res }) { return async function ({ buffer, res }) {
let error let error
httpErrors = { ...defaultErrorMessages, ...httpErrors } errorMessages = { ...defaultErrorMessages, ...errorMessages }
if (res.statusCode === 404) { if (res.statusCode === 404) {
error = new NotFound({ prettyMessage: httpErrors[404] }) error = new NotFound({ prettyMessage: errorMessages[404] })
} else if (res.statusCode !== 200) { } else if (res.statusCode !== 200) {
const underlying = Error( const underlying = Error(
`Got status code ${res.statusCode} (expected 200)` `Got status code ${res.statusCode} (expected 200)`
) )
const props = { underlyingError: underlying } const props = { underlyingError: underlying }
if (httpErrors[res.statusCode] !== undefined) { if (errorMessages[res.statusCode] !== undefined) {
props.prettyMessage = httpErrors[res.statusCode] props.prettyMessage = errorMessages[res.statusCode]
} }
if (res.statusCode >= 500) { if (res.statusCode >= 500) {
error = new Inaccessible(props) error = new Inaccessible(props)

View File

@@ -45,42 +45,6 @@ describe('async error handler', function () {
}) })
}) })
context('when status is 429', function () {
const buffer = Buffer.from('some stuff')
const res = { statusCode: 429 }
it('throws InvalidResponse', async function () {
try {
await checkErrorResponse()({ res, buffer })
expect.fail('Expected to throw')
} catch (e) {
expect(e).to.be.an.instanceof(InvalidResponse)
expect(e.message).to.equal(
'Invalid Response: Got status code 429 (expected 200)'
)
expect(e.prettyMessage).to.equal('rate limited by upstream service')
expect(e.response).to.equal(res)
expect(e.buffer).to.equal(buffer)
}
})
it('displays the custom too many requests', async function () {
const notFoundMessage = "terribly sorry but that's one too many requests"
try {
await checkErrorResponse({ 429: notFoundMessage })({ res, buffer })
expect.fail('Expected to throw')
} catch (e) {
expect(e).to.be.an.instanceof(InvalidResponse)
expect(e.message).to.equal(
'Invalid Response: Got status code 429 (expected 200)'
)
expect(e.prettyMessage).to.equal(
"terribly sorry but that's one too many requests"
)
}
})
})
context('when status is 4xx', function () { context('when status is 4xx', function () {
it('throws InvalidResponse', async function () { it('throws InvalidResponse', async function () {
const res = { statusCode: 499 } const res = { statusCode: 499 }

View File

@@ -16,15 +16,16 @@ import toArray from './to-array.js'
// //
// Logos are resolved in this manner: // Logos are resolved in this manner:
// //
// 1. When `?logo=` contains a named logo or the name of one of the Shields // 1. When `?logo=` contains the name of one of the Shields logos, or contains
// logos or contains base64-encoded SVG, that logo is used. When a // base64-encoded SVG, that logo is used. In the case of a named logo, when
// `&logoColor=` is specified, that color is used (except for the // a `&logoColor=` is specified, that color is used. Otherwise the default
// base64-encoded logos). Otherwise the default color is used. If the color // color is used. `logoColor` will not be applied to a custom
// is specified for a multicolor Shield logo, the named logo will be used and // (base64-encoded) logo; if a custom color is desired the logo should be
// colored. The appearance of the logo can be customized using `logoWidth`, // recolored prior to making the request. The appearance of the logo can be
// and in the case of the popout badge, `logoPosition`. When `?logo=` is // customized using `logoWidth`, and in the case of the popout badge,
// specified, any logo-related parameters specified dynamically by the // `logoPosition`. When `?logo=` is specified, any logo-related parameters
// service, or by default in the service, are ignored. // 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 // 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 // used only by the Endpoint badge. The `logoColor` can be overridden by the
// query string. // query string.

View File

@@ -153,18 +153,10 @@ describe('coalesceBadge', function () {
).and.not.to.be.empty ).and.not.to.be.empty
}) })
it('applies the named monochrome logo with color', function () { it('applies the named 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 () {
expect( expect(
coalesceBadge({}, { namedLogo: 'npm', logoColor: 'blue' }, {}).logo 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 .empty
}) })
@@ -174,25 +166,15 @@ describe('coalesceBadge', function () {
).to.equal(getShieldsIcon({ name: 'npm' })).and.not.be.empty ).to.equal(getShieldsIcon({ name: 'npm' })).and.not.be.empty
}) })
it('overrides the monochrome logo with a color', function () { it('overrides the 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 () {
expect( expect(
coalesceBadge( coalesceBadge(
{ logo: 'npm', logoColor: 'blue' }, { logo: 'npm', logoColor: 'blue' },
{ namedLogo: 'appveyor' }, { namedLogo: 'appveyor' },
{} {}
).logo ).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 () { 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 ).to.equal(getShieldsIcon({ name: 'npm' })).and.not.be.empty
}) })
it("overrides the service monochome logo's color", function () { it("overrides the service 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 () {
expect( expect(
coalesceBadge( coalesceBadge(
{ logoColor: 'blue' }, { logoColor: 'blue' },
{ namedLogo: 'npm', logoColor: 'red' }, { namedLogo: 'npm', logoColor: 'red' },
{} {}
).logo ).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 // https://github.com/badges/shields/issues/2998

View File

@@ -42,7 +42,6 @@ class ShieldsRuntimeError extends Error {
if (props.underlyingError) { if (props.underlyingError) {
this.stack = props.underlyingError.stack this.stack = props.underlyingError.stack
} }
this.cacheSeconds = props.cacheSeconds
} }
} }
@@ -207,9 +206,6 @@ class Deprecated extends ShieldsRuntimeError {
* @property {string} prettyMessage User-facing error message to override the * @property {string} prettyMessage User-facing error message to override the
* value of `defaultPrettyMessage()`. This is the text that will appear on the * value of `defaultPrettyMessage()`. This is the text that will appear on the
* badge when we catch and render the exception (Optional) * badge when we catch and render the exception (Optional)
* @property {number} cacheSeconds Length of time to cache this error response
* for. Defaults to the cacheLength of the service class throwing the error
* (Optional)
*/ */
export { export {

View File

@@ -129,7 +129,6 @@ function transformExample(inExample, index, ServiceClass) {
ServiceClass ServiceClass
) )
const category = categories.find(c => c.id === ServiceClass.category)
return { return {
title, title,
example: { example: {
@@ -147,7 +146,9 @@ function transformExample(inExample, index, ServiceClass) {
style: style === 'flat' ? undefined : style, style: style === 'flat' ? undefined : style,
namedLogo, namedLogo,
}, },
keywords: category ? keywords.concat(category.keywords) : keywords, keywords: keywords.concat(
categories.find(c => c.id === ServiceClass.category).keywords
),
documentation: documentation ? { __html: documentation } : undefined, documentation: documentation ? { __html: documentation } : undefined,
} }
} }

View File

@@ -7,7 +7,7 @@ import {
const userAgent = getUserAgent() const userAgent = getUserAgent()
async function sendRequest(gotWrapper, url, options = {}, systemErrors = {}) { async function sendRequest(gotWrapper, url, options) {
const gotOptions = Object.assign({}, options) const gotOptions = Object.assign({}, options)
gotOptions.throwHttpErrors = false gotOptions.throwHttpErrors = false
gotOptions.retry = { limit: 0 } gotOptions.retry = { limit: 0 }
@@ -22,12 +22,6 @@ async function sendRequest(gotWrapper, url, options = {}, systemErrors = {}) {
underlyingError: new Error('Maximum response size exceeded'), underlyingError: new Error('Maximum response size exceeded'),
}) })
} }
if (err.code in systemErrors) {
throw new Inaccessible({
...systemErrors[err.code],
underlyingError: err,
})
}
throw new Inaccessible({ underlyingError: err }) throw new Inaccessible({ underlyingError: err })
} }
} }

View File

@@ -45,36 +45,6 @@ describe('got wrapper', function () {
) )
}) })
it('should throw a custom error if provided', async function () {
const sendRequest = _fetchFactory(1024)
return (
expect(
sendRequest(
'https://www.google.com/foo/bar',
{ timeout: { request: 1 } },
{
ETIMEDOUT: {
prettyMessage: 'Oh no! A terrible thing has happened',
cacheSeconds: 10,
},
}
)
)
.to.be.rejectedWith(
Inaccessible,
"Inaccessible: Timeout awaiting 'request' for 1ms"
)
// eslint-disable-next-line promise/prefer-await-to-then
.then(error => {
expect(error).to.have.property(
'prettyMessage',
'Oh no! A terrible thing has happened'
)
expect(error).to.have.property('cacheSeconds', 10)
})
)
})
it('should pass a custom user agent header', async function () { it('should pass a custom user agent header', async function () {
nock('https://www.google.com', { nock('https://www.google.com', {
reqheaders: { reqheaders: {

View File

@@ -1,141 +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', '/badges/endpoint-badge')
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`.
// Then the `cacheSeconds` query param can also override both of those
// but 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 }

View File

@@ -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 allow serviceData to 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 allow serviceData to 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=200, s-maxage=200')
})
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)
})
})
})
})

View File

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

View File

@@ -1,10 +1,10 @@
import BaseJsonService from '../base-json.js' import BaseJsonService from '../base-json.js'
class BadBaseService {} class BadBaseService {}
class GoodMixedService extends BaseJsonService { class GoodService extends BaseJsonService {
static category = 'build' static category = 'build'
static route = { base: 'it/is', pattern: 'good' } static route = { base: 'it/is', pattern: 'good' }
} }
class BadMixedService extends BadBaseService {} class BadService extends BadBaseService {}
export default [GoodMixedService, BadMixedService] export default [GoodService, BadService]

View File

@@ -1,3 +1,3 @@
class BadNoBaseService {} class BadService {}
export default BadNoBaseService export default BadService

View File

@@ -1,4 +1,4 @@
class BadBaseService {} class BadBaseService {}
class BadChildService extends BadBaseService {} class BadService extends BadBaseService {}
export default BadChildService export default BadService

View File

@@ -1,12 +1,12 @@
import BaseJsonService from '../base-json.js' import BaseJsonService from '../base-json.js'
class GoodServiceArrayOne extends BaseJsonService { class GoodServiceOne extends BaseJsonService {
static category = 'build' static category = 'build'
static route = { base: 'good', pattern: 'one' } static route = { base: 'good', pattern: 'one' }
} }
class GoodServiceArrayTwo extends BaseJsonService { class GoodServiceTwo extends BaseJsonService {
static category = 'build' static category = 'build'
static route = { base: 'good', pattern: 'two' } static route = { base: 'good', pattern: 'two' }
} }
export default [GoodServiceArrayOne, GoodServiceArrayTwo] export default [GoodServiceOne, GoodServiceTwo]

View File

@@ -1,12 +1,12 @@
import BaseJsonService from '../base-json.js' import BaseJsonService from '../base-json.js'
class GoodServiceObjectOne extends BaseJsonService { class GoodServiceOne extends BaseJsonService {
static category = 'build' static category = 'build'
static route = { base: 'good', pattern: 'one' } static route = { base: 'good', pattern: 'one' }
} }
class GoodServiceObjectTwo extends BaseJsonService { class GoodServiceTwo extends BaseJsonService {
static category = 'build' static category = 'build'
static route = { base: 'good', pattern: 'two' } static route = { base: 'good', pattern: 'two' }
} }
export { GoodServiceObjectOne, GoodServiceObjectTwo } export { GoodServiceOne, GoodServiceTwo }

View File

@@ -1,6 +1,6 @@
import path from 'path' import path from 'path'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
import { globSync } from 'glob' import glob from 'glob'
import countBy from 'lodash.countby' import countBy from 'lodash.countby'
import categories from '../../services/categories.js' import categories from '../../services/categories.js'
import BaseService from './base.js' import BaseService from './base.js'
@@ -13,13 +13,6 @@ const serviceDir = path.join(
'services' '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 { class InvalidService extends Error {
constructor(message) { constructor(message) {
super(message) super(message)
@@ -27,25 +20,9 @@ class InvalidService extends Error {
} }
} }
function getServicePaths(pattern) {
return globSync(toUnixPath(path.join(serviceDir, '**', pattern))).sort()
}
function assertNamesUnique(names, { message }) {
const duplicates = {}
Object.entries(countBy(names))
.filter(([name, count]) => count > 1)
.forEach(([name, count]) => {
duplicates[name] = count
})
if (Object.keys(duplicates).length) {
throw new Error(`${message}: ${JSON.stringify(duplicates, undefined, 2)}`)
}
}
async function loadServiceClasses(servicePaths) { async function loadServiceClasses(servicePaths) {
if (!servicePaths) { if (!servicePaths) {
servicePaths = getServicePaths('*.service.js') servicePaths = glob.sync(path.join(serviceDir, '**', '*.service.js'))
} }
const serviceClasses = [] const serviceClasses = []
@@ -76,14 +53,29 @@ async function loadServiceClasses(servicePaths) {
}) })
} }
return serviceClasses
}
function assertNamesUnique(names, { message }) {
const duplicates = {}
Object.entries(countBy(names))
.filter(([name, count]) => count > 1)
.forEach(([name, count]) => {
duplicates[name] = count
})
if (Object.keys(duplicates).length) {
throw new Error(`${message}: ${JSON.stringify(duplicates, undefined, 2)}`)
}
}
async function checkNames() {
const services = await loadServiceClasses()
assertNamesUnique( assertNamesUnique(
serviceClasses.map(({ name }) => name), services.map(({ name }) => name),
{ {
message: 'Duplicate service names found', message: 'Duplicate service names found',
} }
) )
return serviceClasses
} }
async function collectDefinitions() { async function collectDefinitions() {
@@ -101,16 +93,16 @@ async function collectDefinitions() {
async function loadTesters() { async function loadTesters() {
return Promise.all( return Promise.all(
getServicePaths('*.tester.js').map( glob
async path => await import(`file://${path}`) .sync(path.join(serviceDir, '**', '*.tester.js'))
) .map(async path => await import(`file://${path}`))
) )
} }
export { export {
InvalidService, InvalidService,
loadServiceClasses, loadServiceClasses,
getServicePaths, checkNames,
collectDefinitions, collectDefinitions,
loadTesters, loadTesters,
} }

View File

@@ -2,11 +2,7 @@ import path from 'path'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
import chai from 'chai' import chai from 'chai'
import chaiAsPromised from 'chai-as-promised' import chaiAsPromised from 'chai-as-promised'
import { import { loadServiceClasses, InvalidService } from './loader.js'
loadServiceClasses,
getServicePaths,
InvalidService,
} from './loader.js'
chai.use(chaiAsPromised) chai.use(chaiAsPromised)
const { expect } = chai const { expect } = chai
@@ -69,15 +65,3 @@ describe('loadServiceClasses function', function () {
).to.eventually.have.length(5) ).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)
})
})

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

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

View File

@@ -1,335 +0,0 @@
const baseUrl = process.env.BASE_URL
const globalParamRefs = [
{ $ref: '#/components/parameters/style' },
{ $ref: '#/components/parameters/logo' },
{ $ref: '#/components/parameters/logoColor' },
{ $ref: '#/components/parameters/label' },
{ $ref: '#/components/parameters/labelColor' },
{ $ref: '#/components/parameters/color' },
{ $ref: '#/components/parameters/cacheSeconds' },
{ $ref: '#/components/parameters/link' },
]
function getCodeSamples(altText) {
return [
{
lang: 'URL',
label: 'URL',
source: '$url',
},
{
lang: 'Markdown',
label: 'Markdown',
source: `![${altText}]($url)`,
},
{
lang: 'reStructuredText',
label: 'rSt',
source: `.. image:: $url\n: alt: ${altText}`,
},
{
lang: 'AsciiDoc',
label: 'AsciiDoc',
source: `image:$url[${altText}]`,
},
{
lang: 'HTML',
label: 'HTML',
source: `<img alt="${altText}" src="$url">`,
},
]
}
function pattern2openapi(pattern) {
return pattern
.replace(/:([A-Za-z0-9_\-.]+)(?=[/]?)/g, (matches, grp1) => `{${grp1}}`)
.replace(/\([^)]*\)/g, '')
.replace(/\+$/, '')
}
function getEnum(pattern, paramName) {
const re = new RegExp(`${paramName}\\(([A-Za-z0-9_\\-|]+)\\)`)
const match = pattern.match(re)
if (match === null) {
return undefined
}
if (!match[1].includes('|')) {
return undefined
}
return match[1].split('|')
}
function param2openapi(pattern, paramName, exampleValue, paramType) {
const outParam = {}
outParam.name = paramName
// We don't have description if we are building the OpenAPI spec from examples[]
outParam.in = paramType
if (paramType === 'path') {
outParam.required = true
} else {
/* Occasionally we do have required query params, but we can't
detect this if we are building the OpenAPI spec from examples[]
so just assume all query params are optional */
outParam.required = false
}
if (exampleValue === null && paramType === 'query') {
outParam.schema = { type: 'boolean' }
outParam.allowEmptyValue = true
} else {
outParam.schema = { type: 'string' }
}
if (paramType === 'path') {
outParam.schema.enum = getEnum(pattern, paramName)
}
outParam.example = exampleValue
return outParam
}
function getVariants(pattern) {
/*
given a URL pattern (which may include '/one/or/:more?/:optional/:parameters*')
return an array of all possible permutations:
[
'/one/or/:more/:optional/:parameters',
'/one/or/:optional/:parameters',
'/one/or/:more/:optional',
'/one/or/:optional',
]
*/
const patterns = [pattern.split('/')]
while (patterns.flat().find(p => p.endsWith('?') || p.endsWith('*'))) {
for (let i = 0; i < patterns.length; i++) {
const pattern = patterns[i]
for (let j = 0; j < pattern.length; j++) {
const path = pattern[j]
if (path.endsWith('?') || path.endsWith('*')) {
pattern[j] = path.slice(0, -1)
patterns.push(patterns[i].filter(p => p !== pattern[j]))
}
}
}
}
for (let i = 0; i < patterns.length; i++) {
patterns[i] = patterns[i].join('/')
}
return patterns
}
function examples2openapi(examples) {
const paths = {}
for (const example of examples) {
const patterns = getVariants(example.example.pattern)
for (const pattern of patterns) {
const openApiPattern = pattern2openapi(pattern)
if (
openApiPattern.includes('*') ||
openApiPattern.includes('?') ||
openApiPattern.includes('+') ||
openApiPattern.includes('(')
) {
throw new Error(`unexpected characters in pattern '${openApiPattern}'`)
}
/*
There's several things going on in this block:
1. Filter out any examples for params that don't appear
in this variant of the route
2. Make sure we add params to the array
in the same order they appear in the route
3. If there are any params we don't have an example value for,
make sure they still appear in the pathParams array with
exampleValue == undefined anyway
*/
const pathParams = []
for (const param of openApiPattern
.split('/')
.filter(p => p.startsWith('{') && p.endsWith('}'))) {
const paramName = param.slice(1, -1)
const exampleValue = example.example.namedParams[paramName]
pathParams.push(param2openapi(pattern, paramName, exampleValue, 'path'))
}
const queryParams = example.example.queryParams || {}
const parameters = [
...pathParams,
...Object.entries(queryParams).map(([paramName, exampleValue]) =>
param2openapi(pattern, paramName, exampleValue, 'query')
),
...globalParamRefs,
]
paths[openApiPattern] = {
get: {
summary: example.title,
description: example?.documentation?.__html
.replace(/<br>/g, '<br />') // react does not like <br>
.replace(/{/g, '&#123;')
.replace(/}/g, '&#125;')
.replace(/<style>(.|\n)*?<\/style>/, ''), // workaround for w3c-validation TODO: remove later
parameters,
'x-code-samples': getCodeSamples(example.title),
},
}
}
}
return paths
}
function addGlobalProperties(endpoints) {
const paths = {}
for (const key of Object.keys(endpoints)) {
paths[key] = endpoints[key]
paths[key].get.parameters = [
...paths[key].get.parameters,
...globalParamRefs,
]
paths[key].get['x-code-samples'] = getCodeSamples(paths[key].get.summary)
}
return paths
}
function services2openapi(services) {
const paths = {}
for (const service of services) {
if (service.openApi) {
// if the service declares its own OpenAPI definition, use that...
for (const [key, value] of Object.entries(
addGlobalProperties(service.openApi)
)) {
if (key in paths) {
throw new Error(`Conflicting route: ${key}`)
}
paths[key] = value
}
} else {
// ...otherwise do our best to build one from examples[]
for (const [key, value] of Object.entries(
examples2openapi(service.examples)
)) {
// allow conflicting routes for legacy examples
paths[key] = value
}
}
}
return paths
}
function category2openapi(category, services) {
const spec = {
openapi: '3.0.0',
info: {
version: '1.0.0',
title: category.name,
license: {
name: 'CC0',
},
},
servers: baseUrl ? [{ url: baseUrl }] : undefined,
components: {
parameters: {
style: {
name: 'style',
in: 'query',
required: false,
description:
'One of: flat (default), flat-square, plastic, for-the-badge, social',
schema: {
type: 'string',
},
example: 'flat',
},
logo: {
name: 'logo',
in: 'query',
required: false,
description:
'One of the named logos (bitcoin, dependabot, gitlab, npm, paypal, serverfault, stackexchange, superuser, telegram, travis) or simple-icons. All simple-icons are referenced using icon slugs. You can click the icon title on <a href="https://simpleicons.org/" rel="noopener noreferrer" target="_blank">simple-icons</a> to copy the slug or they can be found in the <a href="https://github.com/simple-icons/simple-icons/blob/master/slugs.md">slugs.md file</a> in the simple-icons repository.',
schema: {
type: 'string',
},
example: 'appveyor',
},
logoColor: {
name: 'logoColor',
in: 'query',
required: false,
description:
'The color of the logo (hex, rgb, rgba, hsl, hsla and css named colors supported). Supported for named logos and Shields logos but not for custom logos. For multicolor Shields logos, the corresponding named logo will be used and colored.',
schema: {
type: 'string',
},
example: 'violet',
},
label: {
name: 'label',
in: 'query',
required: false,
description:
'Override the default left-hand-side text (<a href="https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding">URL-Encoding</a> needed for spaces or special characters!)',
schema: {
type: 'string',
},
example: 'healthiness',
},
labelColor: {
name: 'labelColor',
in: 'query',
required: false,
description:
'Background color of the left part (hex, rgb, rgba, hsl, hsla and css named colors supported).',
schema: {
type: 'string',
},
example: 'abcdef',
},
color: {
name: 'color',
in: 'query',
required: false,
description:
'Background color of the right part (hex, rgb, rgba, hsl, hsla and css named colors supported).',
schema: {
type: 'string',
},
example: 'fedcba',
},
cacheSeconds: {
name: 'cacheSeconds',
in: 'query',
required: false,
description:
'HTTP cache lifetime (rules are applied to infer a default value on a per-badge basis, any values specified below the default will be ignored).',
schema: {
type: 'string',
},
example: '3600',
},
link: {
name: 'link',
in: 'query',
required: false,
description:
'Specify what clicking on the left/right of a badge should do. Note that this only works when integrating your badge in an `<object>` HTML tag, but not an `<img>` tag or a markup language.',
style: 'form',
explode: true,
schema: {
type: 'array',
maxItems: 2,
items: {
type: 'string',
},
},
},
},
},
paths: services2openapi(services),
}
return spec
}
export { category2openapi }

View File

@@ -1,378 +0,0 @@
import chai from 'chai'
import { category2openapi } from './openapi.js'
import BaseJsonService from './base-json.js'
const { expect } = chai
class OpenApiService extends BaseJsonService {
static category = 'build'
static route = { base: 'openapi/service', pattern: ':packageName/:distTag*' }
// this service defines its own API Paths Object
static openApi = {
'/openapi/service/{packageName}': {
get: {
summary: 'OpenApiService Summary',
description: 'OpenApiService Description',
parameters: [
{
name: 'packageName',
description: 'packageName description',
in: 'path',
required: true,
schema: { type: 'string' },
example: 'badge-maker',
},
],
},
},
'/openapi/service/{packageName}/{distTag}': {
get: {
summary: 'OpenApiService Summary (with Tag)',
description: 'OpenApiService Description (with Tag)',
parameters: [
{
name: 'packageName',
description: 'packageName description',
in: 'path',
required: true,
schema: { type: 'string' },
example: 'badge-maker',
},
{
name: 'distTag',
description: 'distTag description',
in: 'path',
required: true,
schema: { type: 'string' },
example: 'latest',
},
],
},
},
}
}
class LegacyService extends BaseJsonService {
static category = 'build'
static route = { base: 'legacy/service', pattern: ':packageName/:distTag*' }
// this service defines an Examples Array
static examples = [
{
title: 'LegacyService Title',
namedParams: { packageName: 'badge-maker' },
staticPreview: { label: 'build', message: 'passing' },
documentation: 'LegacyService Description',
},
{
title: 'LegacyService Title (with Tag)',
namedParams: { packageName: 'badge-maker', distTag: 'latest' },
staticPreview: { label: 'build', message: 'passing' },
documentation: 'LegacyService Description (with Tag)',
},
]
}
const expected = {
openapi: '3.0.0',
info: { version: '1.0.0', title: 'build', license: { name: 'CC0' } },
components: {
parameters: {
style: {
name: 'style',
in: 'query',
required: false,
description:
'One of: flat (default), flat-square, plastic, for-the-badge, social',
schema: { type: 'string' },
example: 'flat',
},
logo: {
name: 'logo',
in: 'query',
required: false,
description:
'One of the named logos (bitcoin, dependabot, gitlab, npm, paypal, serverfault, stackexchange, superuser, telegram, travis) or simple-icons. All simple-icons are referenced using icon slugs. You can click the icon title on <a href="https://simpleicons.org/" rel="noopener noreferrer" target="_blank">simple-icons</a> to copy the slug or they can be found in the <a href="https://github.com/simple-icons/simple-icons/blob/master/slugs.md">slugs.md file</a> in the simple-icons repository.',
schema: { type: 'string' },
example: 'appveyor',
},
logoColor: {
name: 'logoColor',
in: 'query',
required: false,
description:
'The color of the logo (hex, rgb, rgba, hsl, hsla and css named colors supported). Supported for named logos and Shields logos but not for custom logos. For multicolor Shields logos, the corresponding named logo will be used and colored.',
schema: { type: 'string' },
example: 'violet',
},
label: {
name: 'label',
in: 'query',
required: false,
description:
'Override the default left-hand-side text (<a href="https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding">URL-Encoding</a> needed for spaces or special characters!)',
schema: { type: 'string' },
example: 'healthiness',
},
labelColor: {
name: 'labelColor',
in: 'query',
required: false,
description:
'Background color of the left part (hex, rgb, rgba, hsl, hsla and css named colors supported).',
schema: { type: 'string' },
example: 'abcdef',
},
color: {
name: 'color',
in: 'query',
required: false,
description:
'Background color of the right part (hex, rgb, rgba, hsl, hsla and css named colors supported).',
schema: { type: 'string' },
example: 'fedcba',
},
cacheSeconds: {
name: 'cacheSeconds',
in: 'query',
required: false,
description:
'HTTP cache lifetime (rules are applied to infer a default value on a per-badge basis, any values specified below the default will be ignored).',
schema: { type: 'string' },
example: '3600',
},
link: {
name: 'link',
in: 'query',
required: false,
description:
'Specify what clicking on the left/right of a badge should do. Note that this only works when integrating your badge in an `<object>` HTML tag, but not an `<img>` tag or a markup language.',
style: 'form',
explode: true,
schema: { type: 'array', maxItems: 2, items: { type: 'string' } },
},
},
},
paths: {
'/openapi/service/{packageName}': {
get: {
summary: 'OpenApiService Summary',
description: 'OpenApiService Description',
parameters: [
{
name: 'packageName',
description: 'packageName description',
in: 'path',
required: true,
schema: { type: 'string' },
example: 'badge-maker',
},
{ $ref: '#/components/parameters/style' },
{ $ref: '#/components/parameters/logo' },
{ $ref: '#/components/parameters/logoColor' },
{ $ref: '#/components/parameters/label' },
{ $ref: '#/components/parameters/labelColor' },
{ $ref: '#/components/parameters/color' },
{ $ref: '#/components/parameters/cacheSeconds' },
{ $ref: '#/components/parameters/link' },
],
'x-code-samples': [
{ lang: 'URL', label: 'URL', source: '$url' },
{
lang: 'Markdown',
label: 'Markdown',
source: '![OpenApiService Summary]($url)',
},
{
lang: 'reStructuredText',
label: 'rSt',
source: '.. image:: $url\n: alt: OpenApiService Summary',
},
{
lang: 'AsciiDoc',
label: 'AsciiDoc',
source: 'image:$url[OpenApiService Summary]',
},
{
lang: 'HTML',
label: 'HTML',
source: '<img alt="OpenApiService Summary" src="$url">',
},
],
},
},
'/openapi/service/{packageName}/{distTag}': {
get: {
summary: 'OpenApiService Summary (with Tag)',
description: 'OpenApiService Description (with Tag)',
parameters: [
{
name: 'packageName',
description: 'packageName description',
in: 'path',
required: true,
schema: { type: 'string' },
example: 'badge-maker',
},
{
name: 'distTag',
description: 'distTag description',
in: 'path',
required: true,
schema: { type: 'string' },
example: 'latest',
},
{ $ref: '#/components/parameters/style' },
{ $ref: '#/components/parameters/logo' },
{ $ref: '#/components/parameters/logoColor' },
{ $ref: '#/components/parameters/label' },
{ $ref: '#/components/parameters/labelColor' },
{ $ref: '#/components/parameters/color' },
{ $ref: '#/components/parameters/cacheSeconds' },
{ $ref: '#/components/parameters/link' },
],
'x-code-samples': [
{ lang: 'URL', label: 'URL', source: '$url' },
{
lang: 'Markdown',
label: 'Markdown',
source: '![OpenApiService Summary (with Tag)]($url)',
},
{
lang: 'reStructuredText',
label: 'rSt',
source:
'.. image:: $url\n: alt: OpenApiService Summary (with Tag)',
},
{
lang: 'AsciiDoc',
label: 'AsciiDoc',
source: 'image:$url[OpenApiService Summary (with Tag)]',
},
{
lang: 'HTML',
label: 'HTML',
source: '<img alt="OpenApiService Summary (with Tag)" src="$url">',
},
],
},
},
'/legacy/service/{packageName}/{distTag}': {
get: {
summary: 'LegacyService Title (with Tag)',
description: 'LegacyService Description (with Tag)',
parameters: [
{
name: 'packageName',
in: 'path',
required: true,
schema: { type: 'string' },
example: 'badge-maker',
},
{
name: 'distTag',
in: 'path',
required: true,
schema: { type: 'string' },
example: 'latest',
},
{ $ref: '#/components/parameters/style' },
{ $ref: '#/components/parameters/logo' },
{ $ref: '#/components/parameters/logoColor' },
{ $ref: '#/components/parameters/label' },
{ $ref: '#/components/parameters/labelColor' },
{ $ref: '#/components/parameters/color' },
{ $ref: '#/components/parameters/cacheSeconds' },
{ $ref: '#/components/parameters/link' },
],
'x-code-samples': [
{ lang: 'URL', label: 'URL', source: '$url' },
{
lang: 'Markdown',
label: 'Markdown',
source: '![LegacyService Title (with Tag)]($url)',
},
{
lang: 'reStructuredText',
label: 'rSt',
source: '.. image:: $url\n: alt: LegacyService Title (with Tag)',
},
{
lang: 'AsciiDoc',
label: 'AsciiDoc',
source: 'image:$url[LegacyService Title (with Tag)]',
},
{
lang: 'HTML',
label: 'HTML',
source: '<img alt="LegacyService Title (with Tag)" src="$url">',
},
],
},
},
'/legacy/service/{packageName}': {
get: {
summary: 'LegacyService Title (with Tag)',
description: 'LegacyService Description (with Tag)',
parameters: [
{
name: 'packageName',
in: 'path',
required: true,
schema: { type: 'string' },
example: 'badge-maker',
},
{ $ref: '#/components/parameters/style' },
{ $ref: '#/components/parameters/logo' },
{ $ref: '#/components/parameters/logoColor' },
{ $ref: '#/components/parameters/label' },
{ $ref: '#/components/parameters/labelColor' },
{ $ref: '#/components/parameters/color' },
{ $ref: '#/components/parameters/cacheSeconds' },
{ $ref: '#/components/parameters/link' },
],
'x-code-samples': [
{ lang: 'URL', label: 'URL', source: '$url' },
{
lang: 'Markdown',
label: 'Markdown',
source: '![LegacyService Title (with Tag)]($url)',
},
{
lang: 'reStructuredText',
label: 'rSt',
source: '.. image:: $url\n: alt: LegacyService Title (with Tag)',
},
{
lang: 'AsciiDoc',
label: 'AsciiDoc',
source: 'image:$url[LegacyService Title (with Tag)]',
},
{
lang: 'HTML',
label: 'HTML',
source: '<img alt="LegacyService Title (with Tag)" src="$url">',
},
],
},
},
},
}
function clean(obj) {
// remove any undefined values in the object
return JSON.parse(JSON.stringify(obj))
}
describe('category2openapi', function () {
it('generates an Open API spec', function () {
expect(
clean(
category2openapi({ name: 'build' }, [
OpenApiService.getDefinition(),
LegacyService.getDefinition(),
])
)
).to.deep.equal(expected)
})
})

View File

@@ -1,3 +1,4 @@
import url from 'url'
import camelcase from 'camelcase' import camelcase from 'camelcase'
import emojic from 'emojic' import emojic from 'emojic'
import Joi from 'joi' import Joi from 'joi'
@@ -9,7 +10,7 @@ import {
} from './cache-headers.js' } from './cache-headers.js'
import { isValidCategory } from './categories.js' import { isValidCategory } from './categories.js'
import { MetricHelper } from './metric-helper.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' import trace from './trace.js'
const attrSchema = Joi.object({ const attrSchema = Joi.object({
@@ -54,7 +55,7 @@ export default function redirector(attrs) {
static route = route static route = route
static examples = examples static examples = examples
static register({ camp, metricInstance }, { rasterUrl }) { static register({ app, metricInstance }, { rasterUrl }) {
const { regex, captureNames } = prepareRoute({ const { regex, captureNames } = prepareRoute({
...this.route, ...this.route,
withPng: Boolean(rasterUrl), withPng: Boolean(rasterUrl),
@@ -65,17 +66,17 @@ export default function redirector(attrs) {
ServiceClass: this, ServiceClass: this,
}) })
camp.route(regex, async (queryParams, match, end, ask) => { app.get(regex, async (req, res) => {
if (serverHasBeenUpSinceResourceCached(ask.req)) { if (serverHasBeenUpSinceResourceCached(req)) {
// Send Not Modified. // Send Not Modified.
ask.res.statusCode = 304 res.status(304)
ask.res.end() res.end()
return return
} }
const metricHandle = metricHelper.startRequest() const metricHandle = metricHelper.startRequest()
const namedParams = namedParamsForMatch(captureNames, match, this) const { namedParams, format } = paramsForReq(captureNames, req, this)
trace.logTrace( trace.logTrace(
'inbound', 'inbound',
emojic.arrowHeadingUp, emojic.arrowHeadingUp,
@@ -83,12 +84,12 @@ export default function redirector(attrs) {
route.base route.base
) )
trace.logTrace('inbound', emojic.ticket, 'Named params', namedParams) 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)) const targetPath = encodeURI(transformPath(namedParams))
trace.logTrace('validate', emojic.dart, 'Target', targetPath) 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) { if (transformQueryParams) {
const specifiedParams = queryString.parse(urlSuffix) const specifiedParams = queryString.parse(urlSuffix)
@@ -100,21 +101,18 @@ export default function redirector(attrs) {
urlSuffix = `?${outQueryString}` urlSuffix = `?${outQueryString}`
} }
// The final capture group is the extension. const baseUrl = format === 'png' ? rasterUrl : ''
const format = (match.slice(-1)[0] || '.svg').replace(/^\./, '') const redirectUrl = `${baseUrl}${targetPath}.${format}${urlSuffix}`
const redirectUrl = `${
format === 'png' ? rasterUrl : ''
}${targetPath}.${format}${urlSuffix}`
trace.logTrace('outbound', emojic.shield, 'Redirect URL', redirectUrl) trace.logTrace('outbound', emojic.shield, 'Redirect URL', redirectUrl)
ask.res.statusCode = 301 res.status(301)
ask.res.setHeader('Location', redirectUrl) res.setHeader('Location', redirectUrl)
// To avoid caching mistakes for a long time, and to make this simpler // 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. // to reason about, use the same cache semantics as the static badge.
setCacheHeadersForStaticResource(ask.res) setCacheHeadersForStaticResource(res)
ask.res.end() res.end()
metricHandle.noteResponseSent() metricHandle.noteResponseSent()
}) })

View File

@@ -1,7 +1,5 @@
import Camp from '@shields_io/camp'
import portfinder from 'portfinder'
import { expect } from 'chai' import { expect } from 'chai'
import got from '../got-test-client.js' import { ExpressTestHarness } from '../express-test-harness.js'
import redirector from './redirector.js' import redirector from './redirector.js'
describe('Redirector', function () { describe('Redirector', function () {
@@ -63,28 +61,12 @@ describe('Redirector', function () {
expect(redirector({ ...attrs, examples }).examples).to.equal(examples) expect(redirector({ ...attrs, examples }).examples).to.equal(examples)
}) })
describe('ScoutCamp integration', function () { describe('Express 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
}
})
const transformPath = ({ namedParamA }) => `/new/service/${namedParamA}` const transformPath = ({ namedParamA }) => `/new/service/${namedParamA}`
beforeEach(function () { let harness
beforeEach(async function () {
harness = new ExpressTestHarness()
const ServiceClass = redirector({ const ServiceClass = redirector({
category, category,
route, route,
@@ -92,17 +74,20 @@ describe('Redirector', function () {
dateAdded, dateAdded,
}) })
ServiceClass.register( ServiceClass.register(
{ camp }, { app: harness.app },
{ rasterUrl: 'http://raster.example.test' } { rasterUrl: 'http://raster.example.test' }
) )
await harness.start()
})
afterEach(async function () {
await harness.stop()
}) })
it('should redirect as configured', async function () { it('should redirect as configured', async function () {
const { statusCode, headers } = await got( const { statusCode, headers } = await harness.get(
`${baseUrl}/very/old/service/hello-world.svg`, '/very/old/service/hello-world.svg',
{ { followRedirect: false }
followRedirect: false,
}
) )
expect(statusCode).to.equal(301) 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 () { it('should redirect raster extensions to the canonical path as configured', async function () {
const { statusCode, headers } = await got( const { statusCode, headers } = await harness.get(
`${baseUrl}/very/old/service/hello-world.png`, '/very/old/service/hello-world.png',
{ { followRedirect: false }
followRedirect: false,
}
) )
expect(statusCode).to.equal(301) expect(statusCode).to.equal(301)
@@ -124,11 +107,9 @@ describe('Redirector', function () {
}) })
it('should forward the query params', async function () { it('should forward the query params', async function () {
const { statusCode, headers } = await got( const { statusCode, headers } = await harness.get(
`${baseUrl}/very/old/service/hello-world.svg?color=123&style=flat-square`, '/very/old/service/hello-world.svg?color=123&style=flat-square',
{ { followRedirect: false }
followRedirect: false,
}
) )
expect(statusCode).to.equal(301) expect(statusCode).to.equal(301)
@@ -138,11 +119,9 @@ describe('Redirector', function () {
}) })
it('should correctly encode the redirect URL', async function () { it('should correctly encode the redirect URL', async function () {
const { statusCode, headers } = await got( const { statusCode, headers } = await harness.get(
`${baseUrl}/very/old/service/hello%0Dworld.svg?foobar=a%0Db`, '/very/old/service/hello%0Dworld.svg?foobar=a%0Db',
{ { followRedirect: false }
followRedirect: false,
}
) )
expect(statusCode).to.equal(301) expect(statusCode).to.equal(301)
@@ -166,15 +145,13 @@ describe('Redirector', function () {
transformQueryParams, transformQueryParams,
dateAdded, dateAdded,
}) })
ServiceClass.register({ camp }, {}) ServiceClass.register({ app: harness.app }, {})
}) })
it('should forward the transformed query params', async function () { it('should forward the transformed query params', async function () {
const { statusCode, headers } = await got( const { statusCode, headers } = await harness.get(
`${baseUrl}/another/old/service/token/abc123/hello-world.svg`, '/another/old/service/token/abc123/hello-world.svg',
{ { followRedirect: false }
followRedirect: false,
}
) )
expect(statusCode).to.equal(301) expect(statusCode).to.equal(301)
@@ -184,11 +161,9 @@ describe('Redirector', function () {
}) })
it('should forward the specified and transformed query params', async function () { it('should forward the specified and transformed query params', async function () {
const { statusCode, headers } = await got( const { statusCode, headers } = await harness.get(
`${baseUrl}/another/old/service/token/abc123/hello-world.svg?color=123&style=flat-square`, '/another/old/service/token/abc123/hello-world.svg?color=123&style=flat-square',
{ { followRedirect: false }
followRedirect: false,
}
) )
expect(statusCode).to.equal(301) 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 () { it('should use transformed query params on param conflicts by default', async function () {
const { statusCode, headers } = await got( const { statusCode, headers } = await harness.get(
`${baseUrl}/another/old/service/token/abc123/hello-world.svg?color=123&style=flat-square&token=def456`, '/another/old/service/token/abc123/hello-world.svg?color=123&style=flat-square&token=def456',
{ { followRedirect: false }
followRedirect: false,
}
) )
expect(statusCode).to.equal(301) expect(statusCode).to.equal(301)
@@ -224,12 +197,10 @@ describe('Redirector', function () {
overrideTransformedQueryParams: true, overrideTransformedQueryParams: true,
dateAdded, dateAdded,
}) })
ServiceClass.register({ camp }, {}) ServiceClass.register({ app: harness.app }, {})
const { statusCode, headers } = await got( const { statusCode, headers } = await harness.get(
`${baseUrl}/override/service/token/abc123/hello-world.svg?style=flat-square&token=def456`, '/override/service/token/abc123/hello-world.svg?style=flat-square&token=def456',
{ { followRedirect: false }
followRedirect: false,
}
) )
expect(statusCode).to.equal(301) expect(statusCode).to.equal(301)

View File

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

View File

@@ -44,23 +44,29 @@ function prepareRoute({ base, pattern, format, capture, withPng }) {
return { regex, captureNames } return { regex, captureNames }
} }
function namedParamsForMatch(captureNames = [], match, ServiceClass) { function paramsForReq(captureNames = [], req, ServiceClass) {
// Assume the last match is the format, and drop match[0], which is the // In addition to the parameters declared by the service, we have one match
// entire match. // for the format.
const captures = match.slice(1, -1) const expectedNamedParamCount = Object.keys(req.params).length - 1
if (captureNames.length !== expectedNamedParamCount) {
if (captureNames.length !== captures.length) {
throw new Error( throw new Error(
`Service ${ServiceClass.name} declares incorrect number of named params ` + `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) => { 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 }) { function getQueryParamNames({ queryParamSchema }) {
@@ -77,6 +83,6 @@ export {
isValidRoute, isValidRoute,
assertValidRoute, assertValidRoute,
prepareRoute, prepareRoute,
namedParamsForMatch, paramsForReq,
getQueryParamNames, getQueryParamNames,
} }

View File

@@ -1,13 +1,25 @@
import { expect } from 'chai' import { expect } from 'chai'
import Joi from 'joi' import Joi from 'joi'
import { test, given, forCases } from 'sazerac' import { test, given } from 'sazerac'
import { import { prepareRoute, paramsForReq, getQueryParamNames } from './route.js'
prepareRoute,
namedParamsForMatch, function paramsForPath({ regex, captureNames, ServiceClass }, path) {
getQueryParamNames, // Prepare a mock express `req` object.
} from './route.js' 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 () { describe('Route helpers', function () {
const ServiceClass = { name: 'MyService' }
context('A `pattern` with a named param is declared', function () { context('A `pattern` with a named param is declared', function () {
const { regex, captureNames } = prepareRoute({ const { regex, captureNames } = prepareRoute({
base: 'foo', base: 'foo',
@@ -15,22 +27,31 @@ describe('Route helpers', function () {
queryParamSchema: Joi.object({ queryParamA: Joi.string() }).required(), queryParamSchema: Joi.object({ queryParamA: Joi.string() }).required(),
}) })
const regexExec = str => regex.exec(str) const regexExec = path => regex.exec(path)
test(regexExec, () => { test(regexExec, () => {
given('/foo/bar/bar.svg').expect(null) given('/foo/bar/bar.svg').expect(null)
}) })
const namedParams = str => const params = path =>
namedParamsForMatch(captureNames, regex.exec(str)) paramsForPath({ regex, captureNames, ServiceClass }, path)
test(namedParams, () => { test(params, () => {
forCases([ given('/foo/bar.bar.bar.svg').expect({
given('/foo/bar.bar.bar.svg'), namedParams: { namedParamA: 'bar.bar.bar' },
given('/foo/bar.bar.bar.json'), format: 'svg',
]).expect({ namedParamA: 'bar.bar.bar' }) })
given('/foo/bar.bar.bar.json').expect({
namedParams: { namedParamA: 'bar.bar.bar' },
format: 'json',
})
// This pattern catches bugs related to escaping the extension separator. // 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_svg').expect({
given('/foo/bar.bar.bar.zip').expect({ namedParamA: 'bar.bar.bar.zip' }) 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) given('/foo/bar/bar.svg').expect(null)
}) })
const namedParams = str => const params = path =>
namedParamsForMatch(captureNames, regex.exec(str)) paramsForPath({ regex, captureNames, ServiceClass }, path)
test(namedParams, () => { test(params, () => {
forCases([ given('/foo/bar.bar.bar.svg').expect({
given('/foo/bar.bar.bar.svg'), namedParams: { namedParamA: 'bar.bar.bar' },
given('/foo/bar.bar.bar.json'), format: 'svg',
]).expect({ namedParamA: 'bar.bar.bar' }) })
given('/foo/bar.bar.bar.json').expect({
namedParams: { namedParamA: 'bar.bar.bar' },
format: 'json',
})
// This pattern catches bugs related to escaping the extension separator. // 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_svg').expect({
given('/foo/bar.bar.bar.zip').expect({ namedParamA: 'bar.bar.bar.zip' }) 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 () { context('No named params are declared', function () {
const { regex, captureNames } = prepareRoute({ const { regex, captureNames } = prepareRoute({
base: 'foo', base: 'foo',
format: '(?:[^/]+)', format: '(?:[^/]+?)',
}) })
const namedParams = str => const params = path =>
namedParamsForMatch(captureNames, regex.exec(str)) paramsForPath({ regex, captureNames, ServiceClass }, path)
test(namedParams, () => { test(params, () => {
forCases([ given('/foo/bar.bar.bar.svg').expect({ namedParams: {}, format: 'svg' })
given('/foo/bar.bar.bar.svg'), given('/foo/bar.bar.bar.json').expect({ namedParams: {}, format: 'json' })
given('/foo/bar.bar.bar.json'),
]).expect({})
}) })
}) })
@@ -83,13 +112,13 @@ describe('Route helpers', function () {
capture: ['namedParamA'], capture: ['namedParamA'],
}) })
expect(() => it('Throws the expected error', function () {
namedParamsForMatch(captureNames, regex.exec('/foo/bar/baz.svg'), { expect(() =>
name: 'MyService', paramsForPath({ regex, captureNames, ServiceClass }, '/foo/bar/baz.svg')
}) ).to.throw(
).to.throw( 'Service MyService declares incorrect number of named params (expected 2, got 1)'
'Service MyService declares incorrect number of named params (expected 2, got 1)' )
) })
}) })
it('getQueryParamNames', function () { it('getQueryParamNames', function () {

View File

@@ -1,5 +1,8 @@
import Joi from 'joi' import Joi from 'joi'
// This should be kept in sync with the schema in
// `frontend/lib/service-definitions/index.ts`.
const arrayOfStrings = Joi.array().items(Joi.string()).min(0).required() const arrayOfStrings = Joi.array().items(Joi.string()).min(0).required()
const objectOfKeyValues = Joi.object() const objectOfKeyValues = Joi.object()
@@ -43,28 +46,6 @@ const serviceDefinition = Joi.object({
}) })
) )
.default([]), .default([]),
openApi: Joi.object().pattern(
/./,
Joi.object({
get: Joi.object({
summary: Joi.string().required(),
description: Joi.string().required(),
parameters: Joi.array()
.items(
Joi.object({
name: Joi.string().required(),
description: Joi.string(),
in: Joi.string().valid('query', 'path').required(),
required: Joi.boolean().required(),
schema: Joi.object({ type: Joi.string().required() }).required(),
example: Joi.string(),
})
)
.min(1)
.required(),
}).required(),
}).required()
),
}).required() }).required()
function assertValidServiceDefinition(example, message = undefined) { function assertValidServiceDefinition(example, message = undefined) {
@@ -89,4 +70,9 @@ function assertValidServiceDefinitionExport(examples, message = undefined) {
Joi.assert(examples, serviceDefinitionExport, message) Joi.assert(examples, serviceDefinitionExport, message)
} }
export { assertValidServiceDefinition, assertValidServiceDefinitionExport } export {
serviceDefinition,
assertValidServiceDefinition,
serviceDefinitionExport,
assertValidServiceDefinitionExport,
}

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