Compare commits

..

17 Commits

Author SHA1 Message Date
Marcin Mielnicki
a39c7901b5 Require default values 2020-04-21 21:09:50 +02:00
Marcin Mielnicki
a1cdd620e9 Comment added plus small refactring 2020-04-21 20:42:13 +02:00
Marcin Mielnicki
d02c3f045a Unused code removed 2020-04-20 21:35:48 +02:00
Marcin Mielnicki
06eb88eb31 Fetch from new domain in tests 2020-04-20 21:31:53 +02:00
Marcin Mielnicki
85f65734a0 Merge remote-tracking branch 'badges/master' into custom-fetch-limit 2020-04-20 21:23:42 +02:00
Marcin Mielnicki
aa185ea07c typo fix 2020-01-31 16:48:44 +01:00
Marcin Mielnicki
b3b772d95c typo fix 2020-01-24 20:50:03 +01:00
Marcin Mielnicki
670dc2bf77 Removed empty line added 2020-01-24 20:45:39 +01:00
Marcin Mielnicki
4b53ffbd3b Bytes type definition 2020-01-20 20:28:42 +01:00
Marcin Mielnicki
18ff7db947 checkCustomIntegrationConfiguration moved to config.js + tests 2020-01-20 20:15:38 +01:00
Marcin Mielnicki
cce0104ea1 Check there is no extra config 2020-01-19 20:33:04 +01:00
Marcin Mielnicki
56a30ef139 Typo fix 2020-01-18 19:58:12 +01:00
Marcin Mielnicki
46fa8adeb9 Use sazerac in config.spec.js 2020-01-12 17:34:58 +01:00
Marcin Mielnicki
f73f828aaf Merge the default configuration with a custom one 2020-01-12 17:20:15 +01:00
Marcin Mielnicki
9e7dfea103 function for configuration 2020-01-12 17:11:19 +01:00
Marcin Mielnicki
9f6f064193 Default values at the top 2020-01-11 18:23:16 +01:00
Marcin Mielnicki
d5812cbce8 Config for custom fetch limit 2020-01-10 21:10:46 +01:00
1334 changed files with 48524 additions and 76075 deletions

View File

@@ -6,9 +6,7 @@ main_steps: &main_steps
- run:
name: Install dependencies
command: |
npm install --dry-run
npm ci
command: npm ci
environment:
# https://docs.cypress.io/guides/getting-started/installing-cypress.html#Skipping-installation
# We don't need to install the Cypress binary in jobs that aren't actually running Cypress.
@@ -49,9 +47,7 @@ integration_steps: &integration_steps
- run:
name: Install dependencies
command: |
npm install --dry-run
npm ci
command: npm ci
environment:
CYPRESS_INSTALL_BINARY: 0
@@ -72,9 +68,7 @@ services_steps: &services_steps
- run:
name: Install dependencies
command: |
npm install --dry-run
npm ci
command: npm ci
environment:
CYPRESS_INSTALL_BINARY: 0
@@ -92,90 +86,107 @@ services_steps: &services_steps
- store_test_results:
path: junit
run_package_tests: &run_package_tests
when: always
command: |
# https://discuss.circleci.com/t/switch-nodejs-version-on-machine-executor-solved/26675/3
set +e
export NVM_DIR="/opt/circleci/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
nvm install $NODE_VERSION
nvm use $NODE_VERSION
node --version
npm run test:package
package_steps: &package_steps
steps:
- checkout
- run:
name: Install node and npm
name: Install dependencies
command: |
set +e
export NVM_DIR="/opt/circleci/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
nvm install v14
nvm use v14
nvm install v12
nvm use v12
npm install -g npm
npm ci
environment:
CYPRESS_INSTALL_BINARY: 0
# 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/
# https://github.com/badges/shields/blob/master/gh-badges/README.md#node-version-support
- run:
<<: *run_package_tests
environment:
mocha_reporter: mocha-junit-reporter
MOCHA_FILE: junit/badge-maker/v12/results.xml
MOCHA_FILE: junit/gh-badges/v8/results.xml
NODE_VERSION: v8
name: Run package tests on Node 8
- run:
<<: *run_package_tests
environment:
mocha_reporter: mocha-junit-reporter
MOCHA_FILE: junit/gh-badges/v10/results.xml
NODE_VERSION: v10
name: Run package tests on Node 10
- run:
<<: *run_package_tests
environment:
mocha_reporter: mocha-junit-reporter
MOCHA_FILE: junit/gh-badges/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:
npm-install:
docker:
- image: circleci/node:8
steps:
- checkout
- run:
name: Install dependencies
command: npm ci
environment:
CYPRESS_INSTALL_BINARY: 0
main:
docker:
- image: cimg/node:16.14
environment:
NPM_CONFIG_ENGINE_STRICT: 'true'
NPM_CONFIG_STRICT_PEER_DEPS: 'true'
- image: circleci/node:8
<<: *main_steps
main@node-17:
main@node-latest:
docker:
- image: cimg/node:17.7
- image: circleci/node:latest
<<: *main_steps
integration:
docker:
- image: cimg/node:16.14
environment:
NPM_CONFIG_ENGINE_STRICT: 'true'
NPM_CONFIG_STRICT_PEER_DEPS: 'true'
- image: circleci/node:8
- image: redis
<<: *integration_steps
integration@node-17:
integration@node-latest:
docker:
- image: cimg/node:17.7
- image: circleci/node:latest
- image: redis
<<: *integration_steps
danger:
docker:
- image: cimg/node:16.14
- image: circleci/node:8
steps:
- checkout
@@ -195,18 +206,13 @@ jobs:
frontend:
docker:
- image: cimg/node:16.14
environment:
NPM_CONFIG_ENGINE_STRICT: 'true'
NPM_CONFIG_STRICT_PEER_DEPS: 'true'
- image: circleci/node:8
steps:
- checkout
- run:
name: Install dependencies
command: |
npm install --dry-run
npm ci
command: npm ci
environment:
CYPRESS_INSTALL_BINARY: 0
@@ -235,32 +241,25 @@ jobs:
command: npm run build
package:
machine:
image: 'ubuntu-2004:202111-02'
machine: true
<<: *package_steps
services:
docker:
- image: cimg/node:16.14
environment:
NPM_CONFIG_ENGINE_STRICT: 'true'
NPM_CONFIG_STRICT_PEER_DEPS: 'true'
- image: circleci/node:8
<<: *services_steps
services@node-17:
services@node-latest:
docker:
- image: cimg/node:17.7
- image: circleci/node:latest
<<: *services_steps
e2e:
docker:
- image: cypress/base:16.13.0
environment:
NPM_CONFIG_ENGINE_STRICT: 'true'
NPM_CONFIG_STRICT_PEER_DEPS: 'true'
- image: cypress/base:8
steps:
- checkout
@@ -271,9 +270,7 @@ jobs:
- run:
name: Install dependencies
command: |
npm install --dry-run
npm ci
command: npm ci
- run:
name: Frontend build
@@ -311,11 +308,11 @@ workflows:
filters:
branches:
ignore: gh-pages
- main@node-17:
- main@node-latest:
filters:
branches:
ignore: gh-pages
- integration@node-17:
- integration@node-latest:
filters:
branches:
ignore: gh-pages
@@ -333,7 +330,7 @@ workflows:
ignore:
- master
- gh-pages
- services@node-17:
- services@node-latest:
filters:
branches:
ignore:

109
.dependabot/config.yml Normal file
View File

@@ -0,0 +1,109 @@
version: 1
update_configs:
# shields.io dependencies
- package_manager: 'javascript'
directory: '/'
update_schedule: 'weekly'
automerged_updates:
- match:
dependency_name: 'chai*'
update_type: 'semver:minor'
- match:
dependency_name: 'cypress'
update_type: 'semver:minor'
- match:
dependency_name: 'eslint*'
update_type: 'semver:minor'
- match:
dependency_name: 'enzyme*'
update_type: 'semver:minor'
- match:
dependency_name: 'mocha*'
update_type: 'semver:minor'
- match:
dependency_name: 'sazerac'
update_type: 'semver:minor'
- match:
dependency_name: 'sinon*'
update_type: 'semver:minor'
- match:
dependency_name: 'snap-shot-it'
update_type: 'semver:minor'
ignored_updates:
- match:
dependency_name: babel-preset-gatsby
version_requirement: '>=0.3.0'
- match:
dependency_name: camelcase
version_requirement: '>=6.0.0'
- match:
dependency_name: chalk
version_requirement: '>=4.0.0'
- match:
dependency_name: cross-env
version_requirement: '>=7.0.0'
- match:
dependency_name: decamelize
version_requirement: '>=4.0.0'
- match:
dependency_name: escape-string-regexp
version_requirement: '>=3.0.0'
- match:
dependency_name: eslint-plugin-jsdoc
version_requirement: '>=21.0.0'
- match:
dependency_name: gatsby
version_requirement: '>=2.19.50'
- match:
dependency_name: gatsby-plugin-catch-links
version_requirement: '>=2.2.0'
- match:
dependency_name: gatsby-plugin-page-creator
version_requirement: '>=2.2.0'
- match:
dependency_name: gatsby-plugin-react-helmet
version_requirement: '>=3.2.0'
- match:
dependency_name: gatsby-plugin-remove-trailing-slashes
version_requirement: '>=2.2.0'
- match:
dependency_name: gatsby-plugin-styled-components
version_requirement: '>=3.2.0'
- match:
dependency_name: gatsby-plugin-typescript
version_requirement: '>=2.3.0'
- match:
dependency_name: got
version_requirement: '>=10.0.0'
- match:
dependency_name: '@hapi/joi'
version_requirement: '>=17.0.0'
- match:
dependency_name: husky
version_requirement: '>=4.0.0'
- match:
dependency_name: lint-staged
version_requirement: '>=10.0.0'
- match:
dependency_name: nock
version_requirement: '>=12.0.0'
- match:
dependency_name: prom-client
version_requirement: '>=12.0.0'
- match:
dependency_name: react-error-overlay
version_requirement: '>=3.0.0'
- match:
dependency_name: sinon
version_requirement: '>=9.0.0'
- match:
dependency_name: start-server-and-test
version_requirement: '>=1.10.8'
- match:
dependency_name: xmldom
version_requirement: '>=0.3.0'
# gh-badges package dependencies
- package_manager: 'javascript'
directory: '/gh-badges'
update_schedule: 'weekly'

View File

@@ -2,6 +2,5 @@
/build
/coverage
/__snapshots__
public
badge-maker/node_modules/
!.github/
/public
gh-badges/node_modules/

View File

@@ -1,14 +1,13 @@
extends:
- standard
- standard-jsx
- standard-react
- plugin:@typescript-eslint/recommended
- prettier
- prettier/@typescript-eslint
- prettier/standard
- prettier/react
- eslint:recommended
globals:
JSX: 'readonly'
parserOptions:
# Override eslint-config-standard, which incorrectly sets this to "module",
# though that setting is only for ES6 modules, not CommonJS modules.
@@ -17,8 +16,6 @@ parserOptions:
settings:
react:
version: '16.8'
jsdoc:
mode: jsdoc
plugins:
- chai-friendly
@@ -43,7 +40,6 @@ overrides:
es6: true
rules:
no-console: 'off'
'@typescript-eslint/explicit-module-boundary-types': 'off'
- files:
- '**/*.@(ts|tsx)'
@@ -59,7 +55,6 @@ overrides:
'@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:
- core/**/*.ts
@@ -127,15 +122,13 @@ rules:
'@typescript-eslint/no-var-requires': 'off'
'@typescript-eslint/no-use-before-define': 'error'
no-use-before-define: 'off'
# These should be disabled by eslint-config-prettier, but are not.
no-extra-semi: 'off'
# Shields additions.
no-var: 'error'
prefer-const: 'error'
strict: 'error'
arrow-body-style: ['error', 'as-needed']
no-extension-in-require/main: 'error'
object-shorthand: ['error', 'properties']
@@ -145,20 +138,6 @@ rules:
new-cap: ['error', { 'capIsNew': true }]
import/order: ['error', { 'newlines-between': 'never' }]
# Account for destructuring responses from upstream services,
# many of which do not follow camelcase
# Based on original rule configuration from eslint-config-standard
camelcase:
[
'error',
{
ignoreDestructuring: true,
properties: 'never',
ignoreGlobals: true,
allow: ['^UNSAFE_'],
},
]
# Chai friendly.
no-unused-expressions: 'off'
chai-friendly/no-unused-expressions: 'error'
@@ -170,7 +149,7 @@ rules:
# allow Joi as an undefined type
jsdoc/no-undefined-types: ['error', { definedTypes: ['Joi'] }]
# all the other recommended rules as errors (not warnings)
# all the other reccomended rules as errors (not warnings)
jsdoc/check-alignment: 'error'
jsdoc/check-param-names: 'error'
jsdoc/check-tag-names: 'error'

29
.github/ISSUE_TEMPLATE/1_Bug_report.md vendored Normal file
View File

@@ -0,0 +1,29 @@
---
name: 🐛 Bug Report
about: Report errors and problems
labels: 'question'
---
Are you experiencing an issue with...
- [ ] [shields.io](https://shields.io/#/)
- [ ] My own instance
- [ ] [gh-badges NPM package](https://www.npmjs.com/package/gh-badges)
:beetle: **Description**
<!-- A clear and concise description of the problem. -->
:link: **Link to the badge**
<!--
If you are reporting a problem with a specific badge on shields.io,
provide a link to a badge demonstrating the error
-->
:bulb: **Possible Solution**
<!--- Optional: only if you have suggestions on a fix/reason for the bug -->
<!-- Love Shields? Please consider donating $10 to sustain our activities:
👉 https://opencollective.com/shields -->

View File

@@ -1,44 +0,0 @@
name: '🐛 Bug Report'
description: Report errors and problems
labels: [question]
body:
- type: dropdown
id: product
attributes:
label: Are you experiencing an issue with...
options:
- shields.io
- My own instance of shields
- badge-maker NPM package
validations:
required: true
- type: textarea
id: description
attributes:
label: '🐞 Description'
description: A clear and concise description of the problem.
validations:
required: true
- type: textarea
id: link
attributes:
label: '🔗 Link to the badge'
description: If you are reporting a problem with a specific badge on shields.io, provide a link to a badge demonstrating the error
validations:
required: false
- type: textarea
id: possible-solution
attributes:
label: '💡 Possible Solution'
description: 'Optional: only if you have suggestions on a fix/reason for the bug'
validations:
required: false
- 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

@@ -22,7 +22,7 @@ labels: 'keep-service-tests-green'
<!-- Provide a link to the failing test in CircleCI. -->
:lady_beetle: **Stack trace**
:beetle: **Stack trace**
```
<!-- Provide the complete stack trace from the CircleCI test summary. -->

View File

@@ -0,0 +1,17 @@
---
name: ❓ Support Question
about: Ask a question about shields.io
labels: 'question'
---
:question: **Question**
<!--
Ask your question clearly and concisely.
#support on our [Discord](https://discordapp.com/invite/HjJCwm5)
is also a great place to ask questions and get help
-->
<!-- Love Shields? Please consider donating $10 to sustain our activities:
👉 https://opencollective.com/shields -->

View File

@@ -1,7 +0,0 @@
contact_links:
- name: 🎨 Simple Icons
url: https://github.com/badges/shields/discussions/5369
about: Please read this before posting a question about SimpleIcons
- name: ❓ Support Question
url: https://github.com/badges/shields/discussions
about: Ask a question about Shields.io

View File

@@ -1,12 +0,0 @@
name: 'Auto Approve'
description: 'Automatically approve/close selected pull requests for shields.io'
branding:
icon: 'check-circle'
color: 'green'
inputs:
github-token:
description: 'The GITHUB_TOKEN secret'
required: true
runs:
using: 'node12'
main: 'index.js'

View File

@@ -1,68 +0,0 @@
'use strict'
function findChangelogStart(lines) {
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
if (
line === '<summary>Changelog</summary>' &&
lines[i + 2] === '<blockquote>'
) {
return i + 3
}
}
return null
}
function findChangelogEnd(lines, start) {
for (let i = start; i < lines.length; i++) {
const line = lines[i]
if (line === '</blockquote>') {
return i
}
}
return null
}
function allChangelogLinesAreVersionBump(changelogLines) {
return (
changelogLines.length > 0 &&
changelogLines.length ===
changelogLines.filter(line =>
line.includes('Version bump only for package')
).length
)
}
function isPointlessVersionBump(body) {
const pointlessBumpLinks = [
'https://github.com/gatsbyjs/gatsby',
'https://github.com/typescript-eslint/typescript-eslint',
]
const lines = body.split(/\r?\n/)
if (!pointlessBumpLinks.some(link => lines[0].includes(link))) {
return false
}
const start = findChangelogStart(lines)
const end = findChangelogEnd(lines, start)
if (!start || !end) {
return false
}
const changelogLines = lines
.slice(start, end)
.filter(line => !line.startsWith('<h'))
.filter(line => !line.startsWith('<p>All notable changes'))
.filter(
line => !line.startsWith('See <a href="https://conventionalcommits.org">')
)
.filter(line => !line.startsWith('<!--'))
.filter(
line =>
!line.startsWith(
'<p><a href="https://www.gatsbyjs.com/docs/reference/release-notes/'
)
)
return allChangelogLinesAreVersionBump(changelogLines)
}
module.exports = { isPointlessVersionBump }

View File

@@ -1,38 +0,0 @@
'use strict'
const core = require('@actions/core')
const github = require('@actions/github')
const { isPointlessVersionBump } = require('./helpers')
async function run() {
try {
const token = core.getInput('github-token', { required: true })
const { pull_request: pr } = github.context.payload
if (!pr) {
throw new Error('Event payload missing `pull_request`')
}
const client = github.getOctokit(token)
if (
['dependabot[bot]', 'dependabot-preview[bot]'].includes(pr.user.login)
) {
if (isPointlessVersionBump(pr.body)) {
core.debug(`Closing pull request #${pr.number}`)
await client.rest.pulls.update({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
pull_number: pr.number,
state: 'closed',
})
core.debug(`Done.`)
}
}
} catch (error) {
core.setFailed(error.message)
}
}
run()

View File

@@ -1,416 +0,0 @@
{
"name": "close-bot",
"version": "0.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "close-bot",
"version": "0.0.0",
"license": "CC0",
"dependencies": {
"@actions/core": "^1.6.0",
"@actions/github": "^5.0.1"
}
},
"node_modules/@actions/core": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.6.0.tgz",
"integrity": "sha512-NB1UAZomZlCV/LmJqkLhNTqtKfFXJZAUPcfl/zqG7EfsQdeUJtaWO98SGbuQ3pydJ3fHl2CvI/51OKYlCYYcaw==",
"dependencies": {
"@actions/http-client": "^1.0.11"
}
},
"node_modules/@actions/github": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-5.0.1.tgz",
"integrity": "sha512-JZGyPM9ektb8NVTTI/2gfJ9DL7Rk98tQ7OVyTlgTuaQroariRBsOnzjy0I2EarX4xUZpK88YyO503fhmjFdyAg==",
"dependencies": {
"@actions/http-client": "^1.0.11",
"@octokit/core": "^3.6.0",
"@octokit/plugin-paginate-rest": "^2.17.0",
"@octokit/plugin-rest-endpoint-methods": "^5.13.0"
}
},
"node_modules/@actions/http-client": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
"integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
"dependencies": {
"tunnel": "0.0.6"
}
},
"node_modules/@octokit/auth-token": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz",
"integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==",
"dependencies": {
"@octokit/types": "^6.0.3"
}
},
"node_modules/@octokit/core": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz",
"integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==",
"dependencies": {
"@octokit/auth-token": "^2.4.4",
"@octokit/graphql": "^4.5.8",
"@octokit/request": "^5.6.3",
"@octokit/request-error": "^2.0.5",
"@octokit/types": "^6.0.3",
"before-after-hook": "^2.2.0",
"universal-user-agent": "^6.0.0"
}
},
"node_modules/@octokit/endpoint": {
"version": "6.0.12",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz",
"integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==",
"dependencies": {
"@octokit/types": "^6.0.3",
"is-plain-object": "^5.0.0",
"universal-user-agent": "^6.0.0"
}
},
"node_modules/@octokit/graphql": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz",
"integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==",
"dependencies": {
"@octokit/request": "^5.6.0",
"@octokit/types": "^6.0.3",
"universal-user-agent": "^6.0.0"
}
},
"node_modules/@octokit/openapi-types": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-11.2.0.tgz",
"integrity": "sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA=="
},
"node_modules/@octokit/plugin-paginate-rest": {
"version": "2.17.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.17.0.tgz",
"integrity": "sha512-tzMbrbnam2Mt4AhuyCHvpRkS0oZ5MvwwcQPYGtMv4tUa5kkzG58SVB0fcsLulOZQeRnOgdkZWkRUiyBlh0Bkyw==",
"dependencies": {
"@octokit/types": "^6.34.0"
},
"peerDependencies": {
"@octokit/core": ">=2"
}
},
"node_modules/@octokit/plugin-rest-endpoint-methods": {
"version": "5.13.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.13.0.tgz",
"integrity": "sha512-uJjMTkN1KaOIgNtUPMtIXDOjx6dGYysdIFhgA52x4xSadQCz3b/zJexvITDVpANnfKPW/+E0xkOvLntqMYpviA==",
"dependencies": {
"@octokit/types": "^6.34.0",
"deprecation": "^2.3.1"
},
"peerDependencies": {
"@octokit/core": ">=3"
}
},
"node_modules/@octokit/request": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz",
"integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==",
"dependencies": {
"@octokit/endpoint": "^6.0.1",
"@octokit/request-error": "^2.1.0",
"@octokit/types": "^6.16.1",
"is-plain-object": "^5.0.0",
"node-fetch": "^2.6.7",
"universal-user-agent": "^6.0.0"
}
},
"node_modules/@octokit/request-error": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz",
"integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==",
"dependencies": {
"@octokit/types": "^6.0.3",
"deprecation": "^2.0.0",
"once": "^1.4.0"
}
},
"node_modules/@octokit/types": {
"version": "6.34.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.34.0.tgz",
"integrity": "sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw==",
"dependencies": {
"@octokit/openapi-types": "^11.2.0"
}
},
"node_modules/before-after-hook": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz",
"integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ=="
},
"node_modules/deprecation": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
},
"node_modules/is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
},
"node_modules/tunnel": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
"engines": {
"node": ">=0.6.11 <=0.7.0 || >=0.7.3"
}
},
"node_modules/universal-user-agent": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
}
},
"dependencies": {
"@actions/core": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.6.0.tgz",
"integrity": "sha512-NB1UAZomZlCV/LmJqkLhNTqtKfFXJZAUPcfl/zqG7EfsQdeUJtaWO98SGbuQ3pydJ3fHl2CvI/51OKYlCYYcaw==",
"requires": {
"@actions/http-client": "^1.0.11"
}
},
"@actions/github": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-5.0.1.tgz",
"integrity": "sha512-JZGyPM9ektb8NVTTI/2gfJ9DL7Rk98tQ7OVyTlgTuaQroariRBsOnzjy0I2EarX4xUZpK88YyO503fhmjFdyAg==",
"requires": {
"@actions/http-client": "^1.0.11",
"@octokit/core": "^3.6.0",
"@octokit/plugin-paginate-rest": "^2.17.0",
"@octokit/plugin-rest-endpoint-methods": "^5.13.0"
}
},
"@actions/http-client": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
"integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
"requires": {
"tunnel": "0.0.6"
}
},
"@octokit/auth-token": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz",
"integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==",
"requires": {
"@octokit/types": "^6.0.3"
}
},
"@octokit/core": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz",
"integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==",
"requires": {
"@octokit/auth-token": "^2.4.4",
"@octokit/graphql": "^4.5.8",
"@octokit/request": "^5.6.3",
"@octokit/request-error": "^2.0.5",
"@octokit/types": "^6.0.3",
"before-after-hook": "^2.2.0",
"universal-user-agent": "^6.0.0"
}
},
"@octokit/endpoint": {
"version": "6.0.12",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz",
"integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==",
"requires": {
"@octokit/types": "^6.0.3",
"is-plain-object": "^5.0.0",
"universal-user-agent": "^6.0.0"
}
},
"@octokit/graphql": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz",
"integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==",
"requires": {
"@octokit/request": "^5.6.0",
"@octokit/types": "^6.0.3",
"universal-user-agent": "^6.0.0"
}
},
"@octokit/openapi-types": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-11.2.0.tgz",
"integrity": "sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA=="
},
"@octokit/plugin-paginate-rest": {
"version": "2.17.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.17.0.tgz",
"integrity": "sha512-tzMbrbnam2Mt4AhuyCHvpRkS0oZ5MvwwcQPYGtMv4tUa5kkzG58SVB0fcsLulOZQeRnOgdkZWkRUiyBlh0Bkyw==",
"requires": {
"@octokit/types": "^6.34.0"
}
},
"@octokit/plugin-rest-endpoint-methods": {
"version": "5.13.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.13.0.tgz",
"integrity": "sha512-uJjMTkN1KaOIgNtUPMtIXDOjx6dGYysdIFhgA52x4xSadQCz3b/zJexvITDVpANnfKPW/+E0xkOvLntqMYpviA==",
"requires": {
"@octokit/types": "^6.34.0",
"deprecation": "^2.3.1"
}
},
"@octokit/request": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz",
"integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==",
"requires": {
"@octokit/endpoint": "^6.0.1",
"@octokit/request-error": "^2.1.0",
"@octokit/types": "^6.16.1",
"is-plain-object": "^5.0.0",
"node-fetch": "^2.6.7",
"universal-user-agent": "^6.0.0"
}
},
"@octokit/request-error": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz",
"integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==",
"requires": {
"@octokit/types": "^6.0.3",
"deprecation": "^2.0.0",
"once": "^1.4.0"
}
},
"@octokit/types": {
"version": "6.34.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.34.0.tgz",
"integrity": "sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw==",
"requires": {
"@octokit/openapi-types": "^11.2.0"
}
},
"before-after-hook": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz",
"integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ=="
},
"deprecation": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
},
"is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="
},
"node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"requires": {
"whatwg-url": "^5.0.0"
}
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"requires": {
"wrappy": "1"
}
},
"tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
},
"tunnel": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
},
"universal-user-agent": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
},
"webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
},
"whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
"requires": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
}
}
}

View File

@@ -1,16 +0,0 @@
{
"name": "close-bot",
"version": "0.0.0",
"description": "",
"main": "index.js",
"private": true,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "chris48s",
"license": "CC0",
"dependencies": {
"@actions/core": "^1.6.0",
"@actions/github": "^5.0.1"
}
}

View File

@@ -1,8 +0,0 @@
FROM node:12-buster
RUN apt-get update
RUN apt-get install -y jq
RUN apt-get install -y uuid-runtime
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -1,5 +0,0 @@
name: 'draft-release'
description: 'Generate a changelog and propose a release PR'
runs:
using: 'docker'
image: 'Dockerfile'

View File

@@ -1,62 +0,0 @@
#!/bin/bash
set -euxo pipefail
# Set up a git user
git config user.name "release[bot]"
git config user.email "actions@users.noreply.github.com"
# Find last server-YYYY-MM-DD tag
git fetch --unshallow --tags
LAST_TAG=$(git tag | grep server | tail -n 1)
# Find the marker in CHANGELOG.md
INSERT_POINT=$(grep -n "^\-\-\-$" CHANGELOG.md | cut -f1 -d:)
INSERT_POINT=$((INSERT_POINT+1))
# Generate a release name
RELEASE_NAME="server-$(date --rfc-3339=date)"
# Assemble changelog entry
rm -f temp-changes.txt
touch temp-changes.txt
{
echo "## $RELEASE_NAME"
echo ""
git log "$LAST_TAG"..HEAD --no-merges --oneline --pretty="format:- %s" --perl-regexp --author='^((?!dependabot).*)$'
echo $'\n- Dependency updates\n'
} >> temp-changes.txt
BASE_URL="https:\/\/github.com\/badges\/shields\/issues\/"
sed -r -i "s/\((\#)([0-9]+)\)$/\[\1\2\]\($BASE_URL\2\)/g" temp-changes.txt
# Write the changelog
sed -i "${INSERT_POINT} r temp-changes.txt" CHANGELOG.md
# Cleanup
rm temp-changes.txt
# Run prettier (to ensure the markdown file doesn't fail CI)
npx prettier@$(cat package.json | jq -r .devDependencies.prettier) --write "CHANGELOG.md"
# Generate a unique branch name
BRANCH_NAME="$RELEASE_NAME"-$(uuidgen | head -c 8)
git checkout -b "$BRANCH_NAME"
# Commit + push changelog
git add CHANGELOG.md
git commit -m "Update Changelog"
git push origin "$BRANCH_NAME"
# Submit a PR
TITLE="Changelog for Release $RELEASE_NAME"
PR_RESP=$(curl https://api.github.com/repos/"$REPO_NAME"/pulls \
-X POST \
-H "Authorization: token $GITHUB_TOKEN" \
--data '{"title": "'"$TITLE"'", "body": "'"$TITLE"'", "head": "'"$BRANCH_NAME"'", "base": "master"}')
# Add the 'release' label to the PR
PR_API_URL=$(echo "$PR_RESP" | jq -r ._links.issue.href)
curl "$PR_API_URL" \
-X POST \
-H "Authorization: token $GITHUB_TOKEN" \
--data '{"labels":["release"]}'

View File

@@ -1,38 +0,0 @@
version: 2
updates:
# shields.io dependencies
- package-ecosystem: npm
directory: '/'
schedule:
interval: weekly
day: friday
time: '12:00'
open-pull-requests-limit: 99
ignore:
# https://github.com/badges/shields/issues/7324
# https://github.com/badges/shields/issues/7447
# we're stuck with these versions until Safari is compatible with lookbehind regex syntax
# https://caniuse.com/js-regexp-lookbehind
- dependency-name: 'decamelize'
- dependency-name: 'humanize-string'
# https://github.com/badges/shields/pull/7288#issuecomment-974699240
- dependency-name: '@types/node'
# badge-maker package dependencies
- package-ecosystem: npm
directory: '/badge-maker'
schedule:
interval: weekly
day: friday
time: '12:00'
open-pull-requests-limit: 99
# close-bot package dependencies
- package-ecosystem: npm
directory: '/.github/actions/close-bot'
schedule:
interval: weekly
day: friday
time: '12:00'
open-pull-requests-limit: 99

10
.github/probot.js vendored Normal file
View File

@@ -0,0 +1,10 @@
on('pull_request.closed')
.filter(context => context.payload.pull_request.merged)
.filter(
context =>
context.payload.pull_request.head.ref.slice(0, 11) !== 'dependabot/'
)
.filter(context => context.payload.pull_request.base.ref === 'master')
.comment(`This pull request was merged to [{{ pull_request.base.ref }}]({{ repository.html_url }}/tree/{{ pull_request.base.ref }}) branch. This change is now waiting for deployment, which will usually happen within a few days. Stay tuned by joining our \`#ops\` channel on [Discord](https://discordapp.com/invite/HjJCwm5)!
After deployment, changes are copied to [gh-pages]({{ repository.html_url }}/tree/gh-pages) branch: ![](https://img.shields.io/github/commit-status/{{ repository.full_name }}/gh-pages/{{ pull_request.merge_commit_sha }}.svg?label=deploy%20status)`)

View File

@@ -1,7 +0,0 @@
<!--
Be sure to review our Contributing guidelines to help streamline the merging of your PR!
* PR title conventions - https://github.com/badges/shields/blob/master/CONTRIBUTING.md#running-service-tests-in-pull-requests
* Code formatting - https://github.com/badges/shields/blob/master/CONTRIBUTING.md#prettier
* Merge processes and reminders - https://github.com/badges/shields/blob/master/CONTRIBUTING.md#pull-requests
-->

10
.github/workflows/auto-approve.yml vendored Normal file
View File

@@ -0,0 +1,10 @@
name: Auto approve
on: pull_request
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: chris48s/approve-bot@1.0.0
with:
github-token: '${{ secrets.GITHUB_TOKEN }}'

View File

@@ -1,20 +0,0 @@
name: Auto close
on: pull_request_target
permissions:
pull-requests: write
jobs:
build:
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install action dependencies
run: cd .github/actions/close-bot && npm ci
- uses: ./.github/actions/close-bot
with:
github-token: '${{ secrets.GITHUB_TOKEN }}'

View File

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

View File

@@ -1,49 +0,0 @@
name: Create Release
on:
pull_request:
types: [closed]
jobs:
create-release:
if: |
github.event_name == 'pull_request' &&
github.event.action == 'closed' &&
github.event.pull_request.merged == true &&
contains(github.event.pull_request.labels.*.name, 'release')
runs-on: ubuntu-latest
steps:
- name: Get current date
id: date
run: echo "::set-output name=date::$(date --rfc-3339=date)"
- name: Checkout branch "master"
uses: actions/checkout@v2
with:
ref: 'master'
- name: Tag release in GitHub
uses: tvdias/github-tagger@v0.0.2
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
tag: server-${{ steps.date.outputs.date }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push snapshot release to DockerHub
uses: docker/build-push-action@v2
with:
context: .
push: true
tags: shieldsio/shields:server-${{ steps.date.outputs.date }}
build-args: |
version=server-${{ steps.date.outputs.date }}

View File

@@ -1,26 +0,0 @@
name: Deploy Documentation
on:
push:
branches:
- master
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2.3.1
with:
persist-credentials: false
- name: Build
run: |
npm ci
npm run build-docs
- name: Deploy
uses: JamesIves/github-pages-deploy-action@3.7.1
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: gh-pages
FOLDER: api-docs
CLEAN: true

View File

@@ -1,19 +0,0 @@
name: Draft Release
on:
schedule:
- cron: '0 1 1 * *'
# At 01:00 on the first day of every month
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Draft Release
uses: ./.github/actions/draft-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO_NAME: ${{ github.repository }}

View File

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

View File

@@ -1,33 +0,0 @@
name: Build and Publish Next Docker Image
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set Git Short SHA
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
push: true
tags: shieldsio/shields:next
build-args: |
version=${{ env.SHORT_SHA }}

5
.gitignore vendored
View File

@@ -7,7 +7,7 @@
/private
/index.html
/shields.env
badge-maker/package-lock.json
gh-badges/package-lock.json
# Folder view configuration files
.DS_Store
@@ -104,8 +104,7 @@ service-definitions.yml
!/config/local*.template.yml
# Gatsby
/frontend/.cache
/frontend/public
/.cache
/public
# Cypress

View File

@@ -3,3 +3,4 @@ require:
- '@babel/polyfill'
- '@babel/register'
- mocha-yaml-loader
- frontend/mocha-ignore-pngs

30
.nowignore Normal file
View File

@@ -0,0 +1,30 @@
*
!frontend/
!gh-badges/
!lib/
!core/
!logo/
!pages/
!public/
!templates/
!services/
!package-lock.json
!/*.js
!scripts/export-*.js
!config/
config/local*.yml
*.spec.js
*~
.env
.circleci
.github
.vscode
__snapshots__
.buildpacks
.eslint*
.editorconfig
.nycrc*
.gitpod*
.prettier*
CONTRIBUTING.md
Dockerfile

View File

@@ -10,7 +10,6 @@
"**/*-test-helpers.js",
"**/*-fixtures.js",
"**/mocha-*.js",
"**/*.test-d.ts",
"dangerfile.js",
"gatsby-*.js",
"core/service-test-runner",
@@ -22,7 +21,6 @@
"scripts",
"coverage",
"build",
".github",
"**/public/"
".github"
]
}

View File

@@ -2,13 +2,14 @@ package.json
package-lock.json
/__snapshots__
/.next
.cache
/.cache
/api-docs
/build
public
/public
/coverage
private/*.json
/.nyc_output
analytics.json
gh-badges/templates/default-template.json
supported-features.json
service-definitions.yml

View File

@@ -1,5 +1,5 @@
semi: false
singleQuote: true
trailingComma: es5
bracketSpacing: true
endOfLine: lf
arrowParens: avoid

View File

@@ -1,3 +1,7 @@
{
"recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
"recommendations": [
"esbenp.prettier-vscode",
"EditorConfig.EditorConfig",
"dbaeumer.vscode-eslint"
]
}

View File

@@ -1,215 +0,0 @@
# Changelog
Note: this changelog is for the shields.io server. The changelog for the badge-maker NPM package is at https://github.com/badges/shields/blob/master/badge-maker/CHANGELOG.md
---
## server-2022-04-03
- Breaking change: This release updates ioredis from v4 to v5.
If you are using redis for GitHub token pooling, redis connection strings of the form
`redis://junkusername:authpassword@example.com:1234` will need to be updated to
`redis://:authpassword@example.com:1234`. See the
[ioredis upgrade guide](https://github.com/luin/ioredis/wiki/Upgrading-from-v4-to-v5)
for further details.
- fix installation issue on npm >= 8.5.5 [#7809](https://github.com/badges/shields/issues/7809)
- two fixes for [packagist] schemas [#7782](https://github.com/badges/shields/issues/7782)
- allow requireCloudflare setting to work when hosted on fly.io [#7781](https://github.com/badges/shields/issues/7781)
- fix [pypi] badges when package has null license [#7761](https://github.com/badges/shields/issues/7761)
- Add a [pub] publisher badge [#7715](https://github.com/badges/shields/issues/7715)
- Switch Steam file size badge to informational color [#7722](https://github.com/badges/shields/issues/7722)
- Make W3C and Youtube documentation links clickable [#7721](https://github.com/badges/shields/issues/7721)
- Improve Wercker examples [#7720](https://github.com/badges/shields/issues/7720)
- Improve Cirrus CI examples [#7719](https://github.com/badges/shields/issues/7719)
- Support [CodeClimate] responses with multiple data items [#7716](https://github.com/badges/shields/issues/7716)
- Delete [TeamCityCoverage] and [BowerVersion] redirectors [#7718](https://github.com/badges/shields/issues/7718)
- Deprecate [Shippable] service [#7717](https://github.com/badges/shields/issues/7717)
- fix: restore version comparison updates from #4173 [#4254](https://github.com/badges/shields/issues/4254)
- [piwheels], filter out versions with no files [#7696](https://github.com/badges/shields/issues/7696)
- set a longer cacheLength on [librariesio] badges [#7692](https://github.com/badges/shields/issues/7692)
- improve python version formatting [#7682](https://github.com/badges/shields/issues/7682)
- Clarify GitHub All Contributors badge [#7690](https://github.com/badges/shields/issues/7690)
- Support [HexPM] packages with no stable release [#7685](https://github.com/badges/shields/issues/7685)
- Add Test at Scale Badge [#7612](https://github.com/badges/shields/issues/7612)
- [packagist] api v2 support [#7681](https://github.com/badges/shields/issues/7681)
- Add [piwheels] version badge [#7656](https://github.com/badges/shields/issues/7656)
- Dependency updates
## server-2022-03-01
- Add [Conan] version service (#7460)
- remove suspended [github] tokens from the pool [#7654](https://github.com/badges/shields/issues/7654)
- generate links without trailing : if port not set [#7655](https://github.com/badges/shields/issues/7655)
- Use the latest build status when checking docs.rs [#7613](https://github.com/badges/shields/issues/7613)
- Remove no download handling and add API warning to [Wordpress] badges [#7606](https://github.com/badges/shields/issues/7606)
- set a higher default cacheLength on rating/star category [#7587](https://github.com/badges/shields/issues/7587)
- Update [amo] to use v4 API, set custom `cacheLength`s [#7586](https://github.com/badges/shields/issues/7586)
- fix(amo): include trailing slash in API call [#7585](https://github.com/badges/shields/issues/7585)
- fix docker image user agent [#7582](https://github.com/badges/shields/issues/7582)
- Delete deprecated Codetally and continuousphp services [#7572](https://github.com/badges/shields/issues/7572)
- Deprecate [Requires] service [#7571](https://github.com/badges/shields/issues/7571)
- [AUR] Fix RPC URL [#7570](https://github.com/badges/shields/issues/7570)
- Dependency updates
## server-2022-02-01
- [Depfu] Add support for Gitlab [#7475](https://github.com/badges/shields/issues/7475)
- replace label in hn-user-karma with U/ [#7500](https://github.com/badges/shields/issues/7500)
- Support [Feedz] response with multiple pages without items [#7476](https://github.com/badges/shields/issues/7476)
- revert decamelize and humanize-string to old versions [#7449](https://github.com/badges/shields/issues/7449)
- Dependency updates
## server-2022-01-01
- minor [reddit] improvements [#7436](https://github.com/badges/shields/issues/7436)
- [HackerNews] Show User Karma [#7411](https://github.com/badges/shields/issues/7411)
- [YouTube] Drop support for removed dislikes [#7410](https://github.com/badges/shields/issues/7410)
- change closed GitHub issue color to purple [#7374](https://github.com/badges/shields/issues/7374)
- restore cors header injection from #4171 [#4255](https://github.com/badges/shields/issues/4255)
- [GithubPackageJson] Get version from monorepo subfolder package.json [#7350](https://github.com/badges/shields/issues/7350)
- Dependency updates
## server-2021-12-01
- Send better user-agent values [#7309](https://github.com/badges/shields/issues/7309)
Self-hosting users now send a user agent which indicates the server version and starts `shields (self-hosted)/` by default.
This can be configured using the env var `USER_AGENT_BASE`
- upgrade to node 16 [#7271](https://github.com/badges/shields/issues/7271)
- feat: deprecate dependabot badges [#7274](https://github.com/badges/shields/issues/7274)
- fix: npmversion tagged service test [#7269](https://github.com/badges/shields/issues/7269)
- feat: create new Test Results category [#7218](https://github.com/badges/shields/issues/7218)
- Migration from Request to Got for all HTTP requests is completed in this release
- Dependency updates
## server-2021-11-04
- migrate regularUpdate() from request-->got [#7215](https://github.com/badges/shields/issues/7215)
- migrate github badges to use got instead of request; affects [github librariesio] [#7212](https://github.com/badges/shields/issues/7212)
- deprecate David badges [#7197](https://github.com/badges/shields/issues/7197)
- fix: ensure libraries.io header values are processed numerically [#7196](https://github.com/badges/shields/issues/7196)
- Add authentication for Libraries.io-based badges, run [Libraries Bower] [#7080](https://github.com/badges/shields/issues/7080)
- fixes and tests for pipenv helpers [#7194](https://github.com/badges/shields/issues/7194)
- add GitLab Release badge, run all [GitLab] [#7021](https://github.com/badges/shields/issues/7021)
- set content-length header on badge responses [#7179](https://github.com/badges/shields/issues/7179)
- fix [github] release/tag/download schema [#7170](https://github.com/badges/shields/issues/7170)
- Supported nested groups on [GitLabPipeline] badge [#7159](https://github.com/badges/shields/issues/7159)
- Support nested groups on [GitLabTag] badge [#7158](https://github.com/badges/shields/issues/7158)
- Fixing incorrect JetBrains Plugin rating values for [JetBrainsRating] [#7140](https://github.com/badges/shields/issues/7140)
- support using release or tag name in [GitHub] Release version badge [#7075](https://github.com/badges/shields/issues/7075)
- feat: support branches in sonar badges [#7065](https://github.com/badges/shields/issues/7065)
- Add [Modrinth] total downloads badge [#7132](https://github.com/badges/shields/issues/7132)
- remove [github] admin routes [#7105](https://github.com/badges/shields/issues/7105)
- Dependency updates
## server-2021-10-04
- feat: add 2021 support to GitHub Hacktoberfest [#7086](https://github.com/badges/shields/issues/7086)
- Add [ClearlyDefined] service [#6944](https://github.com/badges/shields/issues/6944)
- handle null licenses in crates.io response schema, run [crates] [#7074](https://github.com/badges/shields/issues/7074)
- [OBS] add Open Build Service service-badge [#6993](https://github.com/badges/shields/issues/6993)
- Correction of badges url in self-hosting configuration with a custom port. Issue 7025 [#7036](https://github.com/badges/shields/issues/7036)
- fix: support gitlab token via env var [#7023](https://github.com/badges/shields/issues/7023)
- Add API-based support for [GitLab] badges, add new GitLab Tag badge [#6988](https://github.com/badges/shields/issues/6988)
- [freecodecamp]: allow + symbol in username [#7016](https://github.com/badges/shields/issues/7016)
- Rename Riot to Element in Matrix badge help [#6996](https://github.com/badges/shields/issues/6996)
- Fixed Reddit Negative Karma Issue [#6992](https://github.com/badges/shields/issues/6992)
- Dependency updates
## server-2021-09-01
- use multi-stage build to reduce size of docker images [#6938](https://github.com/badges/shields/issues/6938)
- remove disableStrictSsl param from [jenkins] [#6887](https://github.com/badges/shields/issues/6887)
- refactor(GitHubCommitActivity): switch to v4/GraphQL API [#6959](https://github.com/badges/shields/issues/6959)
- feat: add freecodecamp badge [#6958](https://github.com/badges/shields/issues/6958)
- use the right version of NPM in docker build [#6941](https://github.com/badges/shields/issues/6941)
- [TwitchExtensionVersion] New badge [#6900](https://github.com/badges/shields/issues/6900)
- enforce strict SSL checking for [coverity] [#6886](https://github.com/badges/shields/issues/6886)
- Update self hosting docs [#6877](https://github.com/badges/shields/issues/6877)
- Support optionalDependencies in [GithubPackageJson] [#6749](https://github.com/badges/shields/issues/6749)
- Dependency updates
## server-2021-08-01
- use v5 API for [AUR] badges [#6836](https://github.com/badges/shields/issues/6836)
- [Sonar] Fix invalid fetch query to sonarqube >=6.6 [#6636](https://github.com/badges/shields/issues/6636)
- Delegate discord logo to simple-icons, which matches the current branding [#6764](https://github.com/badges/shields/issues/6764)
- Re-apply 'Migrate request to got (part 1)' [#6755](https://github.com/badges/shields/issues/6755)
- Delete old deprecated badges [#6756](https://github.com/badges/shields/issues/6756)
- Replace opn-cli with open-cli [#6747](https://github.com/badges/shields/issues/6747)
- Verify that Node 14 is installed in development [#6748](https://github.com/badges/shields/issues/6748)
- Migrate from CommonJS to ESM [#6651](https://github.com/badges/shields/issues/6651)
- Add Wikiapiary Extension Badge [WikiapiaryInstalls] [#6678](https://github.com/badges/shields/issues/6678)
- deprecate [beerpay] [#6708](https://github.com/badges/shields/issues/6708)
- deprecate [microbadger] [#6709](https://github.com/badges/shields/issues/6709)
- [npmsioscore] Support npm score [#6630](https://github.com/badges/shields/issues/6630)
- Add [Weblate] badges [#6677](https://github.com/badges/shields/issues/6677)
- Dependency updates
## server-2021-07-01
- improve [MavenCentral], [MavenMetadata], and [GradlePluginPortal] [#6628](https://github.com/badges/shields/issues/6628)
- fix: fix regex to match [codecov]'s flags [#6655](https://github.com/badges/shields/issues/6655)
- fix usage style [#6638](https://github.com/badges/shields/issues/6638)
- update simple-icons to v5 with by-name lookup backwards compatibility [#6591](https://github.com/badges/shields/issues/6591)
- [GradlePluginPortal] add gradle plugin portal [#6449](https://github.com/badges/shields/issues/6449)
- upgrade some vulnerable packages [#6569](https://github.com/badges/shields/issues/6569)
- increase max-age for download and social badges [#6567](https://github.com/badges/shields/issues/6567)
- Dependency updates
## server-2021-06-01
- Changed creating badges to open a new Window/Tab [#6536](https://github.com/badges/shields/issues/6536)
- Make for-the-badge letter spacing more predictable, and rewrite layout logic [#5754](https://github.com/badges/shields/issues/5754)
- deprecate DockerBuild service [#6529](https://github.com/badges/shields/issues/6529)
- Remove rate limiting functionality [#6513](https://github.com/badges/shields/issues/6513)
- [GitHub] Move to 'funding' category [#5846](https://github.com/badges/shields/issues/5846)
- Add GitHub discussions total badge [GithubTotalDiscussions] [#6472](https://github.com/badges/shields/issues/6472)
- Add optional query parameter (include_prereleases) to [GemVersion] [#6451](https://github.com/badges/shields/issues/6451)
- Add [PingPong] Service [#6327](https://github.com/badges/shields/issues/6327)
- Dependency updates
## server-2021-05-01
- Add setting which allows setting a timeout on HTTP requests
This is configured with the new `REQUEST_TIMEOUT_SECONDS` setting. If a request takes longer
than this number of seconds a `408 Request Timeout` response will be returned.
- Deprecate [Bintray] service [#6423](https://github.com/badges/shields/issues/6423)
- Support git hash in [nexus] SNAPSHOT version [#6369](https://github.com/badges/shields/issues/6369)
- Replace 4183C4 with blue [#6366](https://github.com/badges/shields/issues/6366)
- [Youtube] Added channel view count and subscriber count badges [#6333](https://github.com/badges/shields/issues/6333)
- Fix Netlify badge by adding new color schema [#6340](https://github.com/badges/shields/issues/6340)
- [REUSE] Add service badges [#6330](https://github.com/badges/shields/issues/6330)
- Dependency updates
## server-2021-04-01
- Use NPM packages to provide fonts instead of Google Fonts [#6274](https://github.com/badges/shields/issues/6274)
- Prevent duplication of parameters in badge examples [#6272](https://github.com/badges/shields/issues/6272)
- Add docs for all types of releases [#6210](https://github.com/badges/shields/issues/6210)
- refresh self-hosting docs [#6273](https://github.com/badges/shields/issues/6273)
- allow missing 'goal' key in [liberapay] badges [#6258](https://github.com/badges/shields/issues/6258)
- use got to push influx metrics [#6257](https://github.com/badges/shields/issues/6257)
- Dependency updates
## server-2021-03-01
- ensure redirect target path is correctly encoded [#6229](https://github.com/badges/shields/issues/6229)
- [SecurityHeaders] Added a possibility for no follow redirects [#6212](https://github.com/badges/shields/issues/6212)
- catch URL parse error in [endpoint] badge [#6214](https://github.com/badges/shields/issues/6214)
- [Homebrew] Add homebrew downloads badge [#6209](https://github.com/badges/shields/issues/6209)
- update [pkgreview] url [#6189](https://github.com/badges/shields/issues/6189)
- Make [Twitch] a social badge [#6183](https://github.com/badges/shields/issues/6183)
- update [flathub] error handling [#6185](https://github.com/badges/shields/issues/6185)
- Add [Testspace] badges [#6162](https://github.com/badges/shields/issues/6162)
- Dependency updates
## server-2021-02-01
- Docs.rs badge (#6098)
- Fix feedz service in case the package page gets paginated (#6101)
- [Bitbucket] Server Adding Auth Tokens and Resolving Pull Request api … (#6076)
- Dependency updates
## server-2021-01-18
- Gotta start somewhere

View File

@@ -1,129 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
![](https://img.shields.io/badge/conduct-%40shields.io-blue) or directly to [@calebcartwright](https://github.com/calebcartwright) ![](https://img.shields.io/badge/caleb-%40shields.io-blue) or [@paulmelnikow](https://github.com/paulmelnikow) ![](https://img.shields.io/badge/paul-%40shields.io-blue)
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@@ -49,9 +49,8 @@ simple changes, like badge additions. These are usually tagged with
Please review [these impeccable guidelines][code review guidelines].
You can monitor [issues][], [discussions][] and the [chat room][], and help
other people who have questions about contributing to Shields, or using it
for their projects.
You can monitor [issues][] and the [chat room][], and help other people who
have questions about contributing to Shields, or using it for their projects.
Feel free to reach out to one of the [maintainers][]
if you need help getting started.
@@ -59,7 +58,6 @@ if you need help getting started.
[service badge pr tag]: https://github.com/badges/shields/pulls?q=is%3Apr+is%3Aopen+label%3Aservice-badge
[code review guidelines]: https://kickstarter.engineering/a-guide-to-mindful-communication-in-code-reviews-48aab5282e5e
[issues]: https://github.com/badges/shields/issues
[discussions]: https://github.com/badges/shields/discussions
[chat room]: https://discordapp.com/invite/HjJCwm5
[maintainers]: https://github.com/badges/shields#project-leaders
@@ -88,9 +86,9 @@ We're also asking for [one-time \$10 donations](https://opencollective.com/shiel
There are three places to get help:
1. If you're new to the project, a good place to start is the [tutorial][].
2. If you need help getting started or implementing a change, [start a discussion][discussions]
2. If you need help getting started or implementing a change, [open an issue][]
with your question. We promise it's okay to do that. If there is already an
issue open for the feature you're working on, you can post there directly.
issue open for the feature you're working on, you can post there.
3. You can also join the [chat room][] and ask your question there.
[tutorial]: doc/TUTORIAL.md
@@ -98,18 +96,11 @@ There are three places to get help:
## Badge guidelines
- Shields.io hosts integrations for services which are primarily
used by developers or which are widely used by developers.
used by developers or which are widely used by developers
- The left-hand side of a badge should not advertise. It should be a lowercase _noun_
succinctly describing the meaning of the right-hand side.
- Except for badges using the `social` style, logos and links should be _turned off by
- Except for badges using the `social` style, logos should be _turned off by
default_.
- Badges should not obtain data from undocumented or reverse-engineered API endpoints.
- Badges should not obtain data by scraping web pages - these are likely to break frequently.
Whereas API publishers are incentivised to maintain a stable platform for their users,
authors of web pages have no such incentive.
- Badges may require users to specify a token in the badge URL as long it is scoped only to
fetching information and doesn't expose any sensitive information. Generating a token with the
correct scope must be clearly documented.
## Badge URLs
@@ -131,7 +122,13 @@ Prettier before a commit by default.
### Tests
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].
When opening a pull request, include your service name in brackets in the pull
request title. That way, those service tests will run in CI.
e.g. **[Travis] Fix timeout issues**
When changing other code, please add unit tests.
To run the integration tests, you must have redis installed and in your PATH.
@@ -147,35 +144,3 @@ There is a [High-level code walkthrough](doc/code-walkthrough.md) describing the
### Logos
We have [documentation for logo usage](doc/logos.md) which includes [contribution guidance](doc/logos.md#contributing-logos)
## Pull Requests
All code changes are incorporated via pull requests, and pull requests are always squashed into a single commit on merging. Therefore there's no requirement to squash commits within your PR, but feel free to squash or restructure the commits on your PR branch if you think it will be helpful. PRs with well structured commits are always easier to review!
Because all changes are pulled into the main branch via squash merges from PRs, we do **not** support overwriting any aspects of the git history once it hits our main branch. Notably this means we do not support amending commit messages, nor adjusting commit author information once merged.
Accordingly, it is the responsibility of contributors to review this type of information and adjust as needed before marking PRs as ready for review and merging.
You can review and modify your local [git configuration][git-config] via `git config`, and also find more information about amending your commit messages [here][amending-commits].
[git-config]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration
[amending-commits]: https://docs.github.com/en/github/committing-changes-to-your-project/changing-a-commit-message#rewriting-the-most-recent-commit-message
### Running service tests in pull requests
The affected service names must be included in square brackets in the pull request title so that the CI engine will run those service tests. When a pull request affects multiple services, they should be separated with spaces. The test runner is case-insensitive, so they should be capitalized for readability.
For example:
- **[Travis] Fix timeout issues**
- **[Travis Sonar] Support user token authentication**
- **Add tests for [CRAN] and [CPAN]**
Note that many services are part of a "family" of related services. Depending on the changes in your PR you may need to run the tests for just a single service, or for _all_ the services within a family.
For example, a PR title of **[GitHubForks] Foo** will only run the service tests specifically for the GitHub Forks badge, whereas a title of **[GitHub] Foo** will run the service tests for all of the GitHub badges.
In the rare case when it's necessary to see the output of a full service-test
run in a PR (all 2,000+ tests), include `[*****]` in the title. Unless all the tests pass, the build
will fail, so likely it will be necessary to remove it and re-run the tests
before merging.

View File

@@ -1,15 +1,13 @@
FROM node:16-alpine AS Builder
FROM node:8-alpine
RUN mkdir -p /usr/src/app
RUN mkdir /usr/src/app/private
WORKDIR /usr/src/app
COPY package.json package-lock.json /usr/src/app/
# Without the badge-maker package.json and CLI script in place, `npm ci` will fail.
COPY badge-maker /usr/src/app/badge-maker/
# Without the gh-badges package.json and CLI script in place, `npm ci` will fail.
COPY gh-badges /usr/src/app/gh-badges/
RUN apk add python3 make g++
RUN npm install -g "npm@>=7"
# We need dev deps to build the front end. We don't need Cypress, though.
RUN NODE_ENV=development CYPRESS_INSTALL_BINARY=0 npm ci
@@ -18,20 +16,9 @@ RUN npm run build
RUN npm prune --production
RUN npm cache clean --force
# Use multi-stage build to reduce size
FROM node:16-alpine
ARG version=dev
ENV DOCKER_SHIELDS_VERSION=$version
LABEL version=$version
LABEL fly.version=$version
# Run the server using production configs.
ENV NODE_ENV production
WORKDIR /usr/src/app
COPY --from=Builder /usr/src/app /usr/src/app
CMD node server
EXPOSE 80

70
Makefile Normal file
View File

@@ -0,0 +1,70 @@
SHELL:=/bin/bash
SERVER_TMP=${TMPDIR}shields-server-deploy
FRONTEND_TMP=${TMPDIR}shields-frontend-deploy
# This branch is reserved for the deploy process and should not be used for
# development. The deploy script will clobber it. To avoid accidentally
# pushing secrets to GitHub, this branch is configured to reject pushes.
WORKING_BRANCH=server-deploy-working-branch
all: test
deploy: deploy-s0 deploy-s1 deploy-s2 clean-server-deploy deploy-gh-pages deploy-gh-pages-clean
deploy-s0: prepare-server-deploy push-s0
deploy-s1: prepare-server-deploy push-s1
deploy-s2: prepare-server-deploy push-s2
prepare-server-deploy:
# Ship a copy of the front end to each server for debugging.
# https://github.com/badges/shields/issues/1220
INCLUDE_DEV_PAGES=false \
npm run build
rm -rf ${SERVER_TMP}
git worktree prune
git worktree add -B ${WORKING_BRANCH} ${SERVER_TMP}
cp -r public ${SERVER_TMP}
git -C ${SERVER_TMP} add -f public/
git -C ${SERVER_TMP} commit --no-verify -m '[DEPLOY] Add frontend for debugging'
cp config/local-shields-io-production.yml ${SERVER_TMP}/config/
git -C ${SERVER_TMP} add -f config/local-shields-io-production.yml
git -C ${SERVER_TMP} commit --no-verify -m '[DEPLOY] MUST NOT BE ON GITHUB'
clean-server-deploy:
rm -rf ${SERVER_TMP}
git worktree prune
push-s0:
git push -f s0 ${WORKING_BRANCH}:master
push-s1:
git push -f s1 ${WORKING_BRANCH}:master
push-s2:
git push -f s2 ${WORKING_BRANCH}:master
deploy-gh-pages:
rm -rf ${FRONTEND_TMP}
git worktree prune
GATSBY_BASE_URL=https://img.shields.io \
INCLUDE_DEV_PAGES=false \
npm run build
git worktree add -B gh-pages ${FRONTEND_TMP}
git -C ${FRONTEND_TMP} ls-files | xargs git -C ${FRONTEND_TMP} rm
git -C ${FRONTEND_TMP} commit --no-verify -m '[DEPLOY] Completely clean the index'
cp -r public/* ${FRONTEND_TMP}
echo shields.io > ${FRONTEND_TMP}/CNAME
touch ${FRONTEND_TMP}/.nojekyll
git -C ${FRONTEND_TMP} add .
git -C ${FRONTEND_TMP} commit --no-verify -m '[DEPLOY] Add built site'
git push -f origin gh-pages
deploy-gh-pages-clean:
rm -rf ${FRONTEND_TMP}
git worktree prune
test:
npm test
.PHONY: all deploy prepare-server-deploy clean-server-deploy deploy-s0 deploy-s1 deploy-s2 push-s0 push-s1 push-s2 deploy-gh-pages deploy-gh-pages-clean deploy-heroku setup redis test

118
README.md
View File

@@ -1,5 +1,5 @@
<p align="center">
<img src="https://raw.githubusercontent.com/badges/shields/master/readme-logo.svg?sanitize=true"
<img src="https://raw.githubusercontent.com/badges/shields/master/frontend/images/logo.svg?sanitize=true"
height="130">
</p>
<p align="center">
@@ -22,6 +22,9 @@
<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://github.com/badges/shields/compare/gh-pages...master">
<img src="https://img.shields.io/github/commits-since/badges/shields/gh-pages?label=commits%20to%20be%20deployed"
alt="commits to be deployed"></a>
<a href="https://discord.gg/HjJCwm5">
<img src="https://img.shields.io/discord/308323056592486420?logo=discord"
alt="chat on Discord"></a>
@@ -35,27 +38,21 @@ and legible badges in SVG and raster format, which can easily be included in
GitHub readmes or any other web page. The service supports dozens of
continuous integration services, package registries, distributions, app
stores, social networks, code coverage services, and code analysis services.
Every month it serves over 870 million images and is used by some of the
world's most popular open-source projects, [VS Code][vscode], [Vue.js][vue]
and [Bootstrap][bootstrap] to name a few.
[vscode]: https://github.com/Microsoft/vscode
[vue]: https://github.com/vuejs/vue
[bootstrap]: https://github.com/twbs/bootstrap
Every month it serves over 470 million images.
This repo hosts:
- The [Shields.io][shields.io] frontend and server code
- An [NPM library for generating badges][badge-maker]
- [documentation][badge-maker-docs]
- [changelog][badge-maker-changelog]
- An [NPM library for generating badges][gh-badges]
- [documentation][gh-badges-docs]
- [changelog][gh-badges-changelog]
- The [badge design specification][badge-spec]
[shields.io]: https://shields.io/
[badge-maker]: https://www.npmjs.com/package/badge-maker
[gh-badges]: https://www.npmjs.com/package/gh-badges
[badge-spec]: https://github.com/badges/shields/tree/master/spec
[badge-maker-docs]: https://github.com/badges/shields/tree/master/badge-maker/README.md
[badge-maker-changelog]: https://github.com/badges/shields/tree/master/badge-maker/CHANGELOG.md
[gh-badges-docs]: https://github.com/badges/shields/tree/master/gh-badges/README.md
[gh-badges-changelog]: https://github.com/badges/shields/tree/master/gh-badges/CHANGELOG.md
## Examples
@@ -73,13 +70,9 @@ This repo hosts:
[Make your own badges!][custom badges]
(Quick example: `https://img.shields.io/badge/left-right-f39f37`)
[custom badges]: https://shields.io/#your-badge
Browse a [complete list of badges][shields.io].
### Quickstart
Browse a [complete list of badges][shields.io] and locate a particular badge by using the search bar or by browsing the categories. Click on the badge to fill in required data elements for that badge type (like your username or repo) and optionally customize (label, colors etc.). And it's ready for use!
Use the button at the bottom to copy your badge url or snippet, which can then be added to places like your GitHub readme files or other web pages.
[custom badges]: http://shields.io/#your-badge
## Contributing
@@ -89,20 +82,20 @@ and pull requests! You can peruse the [contributing guidelines][contributing].
When adding or changing a service [please add tests][service-tests].
This project has quite a backlog of suggestions! If you're new to the project,
maybe you'd like to open a pull request to address one of them.
You can read a [tutorial on how to add a badge][tutorial].
maybe you'd like to open a pull request to address one of them:
[![GitHub issues by-label](https://img.shields.io/github/issues/badges/shields/good%20first%20issue)](https://github.com/badges/shields/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
You can read a [tutorial on how to add a badge][tutorial].
[service-tests]: https://github.com/badges/shields/blob/master/doc/service-tests.md
[tutorial]: https://github.com/badges/shields/blob/master/doc/TUTORIAL.md
[contributing]: https://github.com/badges/shields/blob/master/CONTRIBUTING.md
[tutorial]: doc/TUTORIAL.md
[contributing]: CONTRIBUTING.md
## Development
1. Install Node 16 or later. You can use the [package manager][] of your choice.
Tests need to pass in Node 16 and 17.
1. Install Node 8 or later. You can use the [package manager][] of your choice.
Tests need to pass in Node 8 and 10.
2. Clone this repository.
3. Run `npm ci` to install the dependencies.
4. Run `npm start` to start the badge server and the frontend dev server.
@@ -131,8 +124,8 @@ Please report any Gitpod bugs, questions, or suggestions in issue
[Snapshot tests][] ensure we don't inadvertently make changes that affect the
SVG or JSON output. When deliberately changing the output, run
`SNAPSHOT_DRY=1 npm run test:package` to preview changes to the saved
snapshots, and `SNAPSHOT_UPDATE=1 npm run test:package` to update them.
`SNAPSHOT_DRY=1 npm run test:js:server` to preview changes to the saved
snapshots, and `SNAPSHOT_UPDATE=1 npm run test:js:server` to update them.
The server can be configured to use [Sentry][] ([configuration][sentry configuration]) and [Prometheus][] ([configuration][prometheus configuration]).
@@ -142,9 +135,9 @@ Daily tests, including a full run of the service tests and overall code coverage
[gitpod]: https://www.gitpod.io/
[snapshot tests]: https://glebbahmutov.com/blog/snapshot-testing/
[prometheus]: https://prometheus.io/
[prometheus configuration]: https://github.com/badges/shields/blob/master/doc/self-hosting.md#prometheus
[prometheus configuration]: doc/self-hosting.md#prometheus
[sentry]: https://sentry.io/
[sentry configuration]: https://github.com/badges/shields/blob/master/doc/self-hosting.md#sentry
[sentry configuration]: doc/self-hosting.md#sentry
[daily-tests]: https://github.com/badges/daily-tests
[nodemon]: https://nodemon.io/
[nodemon debug]: https://github.com/Microsoft/vscode-recipes/tree/master/nodemon
@@ -154,22 +147,7 @@ Daily tests, including a full run of the service tests and overall code coverage
There is documentation about [hosting your own server][self-hosting].
[self-hosting]: https://github.com/badges/shields/blob/master/doc/self-hosting.md
## Related projects
[![Awesome](https://awesome.re/badge.svg)](https://awesome.re)
Status badges are used widely across open-source and private software projects.
Academics have studied the "signal" badges provide about software project
quality. There are many existing libraries for rendering these badges, and
alternatives to the hosted Shields badge service. [awesome-badges][] is a
curated collection of such resources.
[Contributions][contributing to awesome-badges] may be considered there.
(The presence of a project in that collection should not be interpreted as an endorsement nor promotion from the Shields project)
[awesome-badges]: https://github.com/badges/awesome-badges
[contributing to awesome-badges]: https://github.com/badges/awesome-badges/blob/main/CONTRIBUTING.md
[self-hosting]: doc/self-hosting.md
## History
@@ -194,8 +172,8 @@ You can read more about [the project's inception][thread],
[olivierlacan]: https://github.com/olivierlacan
[espadrine]: https://github.com/espadrine
[old-gh-badges]: https://github.com/badges/gh-badges
[motivation]: https://github.com/badges/shields/blob/master/spec/motivation.md
[spec]: https://github.com/badges/shields/blob/master/spec/SPECIFICATION.md
[motivation]: spec/motivation.md
[spec]: spec/SPECIFICATION.md
[thread]: https://github.com/h5bp/lazyweb-requests/issues/150
## Project leaders
@@ -205,6 +183,7 @@ Maintainers:
- [calebcartwright](https://github.com/calebcartwright) (core team)
- [chris48s](https://github.com/chris48s) (core team)
- [Daniel15](https://github.com/Daniel15) (core team)
- [espadrine](https://github.com/espadrine) (core team)
- [paulmelnikow](https://github.com/paulmelnikow) (core team)
- [platan](https://github.com/platan) (core team)
- [PyvesB](https://github.com/PyvesB) (core team)
@@ -212,16 +191,21 @@ Maintainers:
Operations:
- [calebcartwright](https://github.com/calebcartwright)
- [chris48s](https://github.com/chris48s)
- [paulmelnikow](https://github.com/paulmelnikow)
- [PyvesB](https://github.com/PyvesB)
- [espadrine](https://github.com/espadrine) (sysadmin)
- [paulmelnikow](https://github.com/paulmelnikow) (limited access)
Alumni:
- [espadrine](https://github.com/espadrine)
- [olivierlacan](https://github.com/olivierlacan)
## Related projects
- [badgerbadgerbadger gem][gem]
- [pybadges python library][pybadges]
[gem]: https://github.com/badges/badgerbadgerbadger
[pybadges]: https://github.com/google/pybadges
## License
All assets and code are under the [CC0 LICENSE](LICENSE) and in the public
@@ -230,6 +214,28 @@ domain unless specified otherwise.
The assets in `logo/` are trademarks of their respective companies and are
under their terms and license.
## Community
## Contributors
Thanks to the people and companies who donate money, services or time to keep the project running. [https://shields.io/community](https://shields.io/community)
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
<a href="https://github.com/badges/shields/graphs/contributors"><img src="https://opencollective.com/shields/contributors.svg?width=890" /></a>
## Backers
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/shields#backer)]
<a href="https://opencollective.com/shields#backers" target="_blank"><img src="https://opencollective.com/shields/backers.svg?width=890"></a>
## Sponsors
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/shields#sponsor)]
<a href="https://opencollective.com/shields/sponsor/0/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/shields/sponsor/1/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/shields/sponsor/2/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/shields/sponsor/3/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/shields/sponsor/4/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/shields/sponsor/5/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/shields/sponsor/6/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/shields/sponsor/7/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/shields/sponsor/8/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/shields/sponsor/9/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/9/avatar.svg"></a>

View File

@@ -1,25 +0,0 @@
# Security Policy
## Supported Projects
Please follow this guidance when reporting security issues affecting:
- [Shields.io](https://shields.io)
- [Raster.shields.io](https://raster.shields.io)
- Self-hosted Shields instances
- The [squint](https://github.com/badges/squint) raster proxy
- The [badge-maker](https://www.npmjs.com/package/badge-maker) NPM package
The [gh-badges](https://www.npmjs.com/package/gh-badges) and [svg-to-image-proxy](https://www.npmjs.com/package/svg-to-image-proxy) NPM packages are now deprecated and will no longer receive fixes for bugs or security issues.
## Reporting a Vulnerability
If you find a security vulnerability affecting any of our supported projects, please email [security@shields.io](mailto:security@shields.io), rather than opening a public issue on GitHub. After receiving the initial report, we will endeavor to keep you informed of the progress towards a fix and full announcement. We may ask you for additional information. You are also welcome to propose a patch or solution.
Report security bugs in third-party modules to the person or team maintaining the module.
## Coordinated Disclosure
We aim to patch confirmed vulnerabilities within 90 days or less, disclosing the details of those vulnerabilities when a patch is published. We ask that you refrain from sharing your report with others while we work on our patch.
We may want to coordinate an advisory with you to be published simultaneously with the patch, but you are also welcome to self-disclose after 90 days if you prefer. We will never publish information about you or our communications with you without your permission.

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
"keywords": ["badge", "github", "svg", "status"],
"website": "https://shields.io/",
"repository": "https://github.com/badges/shields",
"logo": "https://shields.io/favicon.png",
"logo": "http://shields.io/favicon.png",
"env": {
"CYPRESS_INSTALL_BINARY": {
"description": "Disable the cypress binary installation",
@@ -31,10 +31,6 @@
"TWITCH_CLIENT_SECRET": {
"description": "Configure the client secret to be used for the Twitch service.",
"required": false
},
"WEBLATE_API_KEY": {
"description": "Configure the API key to be used for the Weblate service.",
"required": false
}
},
"formation": {

View File

@@ -1,11 +0,0 @@
interface Format {
label?: string
message: string
labelColor?: string
color?: string
style?: 'plastic' | 'flat' | 'flat-square' | 'for-the-badge' | 'social'
}
export declare class ValidationError extends Error {}
export declare function makeBadge(format: Format): string

View File

@@ -1,35 +0,0 @@
import { expectType, expectError, expectAssignable } from 'tsd'
import { makeBadge, ValidationError } from '.'
expectError(makeBadge('string is invalid'))
expectError(makeBadge({}))
expectError(
makeBadge({
message: 'passed',
style: 'invalid style',
})
)
expectType<string>(
makeBadge({
message: 'passed',
})
)
expectType<string>(
makeBadge({
label: 'build',
message: 'passed',
})
)
expectType<string>(
makeBadge({
label: 'build',
message: 'passed',
labelColor: 'green',
color: 'red',
style: 'flat',
})
)
const error = new ValidationError()
expectAssignable<Error>(error)

View File

@@ -1,991 +0,0 @@
'use strict'
const anafanafo = require('anafanafo')
const { brightness } = require('./color')
const { XmlElement, ElementList } = require('./xml')
// https://github.com/badges/shields/pull/1132
const FONT_SCALE_UP_FACTOR = 10
const FONT_SCALE_DOWN_VALUE = 'scale(.1)'
const FONT_FAMILY = 'Verdana,Geneva,DejaVu Sans,sans-serif'
const WIDTH_FONT = '11px Verdana'
const SOCIAL_FONT_FAMILY = 'Helvetica Neue,Helvetica,Arial,sans-serif'
function capitalize(s) {
return `${s.charAt(0).toUpperCase()}${s.slice(1)}`
}
function colorsForBackground(color) {
const brightnessThreshold = 0.69
if (brightness(color) <= brightnessThreshold) {
return { textColor: '#fff', shadowColor: '#010101' }
} else {
return { textColor: '#333', shadowColor: '#ccc' }
}
}
function roundUpToOdd(val) {
return val % 2 === 0 ? val + 1 : val
}
function preferredWidthOf(str, options) {
// Increase chances of pixel grid alignment.
return roundUpToOdd(anafanafo(str, options) | 0)
}
function createAccessibleText({ label, message }) {
const labelPrefix = label ? `${label}: ` : ''
return labelPrefix + message
}
function hasLinks({ links }) {
const [leftLink, rightLink] = links || []
const hasLeftLink = leftLink && leftLink.length
const hasRightLink = rightLink && rightLink.length
const hasLink = hasLeftLink && hasRightLink
return { hasLink, hasLeftLink, hasRightLink }
}
function shouldWrapBodyWithLink({ links }) {
const { hasLeftLink, hasRightLink } = hasLinks({ links })
return hasLeftLink && !hasRightLink
}
function getLogoElement({ logo, horizPadding, badgeHeight, logoWidth }) {
const logoHeight = 14
if (!logo) return ''
return new XmlElement({
name: 'image',
attrs: {
x: horizPadding,
y: 0.5 * (badgeHeight - logoHeight),
width: logoWidth,
height: logoHeight,
'xlink:href': logo,
},
})
}
function renderBadge(
{ links, leftWidth, rightWidth, height, accessibleText },
content
) {
const width = leftWidth + rightWidth
const leftLink = links[0]
const { hasLink } = hasLinks({ links })
const title = hasLink
? ''
: new XmlElement({ name: 'title', content: [accessibleText] })
const body = shouldWrapBodyWithLink({ links })
? new XmlElement({
name: 'a',
content,
attrs: { target: '_blank', 'xlink:href': leftLink },
})
: new ElementList({ content })
const svgAttrs = {
xmlns: 'http://www.w3.org/2000/svg',
'xmlns:xlink': 'http://www.w3.org/1999/xlink',
width,
height,
}
if (!hasLink) {
svgAttrs.role = 'img'
svgAttrs['aria-label'] = accessibleText
}
const svg = new XmlElement({
name: 'svg',
content: [title, body],
attrs: svgAttrs,
})
return svg.render()
}
class Badge {
static get height() {
throw new Error('Not implemented')
}
static get verticalMargin() {
throw new Error('Not implemented')
}
static get shadow() {
throw new Error('Not implemented')
}
constructor({
label,
message,
links,
logo,
logoWidth,
logoPadding,
color = '#4c1',
labelColor,
}) {
const horizPadding = 5
const hasLogo = !!logo
const totalLogoWidth = logoWidth + logoPadding
const accessibleText = createAccessibleText({ label, message })
const hasLabel = label.length || labelColor
if (labelColor == null) {
labelColor = '#555'
}
labelColor = hasLabel || hasLogo ? labelColor : color
const labelMargin = totalLogoWidth + 1
const labelWidth = label.length
? preferredWidthOf(label, { font: WIDTH_FONT })
: 0
const leftWidth = hasLabel
? labelWidth + 2 * horizPadding + totalLogoWidth
: 0
const messageWidth = preferredWidthOf(message, { font: WIDTH_FONT })
let messageMargin = leftWidth - (message.length ? 1 : 0)
if (!hasLabel) {
if (hasLogo) {
messageMargin = messageMargin + totalLogoWidth + horizPadding
} else {
messageMargin = messageMargin + 1
}
}
let rightWidth = messageWidth + 2 * horizPadding
if (hasLogo && !hasLabel) {
rightWidth += totalLogoWidth + horizPadding - 1
}
const width = leftWidth + rightWidth
this.horizPadding = horizPadding
this.labelMargin = labelMargin
this.messageMargin = messageMargin
this.links = links
this.labelWidth = labelWidth
this.messageWidth = messageWidth
this.leftWidth = leftWidth
this.rightWidth = rightWidth
this.width = width
this.labelColor = labelColor
this.color = color
this.label = label
this.message = message
this.accessibleText = accessibleText
this.logoElement = getLogoElement({
logo,
horizPadding,
badgeHeight: this.constructor.height,
logoWidth,
})
this.foregroundGroupElement = this.getForegroundGroupElement()
}
static render(params) {
return new this(params).render()
}
getTextElement({ leftMargin, content, link, color, textWidth, linkWidth }) {
if (!content.length) return ''
const { textColor, shadowColor } = colorsForBackground(color)
const x =
FONT_SCALE_UP_FACTOR * (leftMargin + 0.5 * textWidth + this.horizPadding)
const text = new XmlElement({
name: 'text',
content: [content],
attrs: {
x,
y: 140 + this.constructor.verticalMargin,
transform: FONT_SCALE_DOWN_VALUE,
fill: textColor,
textLength: FONT_SCALE_UP_FACTOR * textWidth,
},
})
const shadowText = new XmlElement({
name: 'text',
content: [content],
attrs: {
'aria-hidden': 'true',
x,
y: 150 + this.constructor.verticalMargin,
fill: shadowColor,
'fill-opacity': '.3',
transform: FONT_SCALE_DOWN_VALUE,
textLength: FONT_SCALE_UP_FACTOR * textWidth,
},
})
const shadow = this.constructor.shadow ? shadowText : ''
if (!link) {
return new ElementList({ content: [shadow, text] })
}
const rect = new XmlElement({
name: 'rect',
attrs: {
width: linkWidth,
x: leftMargin > 1 ? leftMargin + 1 : 0,
height: this.constructor.height,
fill: 'rgba(0,0,0,0)',
},
})
return new XmlElement({
name: 'a',
content: [rect, shadow, text],
attrs: { target: '_blank', 'xlink:href': link },
})
}
getLabelElement() {
const leftLink = this.links[0]
return this.getTextElement({
leftMargin: this.labelMargin,
content: this.label,
link: !shouldWrapBodyWithLink({ links: this.links })
? leftLink
: undefined,
color: this.labelColor,
textWidth: this.labelWidth,
linkWidth: this.leftWidth,
})
}
getMessageElement() {
const rightLink = this.links[1]
return this.getTextElement({
leftMargin: this.messageMargin,
content: this.message,
link: rightLink,
color: this.color,
textWidth: this.messageWidth,
linkWidth: this.rightWidth,
})
}
getClipPathElement(rx) {
return new XmlElement({
name: 'clipPath',
content: [
new XmlElement({
name: 'rect',
attrs: {
width: this.width,
height: this.constructor.height,
rx,
fill: '#fff',
},
}),
],
attrs: { id: 'r' },
})
}
getBackgroundGroupElement({ withGradient, attrs }) {
const leftRect = new XmlElement({
name: 'rect',
attrs: {
width: this.leftWidth,
height: this.constructor.height,
fill: this.labelColor,
},
})
const rightRect = new XmlElement({
name: 'rect',
attrs: {
x: this.leftWidth,
width: this.rightWidth,
height: this.constructor.height,
fill: this.color,
},
})
const gradient = new XmlElement({
name: 'rect',
attrs: {
width: this.width,
height: this.constructor.height,
fill: 'url(#s)',
},
})
const content = withGradient
? [leftRect, rightRect, gradient]
: [leftRect, rightRect]
return new XmlElement({ name: 'g', content, attrs })
}
getForegroundGroupElement() {
return new XmlElement({
name: 'g',
content: [
this.logoElement,
this.getLabelElement(),
this.getMessageElement(),
],
attrs: {
fill: '#fff',
'text-anchor': 'middle',
'font-family': FONT_FAMILY,
'text-rendering': 'geometricPrecision',
'font-size': 110,
},
})
}
render() {
throw new Error('Not implemented')
}
}
class Plastic extends Badge {
static get height() {
return 18
}
static get verticalMargin() {
return -10
}
static get shadow() {
return true
}
render() {
const gradient = new XmlElement({
name: 'linearGradient',
content: [
new XmlElement({
name: 'stop',
attrs: { offset: 0, 'stop-color': '#fff', 'stop-opacity': '.7' },
}),
new XmlElement({
name: 'stop',
attrs: { offset: '.1', 'stop-color': '#aaa', 'stop-opacity': '.1' },
}),
new XmlElement({
name: 'stop',
attrs: { offset: '.9', 'stop-color': '#000', 'stop-opacity': '.3' },
}),
new XmlElement({
name: 'stop',
attrs: { offset: 1, 'stop-color': '#000', 'stop-opacity': '.5' },
}),
],
attrs: { id: 's', x2: 0, y2: '100%' },
})
const clipPath = this.getClipPathElement(4)
const backgroundGroup = this.getBackgroundGroupElement({
withGradient: true,
attrs: { 'clip-path': 'url(#r)' },
})
return renderBadge(
{
links: this.links,
leftWidth: this.leftWidth,
rightWidth: this.rightWidth,
accessibleText: this.accessibleText,
height: this.constructor.height,
},
[gradient, clipPath, backgroundGroup, this.foregroundGroupElement]
)
}
}
class Flat extends Badge {
static get height() {
return 20
}
static get verticalMargin() {
return 0
}
static get shadow() {
return true
}
render() {
const gradient = new XmlElement({
name: 'linearGradient',
content: [
new XmlElement({
name: 'stop',
attrs: { offset: 0, 'stop-color': '#bbb', 'stop-opacity': '.1' },
}),
new XmlElement({
name: 'stop',
attrs: { offset: 1, 'stop-opacity': '.1' },
}),
],
attrs: { id: 's', x2: 0, y2: '100%' },
})
const clipPath = this.getClipPathElement(3)
const backgroundGroup = this.getBackgroundGroupElement({
withGradient: true,
attrs: { 'clip-path': 'url(#r)' },
})
return renderBadge(
{
links: this.links,
leftWidth: this.leftWidth,
rightWidth: this.rightWidth,
accessibleText: this.accessibleText,
height: this.constructor.height,
},
[gradient, clipPath, backgroundGroup, this.foregroundGroupElement]
)
}
}
class FlatSquare extends Badge {
static get height() {
return 20
}
static get verticalMargin() {
return 0
}
static get shadow() {
return false
}
render() {
const backgroundGroup = this.getBackgroundGroupElement({
withGradient: false,
attrs: { 'shape-rendering': 'crispEdges' },
})
return renderBadge(
{
links: this.links,
leftWidth: this.leftWidth,
rightWidth: this.rightWidth,
accessibleText: this.accessibleText,
height: this.constructor.height,
},
[backgroundGroup, this.foregroundGroupElement]
)
}
}
function social({
label,
message,
links = [],
logo,
logoWidth,
logoPadding,
color = '#4c1',
labelColor = '#555',
}) {
// Social label is styled with a leading capital. Convert to caps here so
// width can be measured using the correct characters.
label = capitalize(label)
const externalHeight = 20
const internalHeight = 19
const labelHorizPadding = 5
const messageHorizPadding = 4
const horizGutter = 6
const totalLogoWidth = logoWidth + logoPadding
const hasMessage = message.length
const font = 'bold 11px Helvetica'
const labelTextWidth = preferredWidthOf(label, { font })
const messageTextWidth = preferredWidthOf(message, { font })
const labelRectWidth = labelTextWidth + totalLogoWidth + 2 * labelHorizPadding
const messageRectWidth = messageTextWidth + 2 * messageHorizPadding
const [leftLink, rightLink] = links
const { hasLeftLink, hasRightLink, hasLink } = hasLinks({ links })
const accessibleText = createAccessibleText({ label, message })
function getMessageBubble() {
if (!hasMessage) return ''
const messageBubbleMainX = labelRectWidth + horizGutter + 0.5
const messageBubbleNotchX = labelRectWidth + horizGutter
const content = [
new XmlElement({
name: 'rect',
attrs: {
x: messageBubbleMainX,
y: 0.5,
width: messageRectWidth,
height: internalHeight,
rx: 2,
fill: '#fafafa',
},
}),
new XmlElement({
name: 'rect',
attrs: {
x: messageBubbleNotchX,
y: 7.5,
width: 0.5,
height: 5,
stroke: '#fafafa',
},
}),
new XmlElement({
name: 'path',
attrs: {
d: `M${messageBubbleMainX} 6.5 l-3 3v1 l3 3`,
stroke: 'd5d5d5',
fill: '#fafafa',
},
}),
]
return new ElementList({ content })
}
function getLabelText() {
const labelTextX =
FONT_SCALE_UP_FACTOR *
(totalLogoWidth + labelTextWidth / 2 + labelHorizPadding)
const labelTextLength = FONT_SCALE_UP_FACTOR * labelTextWidth
const shouldWrapWithLink = hasLeftLink && !shouldWrapBodyWithLink({ links })
const rect = new XmlElement({
name: 'rect',
attrs: {
id: 'llink',
stroke: '#d5d5d5',
fill: 'url(#a)',
x: '.5',
y: '.5',
width: labelRectWidth,
height: internalHeight,
rx: 2,
},
})
const shadow = new XmlElement({
name: 'text',
content: [label],
attrs: {
'aria-hidden': 'true',
x: labelTextX,
y: 150,
fill: '#fff',
transform: FONT_SCALE_DOWN_VALUE,
textLength: labelTextLength,
},
})
const text = new XmlElement({
name: 'text',
content: [label],
attrs: {
x: labelTextX,
y: 140,
transform: FONT_SCALE_DOWN_VALUE,
textLength: labelTextLength,
},
})
return shouldWrapWithLink
? new XmlElement({
name: 'a',
content: [shadow, text, rect],
attrs: { target: '_blank', 'xlink:href': leftLink },
})
: new ElementList({ content: [rect, shadow, text] })
}
function getMessageText() {
if (!hasMessage) return ''
const messageTextX =
FONT_SCALE_UP_FACTOR *
(labelRectWidth + horizGutter + messageRectWidth / 2)
const messageTextLength = FONT_SCALE_UP_FACTOR * messageTextWidth
const rect = new XmlElement({
name: 'rect',
attrs: {
width: messageRectWidth + 1,
x: labelRectWidth + horizGutter,
height: internalHeight + 1,
fill: 'rgba(0,0,0,0)',
},
})
const shadow = new XmlElement({
name: 'text',
content: [message],
attrs: {
'aria-hidden': 'true',
x: messageTextX,
y: 150,
fill: '#fff',
transform: FONT_SCALE_DOWN_VALUE,
textLength: messageTextLength,
},
})
const text = new XmlElement({
name: 'text',
content: [message],
attrs: {
id: 'rlink',
x: messageTextX,
y: 140,
transform: FONT_SCALE_DOWN_VALUE,
textLength: messageTextLength,
},
})
return hasRightLink
? new XmlElement({
name: 'a',
content: [rect, shadow, text],
attrs: { target: '_blank', 'xlink:href': rightLink },
})
: new ElementList({ content: [shadow, text] })
}
const style = new XmlElement({
name: 'style',
content: [
'a:hover #llink{fill:url(#b);stroke:#ccc}a:hover #rlink{fill:#4183c4}',
],
})
const gradients = new ElementList({
content: [
new XmlElement({
name: 'linearGradient',
content: [
new XmlElement({
name: 'stop',
attrs: {
offset: 0,
'stop-color': '#fcfcfc',
'stop-opacity': 0,
},
}),
new XmlElement({
name: 'stop',
attrs: { offset: 1, 'stop-opacity': '.1' },
}),
],
attrs: { id: 'a', x2: 0, y2: '100%' },
}),
new XmlElement({
name: 'linearGradient',
content: [
new XmlElement({
name: 'stop',
attrs: { offset: 0, 'stop-color': '#ccc', 'stop-opacity': '.1' },
}),
new XmlElement({
name: 'stop',
attrs: { offset: 1, 'stop-opacity': '.1' },
}),
],
attrs: { id: 'b', x2: 0, y2: '100%' },
}),
],
})
const labelRect = new XmlElement({
name: 'rect',
attrs: {
stroke: 'none',
fill: '#fcfcfc',
x: 0.5,
y: 0.5,
width: labelRectWidth,
height: internalHeight,
rx: 2,
},
})
const messageBubble = getMessageBubble()
const labelText = getLabelText()
const messageText = getMessageText()
const backgroundGroup = new XmlElement({
name: 'g',
content: [labelRect, messageBubble],
attrs: { stroke: '#d5d5d5' },
})
const foregroundGroup = new XmlElement({
name: 'g',
content: [labelText, messageText],
attrs: {
'aria-hidden': `${!hasLink}`,
fill: '#333',
'text-anchor': 'middle',
'font-family': SOCIAL_FONT_FAMILY,
'text-rendering': 'geometricPrecision',
'font-weight': 700,
'font-size': '110px',
'line-height': '14px',
},
})
const logoElement = getLogoElement({
logo,
horizPadding: labelHorizPadding,
badgeHeight: externalHeight,
logoWidth,
})
return renderBadge(
{
links,
leftWidth: labelRectWidth + 1,
rightWidth: hasMessage ? horizGutter + messageRectWidth : 0,
accessibleText,
height: externalHeight,
},
[style, gradients, backgroundGroup, logoElement, foregroundGroup]
)
}
function forTheBadge({
label,
message,
links,
logo,
logoWidth,
color = '#4c1',
labelColor,
}) {
const FONT_SIZE = 10
const BADGE_HEIGHT = 28
const TEXT_MARGIN = 12
const LOGO_MARGIN = 9
const LOGO_TEXT_GUTTER = 6
const LETTER_SPACING = 1.25
// Prepare content. For the Badge is styled in all caps. It's important to to
// convert to uppercase first so the widths can be measured using the correct
// symbols.
label = label.toUpperCase()
message = message.toUpperCase()
const [leftLink, rightLink] = links
const { hasLeftLink, hasRightLink } = hasLinks({ links })
const outLabelColor = labelColor || '#555'
// Compute text width.
// TODO: This really should count the symbols rather than just using `.length`.
// https://mathiasbynens.be/notes/javascript-unicode
// This is not using `preferredWidthOf()` as it tends to produce larger
// inconsistencies in the letter spacing. The badges look fine, however if you
// replace `textLength` with `letterSpacing` in the rendered SVG, you can see
// the discrepancy. Ideally, swapping out `textLength` for `letterSpacing`
// should not affect the appearance.
const labelTextWidth = label.length
? (anafanafo(label, { font: `${FONT_SIZE}px Verdana` }) | 0) +
LETTER_SPACING * label.length
: 0
const messageTextWidth = message.length
? (anafanafo(message, { font: `bold ${FONT_SIZE}px Verdana` }) | 0) +
LETTER_SPACING * message.length
: 0
// Compute horizontal layout.
// If a `labelColor` is set, the logo is always set against it, even when
// there is no label. When `needsLabelRect` is true, render a label rect and a
// message rect; when false, only a message rect.
const hasLabel = Boolean(label.length)
const needsLabelRect = hasLabel || (logo && labelColor)
let logoMinX, labelTextMinX
if (logo) {
logoMinX = LOGO_MARGIN
labelTextMinX = logoMinX + logoWidth + LOGO_TEXT_GUTTER
} else {
labelTextMinX = TEXT_MARGIN
}
let labelRectWidth, messageTextMinX, messageRectWidth
if (needsLabelRect) {
if (hasLabel) {
labelRectWidth = labelTextMinX + labelTextWidth + TEXT_MARGIN
} else {
labelRectWidth = 2 * LOGO_MARGIN + logoWidth
}
messageTextMinX = labelRectWidth + TEXT_MARGIN
messageRectWidth = 2 * TEXT_MARGIN + messageTextWidth
} else {
if (logo) {
messageTextMinX = TEXT_MARGIN + logoWidth + LOGO_TEXT_GUTTER
messageRectWidth =
2 * TEXT_MARGIN + logoWidth + LOGO_TEXT_GUTTER + messageTextWidth
} else {
messageTextMinX = TEXT_MARGIN
messageRectWidth = 2 * TEXT_MARGIN + messageTextWidth
}
}
const logoElement = getLogoElement({
logo,
horizPadding: logoMinX,
badgeHeight: BADGE_HEIGHT,
logoWidth,
})
function getLabelElement() {
const { textColor } = colorsForBackground(outLabelColor)
const midX = labelTextMinX + 0.5 * labelTextWidth
const text = new XmlElement({
name: 'text',
content: [label],
attrs: {
transform: FONT_SCALE_DOWN_VALUE,
x: FONT_SCALE_UP_FACTOR * midX,
y: 175,
textLength: FONT_SCALE_UP_FACTOR * labelTextWidth,
fill: textColor,
},
})
if (hasLeftLink && !shouldWrapBodyWithLink({ links })) {
const rect = new XmlElement({
name: 'rect',
attrs: {
width: labelRectWidth,
height: BADGE_HEIGHT,
fill: 'rgba(0,0,0,0)',
},
})
return new XmlElement({
name: 'a',
content: [rect, text],
attrs: {
target: '_blank',
'xlink:href': leftLink,
},
})
} else {
return text
}
}
function getMessageElement() {
const { textColor } = colorsForBackground(color)
const midX = messageTextMinX + 0.5 * messageTextWidth
const text = new XmlElement({
name: 'text',
content: [message],
attrs: {
transform: FONT_SCALE_DOWN_VALUE,
x: FONT_SCALE_UP_FACTOR * midX,
y: 175,
textLength: FONT_SCALE_UP_FACTOR * messageTextWidth,
fill: textColor,
'font-weight': 'bold',
},
})
if (hasRightLink) {
const rect = new XmlElement({
name: 'rect',
attrs: {
width: messageRectWidth,
height: BADGE_HEIGHT,
x: labelRectWidth || 0,
fill: 'rgba(0,0,0,0)',
},
})
return new XmlElement({
name: 'a',
content: [rect, text],
attrs: {
target: '_blank',
'xlink:href': rightLink,
},
})
} else {
return text
}
}
let backgroundContent
if (needsLabelRect) {
backgroundContent = [
new XmlElement({
name: 'rect',
attrs: {
width: labelRectWidth,
height: BADGE_HEIGHT,
fill: outLabelColor,
},
}),
new XmlElement({
name: 'rect',
attrs: {
x: labelRectWidth,
width: messageRectWidth,
height: BADGE_HEIGHT,
fill: color,
},
}),
]
} else {
backgroundContent = [
new XmlElement({
name: 'rect',
attrs: {
width: messageRectWidth,
height: BADGE_HEIGHT,
fill: color,
},
}),
]
}
const backgroundGroup = new XmlElement({
name: 'g',
content: backgroundContent,
attrs: {
'shape-rendering': 'crispEdges',
},
})
const foregroundGroup = new XmlElement({
name: 'g',
content: [
logoElement,
hasLabel ? getLabelElement() : '',
getMessageElement(),
],
attrs: {
fill: '#fff',
'text-anchor': 'middle',
'font-family': FONT_FAMILY,
'text-rendering': 'geometricPrecision',
'font-size': FONT_SCALE_UP_FACTOR * FONT_SIZE,
},
})
// Render.
return renderBadge(
{
links,
leftWidth: labelRectWidth || 0,
rightWidth: messageRectWidth,
accessibleText: createAccessibleText({ label, message }),
height: BADGE_HEIGHT,
},
[backgroundGroup, foregroundGroup]
)
}
module.exports = {
plastic: params => Plastic.render(params),
flat: params => Flat.render(params),
'flat-square': params => FlatSquare.render(params),
social,
'for-the-badge': forTheBadge,
}

View File

@@ -1,81 +0,0 @@
'use strict'
/**
* @module badge-maker
*/
const _makeBadge = require('./make-badge')
class ValidationError extends Error {}
function _validate(format) {
if (format !== Object(format)) {
throw new ValidationError('makeBadge takes an argument of type object')
}
if (!('message' in format)) {
throw new ValidationError('Field `message` is required')
}
const stringFields = ['labelColor', 'color', 'message', 'label']
stringFields.forEach(function (field) {
if (field in format && typeof format[field] !== 'string') {
throw new ValidationError(`Field \`${field}\` must be of type string`)
}
})
const styleValues = [
'plastic',
'flat',
'flat-square',
'for-the-badge',
'social',
]
if ('style' in format && !styleValues.includes(format.style)) {
throw new ValidationError(
`Field \`style\` must be one of (${styleValues.toString()})`
)
}
}
function _clean(format) {
const expectedKeys = ['label', 'message', 'labelColor', 'color', 'style']
const cleaned = {}
Object.keys(format).forEach(key => {
if (format[key] != null && expectedKeys.includes(key)) {
cleaned[key] = format[key]
} else {
throw new ValidationError(
`Unexpected field '${key}'. Allowed values are (${expectedKeys.toString()})`
)
}
})
// Legacy.
cleaned.label = cleaned.label || ''
return cleaned
}
/**
* Create a badge
*
* @param {object} format Object specifying badge data
* @param {string} format.label (Optional) Badge label (e.g: 'build')
* @param {string} format.message (Required) Badge message (e.g: 'passing')
* @param {string} format.labelColor (Optional) Label color
* @param {string} format.color (Optional) Message color
* @param {string} format.style (Optional) Visual style e.g: 'flat'
* @returns {string} Badge in SVG format
* @see https://github.com/badges/shields/tree/master/badge-maker/README.md
*/
function makeBadge(format) {
_validate(format)
const cleanedFormat = _clean(format)
return _makeBadge(cleanedFormat)
}
module.exports = {
makeBadge,
ValidationError,
}

View File

@@ -1,75 +0,0 @@
'use strict'
const { expect } = require('chai')
const isSvg = require('is-svg')
const { makeBadge, ValidationError } = require('.')
describe('makeBadge function', function () {
it('should produce badge with valid input', function () {
expect(
makeBadge({
label: 'build',
message: 'passed',
})
).to.satisfy(isSvg)
expect(
makeBadge({
message: 'passed',
})
).to.satisfy(isSvg)
expect(
makeBadge({
label: 'build',
message: 'passed',
color: 'green',
style: 'flat',
})
).to.satisfy(isSvg)
})
it('should throw a ValidationError with invalid inputs', function () {
;[null, undefined, 7, 'foo', 4.25].forEach(x => {
console.log(x)
expect(() => makeBadge(x)).to.throw(
ValidationError,
'makeBadge takes an argument of type object'
)
})
expect(() => makeBadge({})).to.throw(
ValidationError,
'Field `message` is required'
)
expect(() => makeBadge({ label: 'build' })).to.throw(
ValidationError,
'Field `message` is required'
)
expect(() =>
makeBadge({ label: 'build', message: 'passed', labelColor: 7 })
).to.throw(ValidationError, 'Field `labelColor` must be of type string')
expect(() =>
makeBadge({ label: 'build', message: 'passed', format: 'png' })
).to.throw(ValidationError, "Unexpected field 'format'")
expect(() =>
makeBadge({ label: 'build', message: 'passed', template: 'flat' })
).to.throw(ValidationError, "Unexpected field 'template'")
expect(() =>
makeBadge({ label: 'build', message: 'passed', foo: 'bar' })
).to.throw(ValidationError, "Unexpected field 'foo'")
expect(() =>
makeBadge({
label: 'build',
message: 'passed',
style: 'something else',
})
).to.throw(
ValidationError,
'Field `style` must be one of (plastic,flat,flat-square,for-the-badge,social)'
)
expect(() =>
makeBadge({ label: 'build', message: 'passed', style: 'popout' })
).to.throw(
ValidationError,
'Field `style` must be one of (plastic,flat,flat-square,for-the-badge,social)'
)
})
})

View File

@@ -1,46 +0,0 @@
'use strict'
const { toSvgColor } = require('./color')
const badgeRenderers = require('./badge-renderers')
const { stripXmlWhitespace } = require('./xml')
/*
note: makeBadge() is fairly thinly wrapped so if we are making changes here
it is likely this will impact on the package's public interface in index.js
*/
module.exports = function makeBadge({
style = 'flat',
label,
message,
color,
labelColor,
logo,
logoPosition,
logoWidth,
links = ['', ''],
}) {
// String coercion and whitespace removal.
label = `${label}`.trim()
message = `${message}`.trim()
const render = badgeRenderers[style]
if (!render) {
throw new Error(`Unknown badge style: '${style}'`)
}
logoWidth = +logoWidth || (logo ? 14 : 0)
return stripXmlWhitespace(
render({
label,
message,
links,
logo,
logoPosition,
logoWidth,
logoPadding: logo && label.length ? 3 : 0,
color: toSvgColor(color),
labelColor: toSvgColor(labelColor),
})
)
}

View File

@@ -1,509 +0,0 @@
'use strict'
const { expect } = require('chai')
const snapshot = require('snap-shot-it')
const isSvg = require('is-svg')
const prettier = require('prettier')
const makeBadge = require('./make-badge')
function expectBadgeToMatchSnapshot(badgeData) {
snapshot(prettier.format(makeBadge(badgeData), { parser: 'html' }))
}
describe('The badge generator', function () {
describe('SVG', function () {
it('should produce SVG', function () {
expect(makeBadge({ label: 'cactus', message: 'grown' }))
.to.satisfy(isSvg)
.and.to.include('cactus')
.and.to.include('grown')
})
it('should match snapshot', function () {
expectBadgeToMatchSnapshot({ label: 'cactus', message: 'grown' })
})
it('should replace undefined svg badge style with "flat"', function () {
expect(
makeBadge({
label: 'name',
message: 'Bob',
})
)
.to.satisfy(isSvg)
.and.to.equal(
makeBadge({
label: 'name',
message: 'Bob',
style: 'flat',
})
)
})
it('should fail with unknown svg badge style', function () {
expect(() =>
makeBadge({ label: 'name', message: 'Bob', style: 'unknown_style' })
).to.throw(Error, "Unknown badge style: 'unknown_style'")
})
})
describe('"flat" template badge generation', function () {
it('should match snapshots: message/label, no logo', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
style: 'flat',
color: '#b3e',
labelColor: '#0f0',
})
})
it('should match snapshots: message/label, with logo', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
style: 'flat',
color: '#b3e',
labelColor: '#0f0',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
})
})
it('should match snapshots: message only, no logo', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
style: 'flat',
color: '#b3e',
})
})
it('should match snapshots: message only, with logo', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
style: 'flat',
color: '#b3e',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
})
})
it('should match snapshots: message only, with logo and labelColor', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
style: 'flat',
color: '#b3e',
labelColor: '#0f0',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
})
})
it('should match snapshots: message/label, with links', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
style: 'flat',
color: '#b3e',
labelColor: '#0f0',
links: ['https://shields.io/', 'https://www.google.co.uk/'],
})
})
it('should match snapshots: black text when the label color is light', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
style: 'flat',
color: '#000',
labelColor: '#f3f3f3',
})
})
it('should match snapshots: black text when the message color is light', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
style: 'flat',
color: '#e2ffe1',
labelColor: '#000',
})
})
})
describe('"flat-square" template badge generation', function () {
it('should match snapshots: message/label, no logo', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
style: 'flat-square',
color: '#b3e',
labelColor: '#0f0',
})
})
it('should match snapshots: message/label, with logo', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
style: 'flat-square',
color: '#b3e',
labelColor: '#0f0',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
})
})
it('should match snapshots: message only, no logo', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
style: 'flat-square',
color: '#b3e',
})
})
it('should match snapshots: message only, with logo', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
style: 'flat-square',
color: '#b3e',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
})
})
it('should match snapshots: message only, with logo and labelColor', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
style: 'flat-square',
color: '#b3e',
labelColor: '#0f0',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
})
})
it('should match snapshots: message/label, with links', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
style: 'flat-square',
color: '#b3e',
labelColor: '#0f0',
links: ['https://shields.io/', 'https://www.google.co.uk/'],
})
})
it('should match snapshots: black text when the label color is light', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
style: 'flat-square',
color: '#000',
labelColor: '#f3f3f3',
})
})
it('should match snapshots: black text when the message color is light', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
style: 'flat-square',
color: '#e2ffe1',
labelColor: '#000',
})
})
})
describe('"plastic" template badge generation', function () {
it('should match snapshots: message/label, no logo', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
style: 'plastic',
color: '#b3e',
labelColor: '#0f0',
})
})
it('should match snapshots: message/label, with logo', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
style: 'plastic',
color: '#b3e',
labelColor: '#0f0',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
})
})
it('should match snapshots: message only, no logo', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
style: 'plastic',
color: '#b3e',
})
})
it('should match snapshots: message only, with logo', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
style: 'plastic',
color: '#b3e',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
})
})
it('should match snapshots: message only, with logo and labelColor', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
style: 'plastic',
color: '#b3e',
labelColor: '#0f0',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
})
})
it('should match snapshots: message/label, with links', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
style: 'plastic',
color: '#b3e',
labelColor: '#0f0',
links: ['https://shields.io/', 'https://www.google.co.uk/'],
})
})
it('should match snapshots: black text when the label color is light', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
style: 'plastic',
color: '#000',
labelColor: '#f3f3f3',
})
})
it('should match snapshots: black text when the message color is light', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
style: 'plastic',
color: '#e2ffe1',
labelColor: '#000',
})
})
})
describe('"for-the-badge" template badge generation', function () {
// https://github.com/badges/shields/issues/1280
it('numbers should produce a string', function () {
expect(
makeBadge({
label: 1998,
message: 1999,
style: 'for-the-badge',
})
)
.to.include('1998')
.and.to.include('1999')
})
it('lowercase/mixedcase string should produce uppercase string', function () {
expect(
makeBadge({
label: 'Label',
message: '1 string',
style: 'for-the-badge',
})
)
.to.include('LABEL')
.and.to.include('1 STRING')
})
it('should match snapshots: message/label, no logo', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
style: 'for-the-badge',
color: '#b3e',
labelColor: '#0f0',
})
})
it('should match snapshots: message/label, with logo', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
style: 'for-the-badge',
color: '#b3e',
labelColor: '#0f0',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
})
})
it('should match snapshots: message only, no logo', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
style: 'for-the-badge',
color: '#b3e',
})
})
it('should match snapshots: message only, with logo', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
style: 'for-the-badge',
color: '#b3e',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
})
})
it('should match snapshots: message only, with logo and labelColor', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
style: 'for-the-badge',
color: '#b3e',
labelColor: '#0f0',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
})
})
it('should match snapshots: message/label, with links', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
style: 'for-the-badge',
color: '#b3e',
labelColor: '#0f0',
links: ['https://shields.io/', 'https://www.google.co.uk/'],
})
})
it('should match snapshots: black text when the label color is light', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
style: 'for-the-badge',
color: '#000',
labelColor: '#f3f3f3',
})
})
it('should match snapshots: black text when the message color is light', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
style: 'for-the-badge',
color: '#e2ffe1',
labelColor: '#000',
})
})
})
describe('"social" template badge generation', function () {
it('should produce capitalized string for badge key', function () {
expect(
makeBadge({
label: 'some-key',
message: 'some-value',
style: 'social',
})
)
.to.include('Some-key')
.and.to.include('some-value')
})
// https://github.com/badges/shields/issues/1606
it('should handle empty strings used as badge keys', function () {
expect(
makeBadge({
label: '',
message: 'some-value',
style: 'social',
})
)
.to.include('></text>')
.and.to.include('some-value')
})
it('should match snapshots: message/label, no logo', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
style: 'social',
color: '#b3e',
labelColor: '#0f0',
})
})
it('should match snapshots: message/label, with logo', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
style: 'social',
color: '#b3e',
labelColor: '#0f0',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
})
})
it('should match snapshots: message only, no logo', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
style: 'social',
color: '#b3e',
})
})
it('should match snapshots: message only, with logo', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
style: 'social',
color: '#b3e',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
})
})
it('should match snapshots: message only, with logo and labelColor', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
style: 'social',
color: '#b3e',
labelColor: '#0f0',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
})
})
it('should match snapshots: message/label, with links', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
style: 'social',
color: '#b3e',
labelColor: '#0f0',
links: ['https://shields.io/', 'https://www.google.co.uk/'],
})
})
})
describe('badges with logos should always produce the same badge', function () {
it('badge with logo', function () {
expectBadgeToMatchSnapshot({
label: 'label',
message: 'message',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
})
})
})
})

View File

@@ -1,96 +0,0 @@
/**
* @module
*/
'use strict'
function stripXmlWhitespace(xml) {
return xml.replace(/>\s+/g, '>').replace(/<\s+/g, '<').trim()
}
function escapeXml(s) {
if (typeof s === 'number') {
return s
} else if (s === undefined || typeof s !== 'string') {
return undefined
} else {
return s
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
}
}
/**
* Representation of an XML element
*/
class XmlElement {
/**
* Xml Element Constructor
*
* @param {object} attrs Refer to individual attrs
* @param {string} attrs.name
* Name of the XML tag
* @param {Array.<string|module:badge-maker/lib/xml~XmlElement>} [attrs.content=[]]
* Array of objects to render inside the tag. content may contain a mix of
* string and XmlElement objects. If content is `[]` or ommitted the
* element will be rendered as a self-closing element.
* @param {object} [attrs.attrs={}]
* Object representing the tag's attributes as name/value pairs
*/
constructor({ name, content = [], attrs = {} }) {
this.name = name
this.content = content
this.attrs = attrs
}
/**
* Render the XML element to a string, applying appropriate escaping
*
* @returns {string} String representation of the XML element
*/
render() {
const attrsStr = Object.entries(this.attrs)
.map(([k, v]) => ` ${k}="${escapeXml(v)}"`)
.join('')
if (this.content.length > 0) {
const content = this.content
.map(function (el) {
if (typeof el.render === 'function') {
return el.render()
} else {
return escapeXml(el)
}
})
.join(' ')
return stripXmlWhitespace(
`<${this.name}${attrsStr}>${content}</${this.name}>`
)
}
return stripXmlWhitespace(`<${this.name}${attrsStr}/>`)
}
}
/**
* Convenience class. Sometimes it is useful to return an object that behaves
* like an XmlElement but renders multiple XML tags (not wrapped in a <g>).
*/
class ElementList {
constructor({ content = [] }) {
this.content = content
}
render() {
return this.content.reduce(
(acc, el) =>
typeof el.render === 'function'
? acc + el.render()
: acc + escapeXml(el),
''
)
}
}
module.exports = { escapeXml, stripXmlWhitespace, XmlElement, ElementList }

View File

@@ -1,50 +0,0 @@
'use strict'
const { test, given } = require('sazerac')
const { XmlElement } = require('./xml')
function testRender(params) {
return new XmlElement(params).render()
}
describe('XmlElement class', function () {
test(testRender, () => {
given({ name: 'tag' }).expect('<tag/>')
given({ name: 'tag', content: ['text'] }).expect('<tag>text</tag>')
given({
name: 'tag',
content: ['not xml>>>', 'text', new XmlElement({ name: 'xml' })],
}).expect('<tag>not xml&gt;&gt;&gt; text <xml/></tag>')
given({
name: 'nested1',
content: [
new XmlElement({
name: 'nested2',
content: [new XmlElement({ name: 'nested3' })],
}),
],
}).expect('<nested1><nested2><nested3/></nested2></nested1>')
given({
name: 'tag',
attrs: {
int: 47,
text: 'text',
escape: '<escape me>',
},
}).expect('<tag int="47" text="text" escape="&lt;escape me&gt;"/>')
given({
name: 'tag',
content: ['text'],
attrs: {
int: 47,
text: 'text',
escape: '<escape me>',
},
}).expect('<tag int="47" text="text" escape="&lt;escape me&gt;">text</tag>')
})
})

View File

@@ -21,7 +21,7 @@ public:
key: 'HTTPS_KEY'
cert: 'HTTPS_CRT'
redirectUrl: 'REDIRECT_URI'
redirectUri: 'REDIRECT_URI'
rasterUrl: 'RASTER_URL'
@@ -30,6 +30,9 @@ public:
__name: 'ALLOWED_ORIGIN'
__format: 'json'
persistence:
dir: 'PERSISTENCE_DIR'
services:
bitbucketServer:
authorizedOrigins: 'BITBUCKET_SERVER_ORIGINS'
@@ -40,8 +43,6 @@ public:
debug:
enabled: 'GITHUB_DEBUG_ENABLED'
intervalSeconds: 'GITHUB_DEBUG_INTERVAL_SECONDS'
gitlab:
authorizedOrigins: 'GITLAB_ORIGINS'
jenkins:
authorizedOrigins: 'JENKINS_ORIGINS'
jira:
@@ -50,51 +51,43 @@ public:
authorizedOrigins: 'NEXUS_ORIGINS'
npm:
authorizedOrigins: 'NPM_ORIGINS'
obs:
authorizedOrigins: 'OBS_ORIGINS'
sonar:
authorizedOrigins: 'SONAR_ORIGINS'
teamcity:
authorizedOrigins: 'TEAMCITY_ORIGINS'
weblate:
authorizedOrigins: 'WEBLATE_ORIGINS'
trace: 'TRACE_SERVICES'
cacheHeaders:
defaultCacheLengthSeconds: 'BADGE_MAX_AGE_SECONDS'
fetchLimit: 'FETCH_LIMIT'
userAgentBase: 'USER_AGENT_BASE'
rateLimit: 'RATE_LIMIT'
requestTimeoutSeconds: 'REQUEST_TIMEOUT_SECONDS'
requestTimeoutMaxAgeSeconds: 'REQUEST_TIMEOUT_MAX_AGE_SECONDS'
requireCloudflare: 'REQUIRE_CLOUDFLARE'
integrations:
default:
fetchLimit: 'FETCH_LIMIT'
private:
azure_devops_token: 'AZURE_DEVOPS_TOKEN'
bintray_user: 'BINTRAY_USER'
bintray_apikey: 'BINTRAY_API_KEY'
bitbucket_username: 'BITBUCKET_USER'
bitbucket_password: 'BITBUCKET_PASS'
bitbucket_server_username: 'BITBUCKET_SERVER_USER'
bitbucket_server_password: 'BITBUCKET_SERVER_PASS'
discord_bot_token: 'DISCORD_BOT_TOKEN'
drone_token: 'DRONE_TOKEN'
gh_client_id: 'GH_CLIENT_ID'
gh_client_secret: 'GH_CLIENT_SECRET'
gh_token: 'GH_TOKEN'
gitlab_token: 'GITLAB_TOKEN'
jenkins_user: 'JENKINS_USER'
jenkins_pass: 'JENKINS_PASS'
jira_user: 'JIRA_USER'
jira_pass: 'JIRA_PASS'
librariesio_tokens: 'LIBRARIESIO_TOKENS'
nexus_user: 'NEXUS_USER'
nexus_pass: 'NEXUS_PASS'
npm_token: 'NPM_TOKEN'
obs_user: 'OBS_USER'
obs_pass: 'OBS_PASS'
redis_url: 'REDIS_URL'
sentry_dsn: 'SENTRY_DSN'
shields_secret: 'SHIELDS_SECRET'
sl_insight_userUuid: 'SL_INSIGHT_USER_UUID'
sl_insight_apiToken: 'SL_INSIGHT_API_TOKEN'
sonarqube_token: 'SONARQUBE_TOKEN'
@@ -105,5 +98,3 @@ private:
wheelmap_token: 'WHEELMAP_TOKEN'
influx_username: 'INFLUX_USERNAME'
influx_password: 'INFLUX_PASSWORD'
weblate_api_key: 'WEBLATE_API_KEY'
youtube_api_key: 'YOUTUBE_API_KEY'

View File

@@ -16,29 +16,35 @@ public:
cors:
allowedOrigin: []
persistence:
dir: './private'
services:
github:
baseUri: 'https://api.github.com/'
debug:
enabled: false
intervalSeconds: 200
obs:
authorizedOrigins: 'https://api.opensuse.org'
weblate:
authorizedOrigins: 'https://hosted.weblate.org'
trace: false
cacheHeaders:
defaultCacheLengthSeconds: 120
rateLimit: true
handleInternalErrors: true
fetchLimit: '10MB'
userAgentBase: 'shields (self-hosted)'
integrations:
default:
fetchLimit: '10MB'
requestTimeoutSeconds: 120
requestTimeoutMaxAgeSeconds: 30
DynamicJson:
fetchLimit: '2MB'
requireCloudflare: false
DynamicXml:
fetchLimit: '128KB'
DynamicYaml:
fetchLimit: '64KB'
private: {}

View File

@@ -5,4 +5,6 @@ public:
cors:
allowedOrigin: ['http://localhost:3000']
rateLimit: false
handleInternalErrors: false

View File

@@ -1,9 +1,7 @@
private:
# These are the keys which are set on the production servers.
discord_bot_token: ...
gh_client_id: ...
gh_client_secret: ...
gitlab_token: ...
redis_url: ...
sentry_dsn: ...
shields_secret: ...
@@ -11,6 +9,4 @@ private:
sl_insight_apiToken: ...
twitch_client_id: ...
twitch_client_secret: ...
weblate_api_key: ...
wheelmap_token: ...
youtube_api_key: ...

View File

@@ -5,11 +5,6 @@ private:
# you can also set these values through environment variables, which may be
# preferable for self hosting.
gh_token: '...'
gitlab_token: '...'
obs_user: '...'
obs_pass: '...'
twitch_client_id: '...'
twitch_client_secret: '...'
weblate_api_key: '...'
wheelmap_token: '...'
youtube_api_key: '...'

View File

@@ -2,12 +2,7 @@ public:
metrics:
prometheus:
enabled: true
influx:
enabled: true
url: https://metrics.shields.io/telegraf
instanceIdFrom: env-var
instanceIdEnvVarName: HEROKU_DYNO_ID
envLabel: shields-production
endpointEnabled: true
ssl:
isSecure: true
@@ -15,4 +10,10 @@ public:
cors:
allowedOrigin: ['http://shields.io', 'https://shields.io']
redirectUrl: 'https://shields.io/'
rasterUrl: 'https://raster.shields.io'
private:
# These are not really private; they should be moved to `public`.
shields_ips: ['192.99.59.72', '51.254.114.150', '149.56.96.133']

View File

@@ -3,6 +3,8 @@ public:
address: 'localhost'
port: 1111
rateLimit: false
redirectUrl: 'http://frontend.example.test'
rasterUrl: 'http://raster.example.test'

View File

@@ -38,22 +38,18 @@ 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({

View File

@@ -1,7 +1,8 @@
// Avoid "Attempted import error: 'URL' is not exported from 'url'" in frontend.
import url from 'url'
import queryString from 'query-string'
import { compile } from 'path-to-regexp'
'use strict'
const { URL } = require('url')
const queryString = require('query-string')
const { compile } = require('path-to-regexp')
function badgeUrlFromPath({
baseUrl = '',
@@ -58,19 +59,15 @@ 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}` : ''
@@ -146,13 +143,13 @@ function dynamicBadgeUrl({
function rasterRedirectUrl({ rasterUrl }, badgeUrl) {
// Ensure we're always using the `rasterUrl` by using just the path from
// the request URL.
const { pathname, search } = new url.URL(badgeUrl, 'https://bogus.test')
const result = new url.URL(pathname, rasterUrl)
const { pathname, search } = new URL(badgeUrl, 'https://bogus.test')
const result = new URL(pathname, rasterUrl)
result.search = search
return result
}
export {
module.exports = {
badgeUrlFromPath,
badgeUrlFromPattern,
encodeField,

View File

@@ -1,14 +1,16 @@
import { test, given } from 'sazerac'
import {
'use strict'
const { test, given } = require('sazerac')
const {
badgeUrlFromPath,
badgeUrlFromPattern,
encodeField,
staticBadgeUrl,
queryStringStaticBadgeUrl,
dynamicBadgeUrl,
} from './make-badge-url.js'
} = require('./make-badge-url')
describe('Badge URL generation functions', function () {
describe('Badge URL generation functions', function() {
test(badgeUrlFromPath, () => {
given({
baseUrl: 'http://example.com',

View File

@@ -1,14 +1,21 @@
'use strict'
// Escapes `t` using the format specified in
// <https://github.com/espadrine/gh-badges/issues/12#issuecomment-31518129>
function escapeFormat(t) {
return (
t
// Single underscore.
.replace(/(^|[^_])((?:__)*)_(?!_)/g, '$1$2 ')
// Inline single underscore.
.replace(/([^_])_([^_])/g, '$1 $2')
// Leading or trailing underscore.
.replace(/([^_])_$/, '$1 ')
.replace(/^_([^_])/, ' $1')
// Double underscore and double dash.
.replace(/__/g, '_')
.replace(/--/g, '-')
)
}
export { escapeFormat }
module.exports = {
escapeFormat,
}

View File

@@ -1,19 +0,0 @@
import { test, given } from 'sazerac'
import { escapeFormat } from './path-helpers.js'
describe('Badge URL helper functions', function () {
test(escapeFormat, () => {
given('_single leading underscore').expect(' single leading underscore')
given('single trailing underscore_').expect('single trailing underscore ')
given('__double leading underscores').expect('_double leading underscores')
given('double trailing underscores__').expect(
'double trailing underscores_'
)
given('treble___underscores').expect('treble_ underscores')
given('fourfold____underscores').expect('fourfold__underscores')
given('double--dashes').expect('double-dashes')
given('treble---dashes').expect('treble--dashes')
given('fourfold----dashes').expect('fourfold--dashes')
given('once_in_a_blue--moon').expect('once in a blue-moon')
})
})

View File

@@ -1,5 +1,7 @@
import { URL } from 'url'
import { InvalidParameter } from './errors.js'
'use strict'
const { URL } = require('url')
const { InvalidParameter } = require('./errors')
class AuthHelper {
constructor(
@@ -74,7 +76,7 @@ class AuthHelper {
}
static _isInsecureSslRequest({ options = {} }) {
const strictSSL = options?.https?.rejectUnauthorized ?? true
const { strictSSL = true } = options
return strictSSL !== true
}
@@ -107,10 +109,8 @@ class AuthHelper {
}
get _basicAuth() {
const { _user: username, _pass: password } = this
return this.isConfigured
? { username: username || '', password: password || '' }
: undefined
const { _user: user, _pass: pass } = this
return this.isConfigured ? { user, pass } : undefined
}
/*
@@ -133,7 +133,7 @@ class AuthHelper {
const { options, ...rest } = requestParams
return {
options: {
...auth,
auth,
...options,
},
...rest,
@@ -146,11 +146,9 @@ class AuthHelper {
)
}
_bearerAuthHeader(bearerKey) {
get _bearerAuthHeader() {
const { _pass: pass } = this
return this.isConfigured
? { Authorization: `${bearerKey} ${pass}` }
: undefined
return this.isConfigured ? { Authorization: `Bearer ${pass}` } : undefined
}
static _mergeHeaders(requestParams, headers) {
@@ -170,26 +168,20 @@ class AuthHelper {
}
}
withBearerAuthHeader(
requestParams,
bearerKey = 'Bearer' // lgtm [js/hardcoded-credentials]
) {
withBearerAuthHeader(requestParams) {
return this._withAnyAuth(requestParams, requestParams =>
this.constructor._mergeHeaders(
requestParams,
this._bearerAuthHeader(bearerKey)
)
this.constructor._mergeHeaders(requestParams, this._bearerAuthHeader)
)
}
static _mergeQueryParams(requestParams, query) {
const {
options: { searchParams: existingQuery, ...restOptions } = {},
options: { qs: existingQuery, ...restOptions } = {},
...rest
} = requestParams
return {
options: {
searchParams: {
qs: {
...existingQuery,
...query,
},
@@ -209,4 +201,4 @@ class AuthHelper {
}
}
export { AuthHelper }
module.exports = { AuthHelper }

View File

@@ -1,22 +1,24 @@
import { expect } from 'chai'
import { test, given, forCases } from 'sazerac'
import { AuthHelper } from './auth-helper.js'
import { InvalidParameter } from './errors.js'
'use strict'
describe('AuthHelper', function () {
describe('constructor checks', function () {
it('throws without userKey or passKey', function () {
const { expect } = require('chai')
const { test, given, forCases } = require('sazerac')
const { AuthHelper } = require('./auth-helper')
const { InvalidParameter } = require('./errors')
describe('AuthHelper', function() {
describe('constructor checks', function() {
it('throws without userKey or passKey', function() {
expect(() => new AuthHelper({}, {})).to.throw(
Error,
'Expected userKey or passKey to be set'
)
})
it('throws without serviceKey or authorizedOrigins', function () {
it('throws without serviceKey or authorizedOrigins', function() {
expect(
() => new AuthHelper({ userKey: 'myci_user', passKey: 'myci_pass' }, {})
).to.throw(Error, 'Expected authorizedOrigins or serviceKey to be set')
})
it('throws when authorizedOrigins is not an array', function () {
it('throws when authorizedOrigins is not an array', function() {
expect(
() =>
new AuthHelper(
@@ -31,7 +33,7 @@ describe('AuthHelper', function () {
})
})
describe('isValid', function () {
describe('isValid', function() {
function validate(config, privateConfig) {
return new AuthHelper(
{ authorizedOrigins: ['https://example.test'], ...config },
@@ -87,7 +89,7 @@ describe('AuthHelper', function () {
})
})
describe('_basicAuth', function () {
describe('_basicAuth', function() {
function validate(config, privateConfig) {
return new AuthHelper(
{ authorizedOrigins: ['https://example.test'], ...config },
@@ -104,14 +106,14 @@ describe('AuthHelper', function () {
{ userKey: 'myci_user', passKey: 'myci_pass' },
{ myci_user: 'admin', myci_pass: 'abc123' }
),
]).expect({ username: 'admin', password: 'abc123' })
]).expect({ user: 'admin', pass: 'abc123' })
given({ userKey: 'myci_user' }, { myci_user: 'admin' }).expect({
username: 'admin',
password: '',
user: 'admin',
pass: undefined,
})
given({ passKey: 'myci_pass' }, { myci_pass: 'abc123' }).expect({
username: '',
password: 'abc123',
user: undefined,
pass: 'abc123',
})
given({ userKey: 'myci_user', passKey: 'myci_pass' }, {}).expect(
undefined
@@ -120,60 +122,55 @@ describe('AuthHelper', function () {
{ passKey: 'myci_pass', defaultToEmptyStringForUser: true },
{ myci_pass: 'abc123' }
).expect({
username: '',
password: 'abc123',
user: '',
pass: 'abc123',
})
})
})
describe('_isInsecureSslRequest', function () {
describe('_isInsecureSslRequest', function() {
test(AuthHelper._isInsecureSslRequest, () => {
forCases([
given({ url: 'http://example.test' }),
given({ url: 'http://example.test', options: {} }),
given({ url: 'http://example.test', options: { strictSSL: true } }),
given({
url: 'http://example.test',
options: { https: { rejectUnauthorized: true } },
}),
given({
url: 'http://example.test',
options: { https: { rejectUnauthorized: undefined } },
options: { strictSSL: undefined },
}),
]).expect(false)
given({
url: 'http://example.test',
options: { https: { rejectUnauthorized: false } },
options: { strictSSL: false },
}).expect(true)
})
})
describe('enforceStrictSsl', function () {
describe('enforceStrictSsl', function() {
const authConfig = {
userKey: 'myci_user',
passKey: 'myci_pass',
serviceKey: 'myci',
}
context('by default', function () {
context('by default', function() {
const authHelper = new AuthHelper(authConfig, {
public: {
services: { myci: { authorizedOrigins: ['http://myci.test'] } },
},
private: { myci_user: 'admin', myci_pass: 'abc123' },
})
it('does not throw for secure requests', function () {
it('does not throw for secure requests', function() {
expect(() => authHelper.enforceStrictSsl({})).not.to.throw()
})
it('throws for insecure requests', function () {
it('throws for insecure requests', function() {
expect(() =>
authHelper.enforceStrictSsl({
options: { https: { rejectUnauthorized: false } },
})
authHelper.enforceStrictSsl({ options: { strictSSL: false } })
).to.throw(InvalidParameter)
})
})
context("when strict SSL isn't required", function () {
context("when strict SSL isn't required", function() {
const authHelper = new AuthHelper(authConfig, {
public: {
services: {
@@ -185,27 +182,25 @@ describe('AuthHelper', function () {
},
private: { myci_user: 'admin', myci_pass: 'abc123' },
})
it('does not throw for secure requests', function () {
it('does not throw for secure requests', function() {
expect(() => authHelper.enforceStrictSsl({})).not.to.throw()
})
it('does not throw for insecure requests', function () {
it('does not throw for insecure requests', function() {
expect(() =>
authHelper.enforceStrictSsl({
options: { https: { rejectUnauthorized: false } },
})
authHelper.enforceStrictSsl({ options: { strictSSL: false } })
).not.to.throw()
})
})
})
describe('shouldAuthenticateRequest', function () {
describe('shouldAuthenticateRequest', function() {
const authConfig = {
userKey: 'myci_user',
passKey: 'myci_pass',
serviceKey: 'myci',
}
context('by default', function () {
context('by default', function() {
const authHelper = new AuthHelper(authConfig, {
public: {
services: {
@@ -218,20 +213,20 @@ describe('AuthHelper', function () {
})
const shouldAuthenticateRequest = requestOptions =>
authHelper.shouldAuthenticateRequest(requestOptions)
describe('a secure request to an authorized origin', function () {
describe('a secure request to an authorized origin', function() {
test(shouldAuthenticateRequest, () => {
given({ url: 'https://myci.test/api' }).expect(true)
})
})
describe('an insecure request', function () {
describe('an insecure request', function() {
test(shouldAuthenticateRequest, () => {
given({
url: 'https://myci.test/api',
options: { https: { rejectUnauthorized: false } },
options: { strictSSL: false },
}).expect(false)
})
})
describe('a request to an unauthorized origin', function () {
describe('a request to an unauthorized origin', function() {
test(shouldAuthenticateRequest, () => {
forCases([
given({ url: 'http://myci.test/api' }),
@@ -242,7 +237,7 @@ describe('AuthHelper', function () {
})
})
context('when auth over insecure SSL is allowed', function () {
context('when auth over insecure SSL is allowed', function() {
const authHelper = new AuthHelper(authConfig, {
public: {
services: {
@@ -256,20 +251,20 @@ describe('AuthHelper', function () {
})
const shouldAuthenticateRequest = requestOptions =>
authHelper.shouldAuthenticateRequest(requestOptions)
describe('a secure request to an authorized origin', function () {
describe('a secure request to an authorized origin', function() {
test(shouldAuthenticateRequest, () => {
given({ url: 'https://myci.test' }).expect(true)
})
})
describe('an insecure request', function () {
describe('an insecure request', function() {
test(shouldAuthenticateRequest, () => {
given({
url: 'https://myci.test',
options: { https: { rejectUnauthorized: false } },
options: { strictSSL: false },
}).expect(true)
})
})
describe('a request to an unauthorized origin', function () {
describe('a request to an unauthorized origin', function() {
test(shouldAuthenticateRequest, () => {
forCases([
given({ url: 'http://myci.test' }),
@@ -280,7 +275,7 @@ describe('AuthHelper', function () {
})
})
context('when the service is partly configured', function () {
context('when the service is partly configured', function() {
const authHelper = new AuthHelper(authConfig, {
public: {
services: {
@@ -294,7 +289,7 @@ describe('AuthHelper', function () {
})
const shouldAuthenticateRequest = requestOptions =>
authHelper.shouldAuthenticateRequest(requestOptions)
describe('a secure request to an authorized origin', function () {
describe('a secure request to an authorized origin', function() {
test(shouldAuthenticateRequest, () => {
given({ url: 'https://myci.test' }).expect(false)
})
@@ -302,7 +297,7 @@ describe('AuthHelper', function () {
})
})
describe('withBasicAuth', function () {
describe('withBasicAuth', function() {
const authHelper = new AuthHelper(
{
userKey: 'myci_user',
@@ -323,15 +318,14 @@ describe('AuthHelper', function () {
const withBasicAuth = requestOptions =>
authHelper.withBasicAuth(requestOptions)
describe('authenticates a secure request to an authorized origin', function () {
describe('authenticates a secure request to an authorized origin', function() {
test(withBasicAuth, () => {
given({
url: 'https://myci.test/api',
}).expect({
url: 'https://myci.test/api',
options: {
username: 'admin',
password: 'abc123',
auth: { user: 'admin', pass: 'abc123' },
},
})
given({
@@ -343,14 +337,13 @@ describe('AuthHelper', function () {
url: 'https://myci.test/api',
options: {
headers: { Accept: 'application/json' },
username: 'admin',
password: 'abc123',
auth: { user: 'admin', pass: 'abc123' },
},
})
})
})
describe('does not authenticate a request to an unauthorized origin', function () {
describe('does not authenticate a request to an unauthorized origin', function() {
test(withBasicAuth, () => {
given({
url: 'https://other.test/api',
@@ -371,11 +364,11 @@ describe('AuthHelper', function () {
})
})
describe('throws on an insecure SSL request', function () {
describe('throws on an insecure SSL request', function() {
expect(() =>
withBasicAuth({
url: 'https://myci.test/api',
options: { https: { rejectUnauthorized: false } },
options: { strictSSL: false },
})
).to.throw(InvalidParameter)
})

View File

@@ -2,10 +2,12 @@
* @module
*/
import { print } from 'graphql/language/printer.js'
import BaseService from './base.js'
import { InvalidResponse, ShieldsRuntimeError } from './errors.js'
import { parseJson } from './json.js'
'use strict'
const { print } = require('graphql/language/printer')
const BaseService = require('./base')
const { InvalidResponse, ShieldsRuntimeError } = require('./errors')
const { parseJson } = require('./json')
function defaultTransformErrors(errors) {
return new InvalidResponse({ prettyMessage: errors[0].message })
@@ -38,22 +40,19 @@ class BaseGraphqlService extends BaseService {
* representing the query clause of GraphQL POST body
* e.g. gql`{ query { ... } }`
* @param {object} attrs.variables Variables clause of GraphQL POST body
* @param {object} [attrs.options={}] Options to pass to got. See
* [documentation](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md)
* @param {object} [attrs.options={}] Options to pass to request. See
* [documentation](https://github.com/request/request#requestoptions-callback)
* @param {object} [attrs.httpErrorMessages={}] Key-value map of HTTP status codes
* and custom error messages e.g: `{ 404: 'package not found' }`.
* 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)
* @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
* throw error, partial data might be used ignoring the error.
* @param {Function} [attrs.transformErrors=defaultTransformErrors]
* Function which takes an errors object from a GraphQL
* response and returns an instance of ShieldsRuntimeError.
* The default is to return the first entry of the `errors` array as
* an InvalidResponse.
* @returns {object} Parsed response
* @see https://github.com/sindresorhus/got/blob/main/documentation/2-options.md
* @see https://github.com/request/request#requestoptions-callback
*/
async _requestGraphql({
schema,
@@ -62,7 +61,6 @@ class BaseGraphqlService extends BaseService {
variables = {},
options = {},
httpErrorMessages = {},
transformJson = data => data,
transformErrors = defaultTransformErrors,
}) {
const mergedOptions = {
@@ -76,7 +74,7 @@ class BaseGraphqlService extends BaseService {
options: mergedOptions,
errorMessages: httpErrorMessages,
})
const json = transformJson(this._parseJson(buffer))
const json = this._parseJson(buffer)
if (json.errors) {
const exception = transformErrors(json.errors)
if (exception instanceof ShieldsRuntimeError) {
@@ -91,4 +89,4 @@ class BaseGraphqlService extends BaseService {
}
}
export default BaseGraphqlService
module.exports = BaseGraphqlService

View File

@@ -1,17 +1,26 @@
import Joi from 'joi'
import { expect } from 'chai'
import gql from 'graphql-tag'
import sinon from 'sinon'
import BaseGraphqlService from './base-graphql.js'
import { InvalidResponse } from './errors.js'
'use strict'
const Joi = require('@hapi/joi')
const { expect } = require('chai')
const gql = require('graphql-tag')
const sinon = require('sinon')
const BaseGraphqlService = require('./base-graphql')
const { InvalidResponse } = require('./errors')
const dummySchema = Joi.object({
requiredString: Joi.string().required(),
}).required()
class DummyGraphqlService extends BaseGraphqlService {
static category = 'cat'
static route = { base: 'foo' }
static get category() {
return 'cat'
}
static get route() {
return {
base: 'foo',
}
}
async handle() {
const { requiredString } = await this._requestGraphql({
@@ -27,11 +36,11 @@ class DummyGraphqlService extends BaseGraphqlService {
}
}
describe('BaseGraphqlService', function () {
describe('Making requests', function () {
let requestFetcher
beforeEach(function () {
requestFetcher = sinon.stub().returns(
describe('BaseGraphqlService', function() {
describe('Making requests', function() {
let sendAndCacheRequest
beforeEach(function() {
sendAndCacheRequest = sinon.stub().returns(
Promise.resolve({
buffer: '{"some": "json"}',
res: { statusCode: 200 },
@@ -39,13 +48,13 @@ describe('BaseGraphqlService', function () {
)
})
it('invokes _requestFetcher', async function () {
it('invokes _sendAndCacheRequest', async function() {
await DummyGraphqlService.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
expect(requestFetcher).to.have.been.calledOnceWith(
expect(sendAndCacheRequest).to.have.been.calledOnceWith(
'http://example.com/graphql',
{
body: '{"query":"{\\n requiredString\\n}\\n","variables":{}}',
@@ -55,7 +64,7 @@ describe('BaseGraphqlService', function () {
)
})
it('forwards options to _requestFetcher', async function () {
it('forwards options to _sendAndCacheRequest', async function() {
class WithOptions extends DummyGraphqlService {
async handle() {
const { value } = await this._requestGraphql({
@@ -66,38 +75,38 @@ describe('BaseGraphqlService', function () {
requiredString
}
`,
options: { searchParams: { queryParam: 123 } },
options: { qs: { queryParam: 123 } },
})
return { message: value }
}
}
await WithOptions.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
expect(requestFetcher).to.have.been.calledOnceWith(
expect(sendAndCacheRequest).to.have.been.calledOnceWith(
'http://example.com/graphql',
{
body: '{"query":"{\\n requiredString\\n}\\n","variables":{}}',
headers: { Accept: 'application/json' },
method: 'POST',
searchParams: { queryParam: 123 },
qs: { queryParam: 123 },
}
)
})
})
describe('Making badges', function () {
it('handles valid json responses', async function () {
const requestFetcher = async () => ({
describe('Making badges', function() {
it('handles valid json responses', async function() {
const sendAndCacheRequest = async () => ({
buffer: '{"requiredString": "some-string"}',
res: { statusCode: 200 },
})
expect(
await DummyGraphqlService.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
).to.deep.equal({
@@ -105,14 +114,14 @@ describe('BaseGraphqlService', function () {
})
})
it('handles json responses which do not match the schema', async function () {
const requestFetcher = async () => ({
it('handles json responses which do not match the schema', async function() {
const sendAndCacheRequest = async () => ({
buffer: '{"unexpectedKey": "some-string"}',
res: { statusCode: 200 },
})
expect(
await DummyGraphqlService.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
).to.deep.equal({
@@ -122,14 +131,14 @@ describe('BaseGraphqlService', function () {
})
})
it('handles unparseable json responses', async function () {
const requestFetcher = async () => ({
it('handles unparseable json responses', async function() {
const sendAndCacheRequest = async () => ({
buffer: 'not json',
res: { statusCode: 200 },
})
expect(
await DummyGraphqlService.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
).to.deep.equal({
@@ -140,15 +149,15 @@ describe('BaseGraphqlService', function () {
})
})
describe('Error handling', function () {
it('handles generic error', async function () {
const requestFetcher = async () => ({
describe('Error handling', function() {
it('handles generic error', async function() {
const sendAndCacheRequest = async () => ({
buffer: '{ "errors": [ { "message": "oh noes!!" } ] }',
res: { statusCode: 200 },
})
expect(
await DummyGraphqlService.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
).to.deep.equal({
@@ -158,7 +167,7 @@ describe('BaseGraphqlService', function () {
})
})
it('handles custom error', async function () {
it('handles custom error', async function() {
class WithErrorHandler extends DummyGraphqlService {
async handle() {
const { requiredString } = await this._requestGraphql({
@@ -169,7 +178,7 @@ describe('BaseGraphqlService', function () {
requiredString
}
`,
transformErrors: function (errors) {
transformErrors: function(errors) {
if (errors[0].message === 'oh noes!!') {
return new InvalidResponse({
prettyMessage: 'a terrible thing has happened',
@@ -181,13 +190,13 @@ describe('BaseGraphqlService', function () {
}
}
const requestFetcher = async () => ({
const sendAndCacheRequest = async () => ({
buffer: '{ "errors": [ { "message": "oh noes!!" } ] }',
res: { statusCode: 200 },
})
expect(
await WithErrorHandler.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
).to.deep.equal({

View File

@@ -2,8 +2,10 @@
* @module
*/
import BaseService from './base.js'
import { parseJson } from './json.js'
'use strict'
const BaseService = require('./base')
const { parseJson } = require('./json')
/**
* Services which query a JSON endpoint should extend BaseJsonService
@@ -28,14 +30,14 @@ class BaseJsonService extends BaseService {
* @param {object} attrs Refer to individual attrs
* @param {Joi} attrs.schema Joi schema to validate the response against
* @param {string} attrs.url URL to request
* @param {object} [attrs.options={}] Options to pass to got. See
* [documentation](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md)
* @param {object} [attrs.options={}] Options to pass to request. See
* [documentation](https://github.com/request/request#requestoptions-callback)
* @param {object} [attrs.errorMessages={}] Key-value map of status codes
* and custom error messages e.g: `{ 404: 'package not found' }`.
* 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)
* @returns {object} Parsed response
* @see https://github.com/sindresorhus/got/blob/main/documentation/2-options.md
* @see https://github.com/request/request#requestoptions-callback
*/
async _requestJson({ schema, url, options = {}, errorMessages = {} }) {
const mergedOptions = {
@@ -52,4 +54,4 @@ class BaseJsonService extends BaseService {
}
}
export default BaseJsonService
module.exports = BaseJsonService

View File

@@ -1,15 +1,24 @@
import Joi from 'joi'
import { expect } from 'chai'
import sinon from 'sinon'
import BaseJsonService from './base-json.js'
'use strict'
const Joi = require('@hapi/joi')
const { expect } = require('chai')
const sinon = require('sinon')
const BaseJsonService = require('./base-json')
const dummySchema = Joi.object({
requiredString: Joi.string().required(),
}).required()
class DummyJsonService extends BaseJsonService {
static category = 'cat'
static route = { base: 'foo' }
static get category() {
return 'cat'
}
static get route() {
return {
base: 'foo',
}
}
async handle() {
const { requiredString } = await this._requestJson({
@@ -20,11 +29,11 @@ class DummyJsonService extends BaseJsonService {
}
}
describe('BaseJsonService', function () {
describe('Making requests', function () {
let requestFetcher
beforeEach(function () {
requestFetcher = sinon.stub().returns(
describe('BaseJsonService', function() {
describe('Making requests', function() {
let sendAndCacheRequest
beforeEach(function() {
sendAndCacheRequest = sinon.stub().returns(
Promise.resolve({
buffer: '{"some": "json"}',
res: { statusCode: 200 },
@@ -32,13 +41,13 @@ describe('BaseJsonService', function () {
)
})
it('invokes _requestFetcher', async function () {
it('invokes _sendAndCacheRequest', async function() {
await DummyJsonService.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
expect(requestFetcher).to.have.been.calledOnceWith(
expect(sendAndCacheRequest).to.have.been.calledOnceWith(
'http://example.com/foo.json',
{
headers: { Accept: 'application/json' },
@@ -46,43 +55,43 @@ describe('BaseJsonService', function () {
)
})
it('forwards options to _requestFetcher', async function () {
it('forwards options to _sendAndCacheRequest', async function() {
class WithOptions extends DummyJsonService {
async handle() {
const { value } = await this._requestJson({
schema: dummySchema,
url: 'http://example.com/foo.json',
options: { method: 'POST', searchParams: { queryParam: 123 } },
options: { method: 'POST', qs: { queryParam: 123 } },
})
return { message: value }
}
}
await WithOptions.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
expect(requestFetcher).to.have.been.calledOnceWith(
expect(sendAndCacheRequest).to.have.been.calledOnceWith(
'http://example.com/foo.json',
{
headers: { Accept: 'application/json' },
method: 'POST',
searchParams: { queryParam: 123 },
qs: { queryParam: 123 },
}
)
})
})
describe('Making badges', function () {
it('handles valid json responses', async function () {
const requestFetcher = async () => ({
describe('Making badges', function() {
it('handles valid json responses', async function() {
const sendAndCacheRequest = async () => ({
buffer: '{"requiredString": "some-string"}',
res: { statusCode: 200 },
})
expect(
await DummyJsonService.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
).to.deep.equal({
@@ -90,14 +99,14 @@ describe('BaseJsonService', function () {
})
})
it('handles json responses which do not match the schema', async function () {
const requestFetcher = async () => ({
it('handles json responses which do not match the schema', async function() {
const sendAndCacheRequest = async () => ({
buffer: '{"unexpectedKey": "some-string"}',
res: { statusCode: 200 },
})
expect(
await DummyJsonService.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
).to.deep.equal({
@@ -107,14 +116,14 @@ describe('BaseJsonService', function () {
})
})
it('handles unparseable json responses', async function () {
const requestFetcher = async () => ({
it('handles unparseable json responses', async function() {
const sendAndCacheRequest = async () => ({
buffer: 'not json',
res: { statusCode: 200 },
})
expect(
await DummyJsonService.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
).to.deep.equal({

View File

@@ -0,0 +1,74 @@
'use strict'
const makeBadge = require('../../gh-badges/lib/make-badge')
const BaseService = require('./base')
const { MetricHelper } = require('./metric-helper')
const { setCacheHeaders } = require('./cache-headers')
const { makeSend } = require('./legacy-result-sender')
const coalesceBadge = require('./coalesce-badge')
const { prepareRoute, namedParamsForMatch } = require('./route')
// Badges are subject to two independent types of caching: in-memory and
// downstream.
//
// Services deriving from `NonMemoryCachingBaseService` are not cached in
// memory on the server. This means that each request that hits the server
// triggers another call to the handler. When using badges for server
// diagnostics, that's useful!
//
// In contrast, The `handle()` function of most other `BaseService`
// subclasses is wrapped in onboard, in-memory caching. See `lib /request-
// handler.js` and `BaseService.prototype.register()`.
//
// All services, including those extending NonMemoryCachingBaseServices, may
// be cached _downstream_. This is governed by cache headers, which are
// configured by the service, the user's request, and the server's default
// cache length.
module.exports = class NonMemoryCachingBaseService extends BaseService {
static register({ camp, metricInstance }, serviceConfig) {
const { cacheHeaders: cacheHeaderConfig } = serviceConfig
const { _cacheLength: serviceDefaultCacheLengthSeconds } = this
const { regex, captureNames } = prepareRoute(this.route)
const metricHelper = MetricHelper.create({
metricInstance,
ServiceClass: this,
})
camp.route(regex, async (queryParams, match, end, ask) => {
const metricHandle = metricHelper.startRequest()
const namedParams = namedParamsForMatch(captureNames, match, this)
const serviceData = await this.invoke(
{},
serviceConfig,
namedParams,
queryParams
)
const badgeData = coalesceBadge(
queryParams,
serviceData,
this.defaultBadgeData,
this
)
// The final capture group is the extension.
const format = (match.slice(-1)[0] || '.svg').replace(/^\./, '')
badgeData.format = format
const svg = makeBadge(badgeData)
setCacheHeaders({
cacheHeaderConfig,
serviceDefaultCacheLengthSeconds,
queryParams,
res: ask.res,
})
makeSend(format, ask.res, end)(svg)
metricHandle.noteResponseSent()
})
}
}

View File

@@ -1,29 +1,60 @@
import BaseService from './base.js'
import {
'use strict'
const makeBadge = require('../../gh-badges/lib/make-badge')
const BaseService = require('./base')
const {
serverHasBeenUpSinceResourceCached,
setCacheHeadersForStaticResource,
} from './cache-headers.js'
import { prepareRoute } from './route.js'
} = require('./cache-headers')
const { makeSend } = require('./legacy-result-sender')
const { MetricHelper } = require('./metric-helper')
const coalesceBadge = require('./coalesce-badge')
const { prepareRoute, namedParamsForMatch } = require('./route')
export default class BaseStaticService extends BaseService {
static _applyCacheHeaders({ res }) {
setCacheHeadersForStaticResource(res)
}
module.exports = class BaseStaticService extends BaseService {
static register({ camp, metricInstance }, serviceConfig) {
const { regex, captureNames } = prepareRoute(this.route)
static register({ app, ...serviceContext }, serviceConfig) {
const { regex } = prepareRoute(this.route)
app.get(
regex,
(req, res, next) => {
if (serverHasBeenUpSinceResourceCached(req)) {
// Send Not Modified.
res.status(304)
res.end()
} else {
next()
}
},
this.makeExpressHandler(serviceContext, serviceConfig)
)
const metricHelper = MetricHelper.create({
metricInstance,
ServiceClass: this,
})
camp.route(regex, async (queryParams, match, end, ask) => {
if (serverHasBeenUpSinceResourceCached(ask.req)) {
// Send Not Modified.
ask.res.statusCode = 304
ask.res.end()
return
}
const metricHandle = metricHelper.startRequest()
const namedParams = namedParamsForMatch(captureNames, match, this)
const serviceData = await this.invoke(
{},
serviceConfig,
namedParams,
queryParams
)
const badgeData = coalesceBadge(
queryParams,
serviceData,
this.defaultBadgeData,
this
)
// The final capture group is the extension.
const format = (match.slice(-1)[0] || '.svg').replace(/^\./, '')
badgeData.format = format
setCacheHeadersForStaticResource(ask.res)
const svg = makeBadge(badgeData)
makeSend(format, ask.res, end)(svg)
metricHandle.noteResponseSent()
})
}
}

View File

@@ -2,11 +2,13 @@
* @module
*/
'use strict'
// See available emoji at http://emoji.muan.co/
import emojic from 'emojic'
import BaseService from './base.js'
import trace from './trace.js'
import { InvalidResponse } from './errors.js'
const emojic = require('emojic')
const BaseService = require('./base')
const trace = require('./trace')
const { InvalidResponse } = require('./errors')
const defaultValueMatcher = />([^<>]+)<\/text><\/g>/
const leadingWhitespace = /(?:\r\n\s*|\r\s*|\n\s*)/g
@@ -51,14 +53,14 @@ class BaseSvgScrapingService extends BaseService {
* @param {RegExp} attrs.valueMatcher
* RegExp to match the value we want to parse from the SVG
* @param {string} attrs.url URL to request
* @param {object} [attrs.options={}] Options to pass to got. See
* [documentation](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md)
* @param {object} [attrs.options={}] Options to pass to request. See
* [documentation](https://github.com/request/request#requestoptions-callback)
* @param {object} [attrs.errorMessages={}] Key-value map of status codes
* and custom error messages e.g: `{ 404: 'package not found' }`.
* 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)
* @returns {object} Parsed response
* @see https://github.com/sindresorhus/got/blob/main/documentation/2-options.md
* @see https://github.com/request/request#requestoptions-callback
*/
async _requestSvg({
schema,
@@ -88,4 +90,4 @@ class BaseSvgScrapingService extends BaseService {
}
}
export default BaseSvgScrapingService
module.exports = BaseSvgScrapingService

View File

@@ -1,16 +1,29 @@
import { expect } from 'chai'
import sinon from 'sinon'
import Joi from 'joi'
import makeBadge from '../../badge-maker/lib/make-badge.js'
import BaseSvgScrapingService from './base-svg-scraping.js'
'use strict'
const { expect } = require('chai')
const sinon = require('sinon')
const Joi = require('@hapi/joi')
const makeBadge = require('../../gh-badges/lib/make-badge')
const BaseSvgScrapingService = require('./base-svg-scraping')
function makeExampleSvg({ label, message }) {
return makeBadge({ text: ['this is the label', 'this is the result!'] })
}
const schema = Joi.object({
message: Joi.string().required(),
}).required()
class DummySvgScrapingService extends BaseSvgScrapingService {
static category = 'cat'
static route = { base: 'foo' }
static get category() {
return 'cat'
}
static get route() {
return {
base: 'foo',
}
}
async handle() {
return this._requestSvg({
@@ -20,23 +33,26 @@ class DummySvgScrapingService extends BaseSvgScrapingService {
}
}
describe('BaseSvgScrapingService', function () {
describe('BaseSvgScrapingService', function() {
const exampleLabel = 'this is the label'
const exampleMessage = 'this is the result!'
const exampleSvg = makeBadge({ label: exampleLabel, message: exampleMessage })
const exampleSvg = makeExampleSvg({
label: exampleLabel,
message: exampleMessage,
})
describe('valueFromSvgBadge', function () {
it('should find the correct value', function () {
describe('valueFromSvgBadge', function() {
it('should find the correct value', function() {
expect(BaseSvgScrapingService.valueFromSvgBadge(exampleSvg)).to.equal(
exampleMessage
)
})
})
describe('Making requests', function () {
let requestFetcher
beforeEach(function () {
requestFetcher = sinon.stub().returns(
describe('Making requests', function() {
let sendAndCacheRequest
beforeEach(function() {
sendAndCacheRequest = sinon.stub().returns(
Promise.resolve({
buffer: exampleSvg,
res: { statusCode: 200 },
@@ -44,13 +60,13 @@ describe('BaseSvgScrapingService', function () {
)
})
it('invokes _requestFetcher with the expected header', async function () {
it('invokes _sendAndCacheRequest with the expected header', async function() {
await DummySvgScrapingService.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
expect(requestFetcher).to.have.been.calledOnceWith(
expect(sendAndCacheRequest).to.have.been.calledOnceWith(
'http://example.com/foo.svg',
{
headers: { Accept: 'image/svg+xml' },
@@ -58,7 +74,7 @@ describe('BaseSvgScrapingService', function () {
)
})
it('forwards options to _requestFetcher', async function () {
it('forwards options to _sendAndCacheRequest', async function() {
class WithCustomOptions extends DummySvgScrapingService {
async handle() {
const { message } = await this._requestSvg({
@@ -66,7 +82,7 @@ describe('BaseSvgScrapingService', function () {
url: 'http://example.com/foo.svg',
options: {
method: 'POST',
searchParams: { queryParam: 123 },
qs: { queryParam: 123 },
},
})
return { message }
@@ -74,30 +90,30 @@ describe('BaseSvgScrapingService', function () {
}
await WithCustomOptions.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
expect(requestFetcher).to.have.been.calledOnceWith(
expect(sendAndCacheRequest).to.have.been.calledOnceWith(
'http://example.com/foo.svg',
{
method: 'POST',
headers: { Accept: 'image/svg+xml' },
searchParams: { queryParam: 123 },
qs: { queryParam: 123 },
}
)
})
})
describe('Making badges', function () {
it('handles valid svg responses', async function () {
const requestFetcher = async () => ({
describe('Making badges', function() {
it('handles valid svg responses', async function() {
const sendAndCacheRequest = async () => ({
buffer: exampleSvg,
res: { statusCode: 200 },
})
expect(
await DummySvgScrapingService.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
).to.deep.equal({
@@ -105,9 +121,11 @@ describe('BaseSvgScrapingService', function () {
})
})
it('allows overriding the valueMatcher', async function () {
it('allows overriding the valueMatcher', async function() {
class WithValueMatcher extends BaseSvgScrapingService {
static route = {}
static get route() {
return {}
}
async handle() {
return this._requestSvg({
@@ -117,13 +135,13 @@ describe('BaseSvgScrapingService', function () {
})
}
}
const requestFetcher = async () => ({
const sendAndCacheRequest = async () => ({
buffer: '<desc>a different message</desc>',
res: { statusCode: 200 },
})
expect(
await WithValueMatcher.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
).to.deep.equal({
@@ -131,14 +149,14 @@ describe('BaseSvgScrapingService', function () {
})
})
it('handles unparseable svg responses', async function () {
const requestFetcher = async () => ({
it('handles unparseable svg responses', async function() {
const sendAndCacheRequest = async () => ({
buffer: 'not svg yo',
res: { statusCode: 200 },
})
expect(
await DummySvgScrapingService.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
).to.deep.equal({

View File

@@ -2,12 +2,14 @@
* @module
*/
'use strict'
// See available emoji at http://emoji.muan.co/
import emojic from 'emojic'
import { XMLParser, XMLValidator } from 'fast-xml-parser'
import BaseService from './base.js'
import trace from './trace.js'
import { InvalidResponse } from './errors.js'
const emojic = require('emojic')
const fastXmlParser = require('fast-xml-parser')
const BaseService = require('./base')
const trace = require('./trace')
const { InvalidResponse } = require('./errors')
/**
* Services which query a XML endpoint should extend BaseXmlService
@@ -22,8 +24,8 @@ class BaseXmlService extends BaseService {
* @param {object} attrs Refer to individual attrs
* @param {Joi} attrs.schema Joi schema to validate the response against
* @param {string} attrs.url URL to request
* @param {object} [attrs.options={}] Options to pass to got. See
* [documentation](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md)
* @param {object} [attrs.options={}] Options to pass to request. See
* [documentation](https://github.com/request/request#requestoptions-callback)
* @param {object} [attrs.errorMessages={}] Key-value map of status codes
* and custom error messages e.g: `{ 404: 'package not found' }`.
* This can be used to extend or override the
@@ -31,7 +33,7 @@ class BaseXmlService extends BaseService {
* @param {object} [attrs.parserOptions={}] Options to pass to fast-xml-parser. See
* [documentation](https://github.com/NaturalIntelligence/fast-xml-parser#xml-to-json)
* @returns {object} Parsed response
* @see https://github.com/sindresorhus/got/blob/main/documentation/2-options.md
* @see https://github.com/request/request#requestoptions-callback
* @see https://github.com/NaturalIntelligence/fast-xml-parser#xml-to-json
*/
async _requestXml({
@@ -51,15 +53,14 @@ class BaseXmlService extends BaseService {
options: mergedOptions,
errorMessages,
})
const validateResult = XMLValidator.validate(buffer)
const validateResult = fastXmlParser.validate(buffer)
if (validateResult !== true) {
throw new InvalidResponse({
prettyMessage: 'unparseable xml response',
underlyingError: validateResult.err,
})
}
const parser = new XMLParser(parserOptions)
const xml = parser.parse(buffer)
const xml = fastXmlParser.parse(buffer, parserOptions)
logTrace(emojic.dart, 'Response XML (before validation)', xml, {
deep: true,
})
@@ -67,4 +68,4 @@ class BaseXmlService extends BaseService {
}
}
export default BaseXmlService
module.exports = BaseXmlService

View File

@@ -1,15 +1,24 @@
import Joi from 'joi'
import { expect } from 'chai'
import sinon from 'sinon'
import BaseXmlService from './base-xml.js'
'use strict'
const Joi = require('@hapi/joi')
const { expect } = require('chai')
const sinon = require('sinon')
const BaseXmlService = require('./base-xml')
const dummySchema = Joi.object({
requiredString: Joi.string().required(),
}).required()
class DummyXmlService extends BaseXmlService {
static category = 'cat'
static route = { base: 'foo' }
static get category() {
return 'cat'
}
static get route() {
return {
base: 'foo',
}
}
async handle() {
const { requiredString } = await this._requestXml({
@@ -20,11 +29,11 @@ class DummyXmlService extends BaseXmlService {
}
}
describe('BaseXmlService', function () {
describe('Making requests', function () {
let requestFetcher
beforeEach(function () {
requestFetcher = sinon.stub().returns(
describe('BaseXmlService', function() {
describe('Making requests', function() {
let sendAndCacheRequest
beforeEach(function() {
sendAndCacheRequest = sinon.stub().returns(
Promise.resolve({
buffer: '<requiredString>some-string</requiredString>',
res: { statusCode: 200 },
@@ -32,13 +41,13 @@ describe('BaseXmlService', function () {
)
})
it('invokes _requestFetcher', async function () {
it('invokes _sendAndCacheRequest', async function() {
await DummyXmlService.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
expect(requestFetcher).to.have.been.calledOnceWith(
expect(sendAndCacheRequest).to.have.been.calledOnceWith(
'http://example.com/foo.xml',
{
headers: { Accept: 'application/xml, text/xml' },
@@ -46,45 +55,47 @@ describe('BaseXmlService', function () {
)
})
it('forwards options to _requestFetcher', async function () {
it('forwards options to _sendAndCacheRequest', async function() {
class WithCustomOptions extends BaseXmlService {
static route = {}
static get route() {
return {}
}
async handle() {
const { requiredString } = await this._requestXml({
schema: dummySchema,
url: 'http://example.com/foo.xml',
options: { method: 'POST', searchParams: { queryParam: 123 } },
options: { method: 'POST', qs: { queryParam: 123 } },
})
return { message: requiredString }
}
}
await WithCustomOptions.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
expect(requestFetcher).to.have.been.calledOnceWith(
expect(sendAndCacheRequest).to.have.been.calledOnceWith(
'http://example.com/foo.xml',
{
headers: { Accept: 'application/xml, text/xml' },
method: 'POST',
searchParams: { queryParam: 123 },
qs: { queryParam: 123 },
}
)
})
})
describe('Making badges', function () {
it('handles valid xml responses', async function () {
const requestFetcher = async () => ({
describe('Making badges', function() {
it('handles valid xml responses', async function() {
const sendAndCacheRequest = async () => ({
buffer: '<requiredString>some-string</requiredString>',
res: { statusCode: 200 },
})
expect(
await DummyXmlService.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
).to.deep.equal({
@@ -92,7 +103,7 @@ describe('BaseXmlService', function () {
})
})
it('parses XML response with custom parser options', async function () {
it('parses XML response with custom parser options', async function() {
const customParserOption = { trimValues: false }
class DummyXmlServiceWithParserOption extends DummyXmlService {
async handle() {
@@ -104,14 +115,14 @@ describe('BaseXmlService', function () {
return { message: requiredString }
}
}
const requestFetcher = async () => ({
const sendAndCacheRequest = async () => ({
buffer:
'<requiredString>some-string with trailing whitespace </requiredString>',
res: { statusCode: 200 },
})
expect(
await DummyXmlServiceWithParserOption.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
).to.deep.equal({
@@ -119,14 +130,14 @@ describe('BaseXmlService', function () {
})
})
it('handles xml responses which do not match the schema', async function () {
const requestFetcher = async () => ({
it('handles xml responses which do not match the schema', async function() {
const sendAndCacheRequest = async () => ({
buffer: '<unexpectedAttribute>some-string</unexpectedAttribute>',
res: { statusCode: 200 },
})
expect(
await DummyXmlService.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
).to.deep.equal({
@@ -136,14 +147,14 @@ describe('BaseXmlService', function () {
})
})
it('handles unparseable xml responses', async function () {
const requestFetcher = async () => ({
it('handles unparseable xml responses', async function() {
const sendAndCacheRequest = async () => ({
buffer: 'not xml',
res: { statusCode: 200 },
})
expect(
await DummyXmlService.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
).to.deep.equal({

View File

@@ -2,11 +2,13 @@
* @module
*/
import emojic from 'emojic'
import yaml from 'js-yaml'
import BaseService from './base.js'
import { InvalidResponse } from './errors.js'
import trace from './trace.js'
'use strict'
const emojic = require('emojic')
const yaml = require('js-yaml')
const BaseService = require('./base')
const { InvalidResponse } = require('./errors')
const trace = require('./trace')
/**
* Services which query a YAML endpoint should extend BaseYamlService
@@ -21,15 +23,15 @@ class BaseYamlService extends BaseService {
* @param {object} attrs Refer to individual attrs
* @param {Joi} attrs.schema Joi schema to validate the response against
* @param {string} attrs.url URL to request
* @param {object} [attrs.options={}] Options to pass to got. See
* [documentation](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md)
* @param {object} [attrs.options={}] Options to pass to request. See
* [documentation](https://github.com/request/request#requestoptions-callback)
* @param {object} [attrs.errorMessages={}] Key-value map of status codes
* and custom error messages e.g: `{ 404: 'package not found' }`.
* 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)
* @param {object} [attrs.encoding='utf8'] Character encoding
* @returns {object} Parsed response
* @see https://github.com/sindresorhus/got/blob/main/documentation/2-options.md
* @see https://github.com/request/request#requestoptions-callback
*/
async _requestYaml({
schema,
@@ -55,7 +57,7 @@ class BaseYamlService extends BaseService {
})
let parsed
try {
parsed = yaml.load(buffer.toString(), encoding)
parsed = yaml.safeLoad(buffer.toString(), encoding)
} catch (err) {
logTrace(emojic.dart, 'Response YAML (unparseable)', buffer)
throw new InvalidResponse({
@@ -70,4 +72,4 @@ class BaseYamlService extends BaseService {
}
}
export default BaseYamlService
module.exports = BaseYamlService

View File

@@ -1,15 +1,24 @@
import Joi from 'joi'
import { expect } from 'chai'
import sinon from 'sinon'
import BaseYamlService from './base-yaml.js'
'use strict'
const Joi = require('@hapi/joi')
const { expect } = require('chai')
const sinon = require('sinon')
const BaseYamlService = require('./base-yaml')
const dummySchema = Joi.object({
requiredString: Joi.string().required(),
}).required()
class DummyYamlService extends BaseYamlService {
static category = 'cat'
static route = { base: 'foo' }
static get category() {
return 'cat'
}
static get route() {
return {
base: 'foo',
}
}
async handle() {
const { requiredString } = await this._requestYaml({
@@ -36,11 +45,11 @@ foo: bar
foo: baz
`
describe('BaseYamlService', function () {
describe('Making requests', function () {
let requestFetcher
beforeEach(function () {
requestFetcher = sinon.stub().returns(
describe('BaseYamlService', function() {
describe('Making requests', function() {
let sendAndCacheRequest
beforeEach(function() {
sendAndCacheRequest = sinon.stub().returns(
Promise.resolve({
buffer: expectedYaml,
res: { statusCode: 200 },
@@ -48,13 +57,13 @@ describe('BaseYamlService', function () {
)
})
it('invokes _requestFetcher', async function () {
it('invokes _sendAndCacheRequest', async function() {
await DummyYamlService.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
expect(requestFetcher).to.have.been.calledOnceWith(
expect(sendAndCacheRequest).to.have.been.calledOnceWith(
'http://example.com/foo.yaml',
{
headers: {
@@ -65,24 +74,24 @@ describe('BaseYamlService', function () {
)
})
it('forwards options to _requestFetcher', async function () {
it('forwards options to _sendAndCacheRequest', async function() {
class WithOptions extends DummyYamlService {
async handle() {
const { requiredString } = await this._requestYaml({
schema: dummySchema,
url: 'http://example.com/foo.yaml',
options: { method: 'POST', searchParams: { queryParam: 123 } },
options: { method: 'POST', qs: { queryParam: 123 } },
})
return { message: requiredString }
}
}
await WithOptions.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
expect(requestFetcher).to.have.been.calledOnceWith(
expect(sendAndCacheRequest).to.have.been.calledOnceWith(
'http://example.com/foo.yaml',
{
headers: {
@@ -90,21 +99,21 @@ describe('BaseYamlService', function () {
'text/x-yaml, text/yaml, application/x-yaml, application/yaml, text/plain',
},
method: 'POST',
searchParams: { queryParam: 123 },
qs: { queryParam: 123 },
}
)
})
})
describe('Making badges', function () {
it('handles valid yaml responses', async function () {
const requestFetcher = async () => ({
describe('Making badges', function() {
it('handles valid yaml responses', async function() {
const sendAndCacheRequest = async () => ({
buffer: expectedYaml,
res: { statusCode: 200 },
})
expect(
await DummyYamlService.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
).to.deep.equal({
@@ -112,14 +121,14 @@ describe('BaseYamlService', function () {
})
})
it('handles yaml responses which do not match the schema', async function () {
const requestFetcher = async () => ({
it('handles yaml responses which do not match the schema', async function() {
const sendAndCacheRequest = async () => ({
buffer: unexpectedYaml,
res: { statusCode: 200 },
})
expect(
await DummyYamlService.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
).to.deep.equal({
@@ -129,14 +138,14 @@ describe('BaseYamlService', function () {
})
})
it('handles unparseable yaml responses', async function () {
const requestFetcher = async () => ({
it('handles unparseable yaml responses', async function() {
const sendAndCacheRequest = async () => ({
buffer: invalidYaml,
res: { statusCode: 200 },
})
expect(
await DummyYamlService.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
).to.deep.equal({

View File

@@ -1,42 +1,37 @@
'use strict'
/**
* @module
*/
const bytes = require('bytes')
// See available emoji at http://emoji.muan.co/
import emojic from 'emojic'
import Joi from 'joi'
import log from '../server/log.js'
import makeBadge from '../../badge-maker/lib/make-badge.js'
import { AuthHelper } from './auth-helper.js'
import { MetricHelper, MetricNames } from './metric-helper.js'
import {
coalesceCacheLength,
setHeadersForCacheLength,
} from './cache-headers.js'
import { assertValidCategory } from './categories.js'
import checkErrorResponse from './check-error-response.js'
import coalesceBadge from './coalesce-badge.js'
import {
const emojic = require('emojic')
const Joi = require('@hapi/joi')
const log = require('../server/log')
const { AuthHelper } = require('./auth-helper')
const { MetricHelper, MetricNames } = require('./metric-helper')
const { assertValidCategory } = require('./categories')
const checkErrorResponse = require('./check-error-response')
const coalesceBadge = require('./coalesce-badge')
const {
NotFound,
InvalidResponse,
Inaccessible,
ImproperlyConfigured,
InvalidParameter,
Deprecated,
} from './errors.js'
import { validateExample, transformExample } from './examples.js'
import { fetch } from './got.js'
import { makeJsonBadge } from './make-json-badge.js'
import {
} = require('./errors')
const { validateExample, transformExample } = require('./examples')
const {
makeFullUrl,
assertValidRoute,
paramsForReq,
prepareRoute,
namedParamsForMatch,
getQueryParamNames,
} from './route.js'
import { assertValidServiceDefinition } from './service-definitions.js'
import trace from './trace.js'
import validate from './validate.js'
} = require('./route')
const { assertValidServiceDefinition } = require('./service-definitions')
const trace = require('./trace')
const validate = require('./validate')
const defaultBadgeDataSchema = Joi.object({
label: Joi.string(),
@@ -64,7 +59,10 @@ const serviceDataSchema = Joi.object({
// `render()` to always return a string.
message: Joi.alternatives(Joi.string().allow(''), Joi.number()).required(),
color: Joi.string(),
link: Joi.array().items(Joi.string().uri()).single().max(2),
link: Joi.array()
.items(Joi.string().uri())
.single()
.max(2),
// Generally services should not use these options, which are provided to
// support the Endpoint badge.
labelColor: Joi.string(),
@@ -73,7 +71,9 @@ const serviceDataSchema = Joi.object({
logoColor: optionalStringWhenNamedLogoPresent,
logoWidth: optionalNumberWhenAnyLogoPresent,
logoPosition: optionalNumberWhenAnyLogoPresent,
cacheSeconds: Joi.number().integer().min(0),
cacheSeconds: Joi.number()
.integer()
.min(0),
style: Joi.string(),
})
.oxor('namedLogo', 'logoSvg')
@@ -96,7 +96,9 @@ class BaseService {
throw new Error(`Category not set for ${this.name}`)
}
static isDeprecated = false
static get isDeprecated() {
return false
}
/**
* Route to mount this service on
@@ -114,19 +116,18 @@ class BaseService {
*
* See also the config schema in `./server.js` and `doc/server-secrets.md`.
*
* To use the configured auth in the handler or fetch method, wrap the
* _request() input params in a call to one of:
* - this.authHelper.withBasicAuth()
* - this.authHelper.withBearerAuthHeader()
* - this.authHelper.withQueryStringAuth()
*
* For example:
* this._request(this.authHelper.withBasicAuth({ url, schema, options }))
* To use the configured auth in the handler or fetch method, pass the
* credentials to the request. For example:
* - `{ options: { auth: this.authHelper.basicAuth } }`
* - `{ options: { headers: this.authHelper.bearerAuthHeader } }`
* - `{ options: { qs: { token: this.authHelper.pass } } }`
*
* @abstract
* @type {module:core/base-service/base~Auth}
*/
static auth = undefined
static get auth() {
return undefined
}
/**
* Array of Example objects describing example URLs for this service.
@@ -144,7 +145,9 @@ class BaseService {
* @abstract
* @type {module:core/base-service/base~Example[]}
*/
static examples = []
static get examples() {
return []
}
static get _cacheLength() {
const cacheLengths = {
@@ -152,9 +155,6 @@ class BaseService {
license: 3600,
version: 300,
debug: 60,
downloads: 900,
rating: 900,
social: 900,
}
return cacheLengths[this.category]
}
@@ -166,7 +166,9 @@ class BaseService {
*
* @type {module:core/base-service/base~DefaultBadgeData}
*/
static defaultBadgeData = {}
static get defaultBadgeData() {
return {}
}
static render(props) {
throw new Error(`render() function not implemented for ${this.name}`)
@@ -214,10 +216,10 @@ class BaseService {
}
constructor(
{ requestFetcher, authHelper, metricHelper },
{ sendAndCacheRequest, authHelper, metricHelper },
{ handleInternalErrors }
) {
this._requestFetcher = requestFetcher
this._requestFetcher = sendAndCacheRequest
this.authHelper = authHelper
this._handleInternalErrors = handleInternalErrors
this._metricHelper = metricHelper
@@ -225,25 +227,16 @@ class BaseService {
async _request({ url, options = {}, errorMessages = {} }) {
const logTrace = (...args) => trace.logTrace('fetch', ...args)
let logUrl = url
const logOptions = Object.assign({}, options)
if ('searchParams' in options) {
const params = new URLSearchParams(options.searchParams)
logUrl = `${url}?${params.toString()}`
delete logOptions.searchParams
}
logTrace(
emojic.bowAndArrow,
'Request',
`${logUrl}\n${JSON.stringify(logOptions, null, 2)}`
)
logTrace(emojic.bowAndArrow, 'Request', url, '\n', options)
const { res, buffer } = await this._requestFetcher(url, options)
await this._meterResponse(res, buffer)
logTrace(emojic.dart, 'Response status code', res.statusCode)
return checkErrorResponse(errorMessages)({ buffer, res })
}
static enabledMetrics = []
static get enabledMetrics() {
return []
}
static isMetricEnabled(metricName) {
return this.enabledMetrics.includes(metricName)
@@ -285,7 +278,7 @@ class BaseService {
/**
* Asynchronous function to handle requests for this service. Take the route
* parameters (as defined in the `route` property), perform a request using
* `this._requestFetcher`, and return the badge data.
* `this._sendAndCacheRequest`, and return the badge data.
*
* @abstract
* @param {object} namedParams Params parsed from route pattern
@@ -429,90 +422,56 @@ class BaseService {
return serviceData
}
// `defaultCacheLengthSeconds` can be overridden by
// `serviceDefaultCacheLengthSeconds` (either by category or on a badge-
// by-badge basis). Then in turn that can be overridden by
// `serviceOverrideCacheLengthSeconds` (which we expect to be used only in
// the dynamic badge) but only if `serviceOverrideCacheLengthSeconds` is
// longer than `serviceDefaultCacheLengthSeconds` and then the `cacheSeconds`
// query param can also override both of those but again only if `cacheSeconds`
// is longer.
//
// Ref: https://github.com/badges/shields/pull/2755
static _applyCacheHeaders({
cacheHeaderConfig,
req,
res,
serviceOverrideCacheLengthSeconds,
}) {
const cacheLengthSeconds = coalesceCacheLength({
cacheHeaderConfig,
serviceDefaultCacheLengthSeconds: this._cacheLength,
serviceOverrideCacheLengthSeconds,
queryParams: req.query,
})
setHeadersForCacheLength(res, cacheLengthSeconds)
}
static makeExpressHandler(
{ githubApiProvider, librariesIoApiProvider, metricInstance },
static register(
{ camp, handleRequest, githubApiProvider, metricInstance },
serviceConfig
) {
const { cacheHeaders: cacheHeaderConfig, fetchLimit } = serviceConfig
const fetchLimitBytes = bytes.parse(fetchLimit)
const { regex, captureNames } = prepareRoute(this.route)
const queryParams = getQueryParamNames(this.route)
const metricHelper = MetricHelper.create({
metricInstance,
ServiceClass: this,
})
const { captureNames } = prepareRoute(this.route)
const { cacheHeaders: cacheHeaderConfig } = serviceConfig
return async (req, res) => {
const metricHandle = metricHelper.startRequest()
camp.route(
regex,
handleRequest(cacheHeaderConfig, {
queryParams,
handler: async (queryParams, match, sendBadge, request) => {
const metricHandle = metricHelper.startRequest()
const { namedParams, format } = paramsForReq(captureNames, req, this)
const serviceData = await this.invoke(
{
requestFetcher: fetch,
githubApiProvider,
librariesIoApiProvider,
metricHelper,
const namedParams = namedParamsForMatch(captureNames, match, this)
const serviceData = await this.invoke(
{
sendAndCacheRequest: request.asPromise,
sendAndCacheRequestWithCallbacks: request,
githubApiProvider,
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()
},
serviceConfig,
namedParams,
req.query
)
const badgeData = coalesceBadge(
req.query,
serviceData,
this.defaultBadgeData,
this
)
this._applyCacheHeaders({
cacheHeaderConfig,
req,
res,
serviceOverrideCacheLengthSeconds: badgeData.cacheLengthSeconds,
cacheLength: this._cacheLength,
fetchLimitBytes,
})
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))
)
}
}
@@ -610,4 +569,4 @@ class BaseService {
* An HTML string that is included in the badge popup.
*/
export default BaseService
module.exports = BaseService

View File

@@ -1,25 +1,23 @@
import Joi from 'joi'
import chai from 'chai'
import isSvg from 'is-svg'
import sinon from 'sinon'
import prometheus from 'prom-client'
import chaiAsPromised from 'chai-as-promised'
import PrometheusMetrics from '../server/prometheus-metrics.js'
import { ExpressTestHarness } from '../express-test-harness.js'
import trace from './trace.js'
import {
'use strict'
const Joi = require('@hapi/joi')
const chai = require('chai')
const { expect } = chai
const sinon = require('sinon')
const prometheus = require('prom-client')
const PrometheusMetrics = require('../server/prometheus-metrics')
const trace = require('./trace')
const {
NotFound,
Inaccessible,
InvalidResponse,
InvalidParameter,
Deprecated,
} from './errors.js'
import BaseService from './base.js'
import { MetricHelper, MetricNames } from './metric-helper.js'
import '../register-chai-plugins.spec.js'
const { expect } = chai
chai.use(chaiAsPromised)
} = require('./errors')
const BaseService = require('./base')
const { MetricHelper, MetricNames } = require('./metric-helper')
require('../register-chai-plugins.spec')
chai.use(require('chai-as-promised'))
const queryParamSchema = Joi.object({
queryParamA: Joi.string(),
@@ -31,19 +29,32 @@ const queryParamSchema = Joi.object({
.required()
class DummyService extends BaseService {
static category = 'other'
static route = { base: 'foo', pattern: ':namedParamA', queryParamSchema }
static get category() {
return 'other'
}
static examples = [
{
pattern: ':world',
namedParams: { world: 'World' },
staticPreview: this.render({ namedParamA: 'foo', queryParamA: 'bar' }),
keywords: ['hello'],
},
]
static get route() {
return {
base: 'foo',
pattern: ':namedParamA',
queryParamSchema,
}
}
static defaultBadgeData = { label: 'cat', namedLogo: 'appveyor' }
static get examples() {
return [
{
pattern: ':world',
namedParams: { world: 'World' },
staticPreview: this.render({ namedParamA: 'foo', queryParamA: 'bar' }),
keywords: ['hello'],
},
]
}
static get defaultBadgeData() {
return { label: 'cat', namedLogo: 'appveyor' }
}
static render({ namedParamA, queryParamA }) {
return {
@@ -57,22 +68,21 @@ class DummyService extends BaseService {
}
class DummyServiceWithServiceResponseSizeMetricEnabled extends DummyService {
static enabledMetrics = [MetricNames.SERVICE_RESPONSE_SIZE]
static get enabledMetrics() {
return [MetricNames.SERVICE_RESPONSE_SIZE]
}
}
describe('BaseService', function () {
describe('BaseService', function() {
const defaultConfig = {
handleInternalErrors: false,
cacheHeaders: { defaultCacheLengthSeconds: 120 },
public: {
handleInternalErrors: false,
services: {},
cacheHeaders: { defaultCacheLengthSeconds: 120 },
},
private: {},
}
it('Invokes the handler as expected', async function () {
it('Invokes the handler as expected', async function() {
expect(
await DummyService.invoke(
{},
@@ -85,7 +95,7 @@ describe('BaseService', function () {
})
})
it('Validates query params', async function () {
it('Validates query params', async function() {
expect(
await DummyService.invoke(
{},
@@ -100,43 +110,49 @@ describe('BaseService', function () {
})
})
describe('Required overrides', function () {
it('Should throw if render() is not overridden', function () {
describe('Required overrides', function() {
it('Should throw if render() is not overridden', function() {
expect(() => BaseService.render()).to.throw(
/^render\(\) function not implemented for BaseService$/
)
})
it('Should throw if route is not overridden', function () {
it('Should throw if route is not overridden', function() {
return expect(BaseService.invoke({}, {}, {})).to.be.rejectedWith(
/^Route not defined for BaseService$/
)
})
class WithRoute extends BaseService {
static route = {}
static get route() {
return {}
}
}
it('Should throw if handle() is not overridden', function () {
it('Should throw if handle() is not overridden', function() {
return expect(WithRoute.invoke({}, {}, {})).to.be.rejectedWith(
/^Handler not implemented for WithRoute$/
)
})
it('Should throw if category is not overridden', function () {
it('Should throw if category is not overridden', function() {
expect(() => BaseService.category).to.throw(
/^Category not set for BaseService$/
)
})
})
describe('Logging', function () {
beforeEach(function () {
sinon.stub(trace, 'logTrace')
describe('Logging', function() {
let sandbox
beforeEach(function() {
sandbox = sinon.createSandbox()
})
afterEach(function () {
sinon.restore()
afterEach(function() {
sandbox.restore()
})
it('Invokes the logger as expected', async function () {
beforeEach(function() {
sandbox.stub(trace, 'logTrace')
})
it('Invokes the logger as expected', async function() {
await DummyService.invoke(
{},
defaultConfig,
@@ -164,8 +180,8 @@ describe('BaseService', function () {
})
})
describe('Service data validation', function () {
it('Allows a link array', async function () {
describe('Service data validation', function() {
it('Allows a link array', async function() {
const message = 'hello'
const link = ['https://example.com/', 'https://other.example.com/']
class LinkService extends DummyService {
@@ -186,7 +202,7 @@ describe('BaseService', function () {
})
})
context('On invalid data', function () {
context('On invalid data', function() {
class ThrowingService extends DummyService {
async handle() {
return {
@@ -195,7 +211,7 @@ describe('BaseService', function () {
}
}
it('Throws a validation error on invalid data', async function () {
it('Throws a validation error on invalid data', async function() {
try {
await ThrowingService.invoke(
{},
@@ -213,7 +229,7 @@ describe('BaseService', function () {
// Ensure debuggabillity.
// https://github.com/badges/shields/issues/3784
it('Includes the service class in the stack trace', async function () {
it('Includes the service class in the stack trace', async function() {
try {
await ThrowingService.invoke(
{},
@@ -228,8 +244,8 @@ describe('BaseService', function () {
})
})
describe('Error handling', function () {
it('Handles internal errors', async function () {
describe('Error handling', function() {
it('Handles internal errors', async function() {
class ThrowingService extends DummyService {
async handle() {
throw Error("I've made a huge mistake")
@@ -249,8 +265,8 @@ describe('BaseService', function () {
})
})
describe('Handles known subtypes of ShieldsInternalError', function () {
it('handles NotFound errors', async function () {
describe('Handles known subtypes of ShieldsInternalError', function() {
it('handles NotFound errors', async function() {
class ThrowingService extends DummyService {
async handle() {
throw new NotFound()
@@ -265,7 +281,7 @@ describe('BaseService', function () {
})
})
it('handles Inaccessible errors', async function () {
it('handles Inaccessible errors', async function() {
class ThrowingService extends DummyService {
async handle() {
throw new Inaccessible()
@@ -280,7 +296,7 @@ describe('BaseService', function () {
})
})
it('handles InvalidResponse errors', async function () {
it('handles InvalidResponse errors', async function() {
class ThrowingService extends DummyService {
async handle() {
throw new InvalidResponse()
@@ -295,7 +311,7 @@ describe('BaseService', function () {
})
})
it('handles Deprecated', async function () {
it('handles Deprecated', async function() {
class ThrowingService extends DummyService {
async handle() {
throw new Deprecated()
@@ -310,7 +326,7 @@ describe('BaseService', function () {
})
})
it('handles InvalidParameter errors', async function () {
it('handles InvalidParameter errors', async function() {
class ThrowingService extends DummyService {
async handle() {
throw new InvalidParameter()
@@ -327,53 +343,76 @@ describe('BaseService', function () {
})
})
describe('Express integration', function () {
let harness
beforeEach(async function () {
harness = new ExpressTestHarness()
DummyService.register({ app: harness.app }, defaultConfig)
await harness.start()
})
describe('ScoutCamp integration', function() {
// TODO Strangly, without the useless escape the regexes do not match in Node 12.
// eslint-disable-next-line no-useless-escape
const expectedRouteRegex = /^\/foo\/([^\/]+?)(|\.svg|\.json)$/
afterEach(async function () {
await harness.stop()
})
let mockCamp
let mockHandleRequest
it('fulfills the request for an SVG badge', async function () {
const { headers, body } = await harness.get(
'/foo/bar.svg?queryParamA=%3F'
beforeEach(function() {
mockCamp = {
route: sinon.spy(),
}
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('fulfills the request for a JSON badge', async function () {
const { headers, body } = await harness.get(
'/foo/bar.json?queryParamA=%3F',
{ responseType: 'json' }
)
it('registers the service', function() {
expect(mockCamp.route).to.have.been.calledOnce
expect(mockCamp.route).to.have.been.calledWith(expectedRouteRegex)
})
expect(headers).to.include({
'content-type': 'application/json; charset=utf-8',
})
it('handles the request', async function() {
expect(mockHandleRequest).to.have.been.calledOnce
expect(body).to.include({
label: 'cat',
message: 'Hello namedParamA: bar with queryParamA: ?',
const {
queryParams: serviceQueryParams,
handler: requestHandler,
} = mockHandleRequest.getCall(0).args[1]
expect(serviceQueryParams).to.deep.equal([
'queryParamA',
'legacyQueryParamA',
])
const mockSendBadge = sinon.spy()
const mockRequest = {
asPromise: sinon.spy(),
}
const queryParams = { queryParamA: '?' }
const match = '/foo/bar.svg'.match(expectedRouteRegex)
await requestHandler(queryParams, match, mockSendBadge, mockRequest)
const expectedFormat = 'svg'
expect(mockSendBadge).to.have.been.calledOnce
expect(mockSendBadge).to.have.been.calledWith(expectedFormat, {
text: ['cat', 'Hello namedParamA: bar with queryParamA: ?'],
color: 'lightgrey',
template: undefined,
namedLogo: undefined,
logo: undefined,
logoWidth: undefined,
logoPosition: undefined,
links: [],
labelColor: undefined,
cacheLengthSeconds: undefined,
})
})
})
describe('getDefinition', function () {
it('returns the expected result', function () {
const { category, name, isDeprecated, route, examples } =
DummyService.getDefinition()
describe('getDefinition', function() {
it('returns the expected result', function() {
const {
category,
name,
isDeprecated,
route,
examples,
} = DummyService.getDefinition()
expect({
category,
name,
@@ -393,12 +432,12 @@ describe('BaseService', function () {
})
})
describe('validate', function () {
describe('validate', function() {
const dummySchema = Joi.object({
requiredString: Joi.string().required(),
}).required()
it('throws error for invalid responses', function () {
it('throws error for invalid responses', function() {
expect(() =>
DummyService._validate(
{ requiredString: ['this', "shouldn't", 'work'] },
@@ -410,21 +449,25 @@ describe('BaseService', function () {
})
})
describe('request', function () {
beforeEach(function () {
sinon.stub(trace, 'logTrace')
describe('request', function() {
let sandbox
beforeEach(function() {
sandbox = sinon.createSandbox()
})
afterEach(function () {
sinon.restore()
afterEach(function() {
sandbox.restore()
})
beforeEach(function() {
sandbox.stub(trace, 'logTrace')
})
it('logs appropriate information', async function () {
const requestFetcher = async () => ({
it('logs appropriate information', async function() {
const sendAndCacheRequest = async () => ({
buffer: '',
res: { statusCode: 200 },
})
const serviceInstance = new DummyService(
{ requestFetcher },
{ sendAndCacheRequest },
defaultConfig
)
@@ -436,7 +479,9 @@ describe('BaseService', function () {
'fetch',
sinon.match.string,
'Request',
`${url}\n${JSON.stringify(options, null, 2)}`
url,
'\n',
options
)
expect(trace.logTrace).to.be.calledWithMatch(
'fetch',
@@ -446,13 +491,13 @@ describe('BaseService', function () {
)
})
it('handles errors', async function () {
const requestFetcher = async () => ({
it('handles errors', async function() {
const sendAndCacheRequest = async () => ({
buffer: '',
res: { statusCode: 404 },
})
const serviceInstance = new DummyService(
{ requestFetcher },
{ sendAndCacheRequest },
defaultConfig
)
@@ -467,31 +512,30 @@ describe('BaseService', function () {
})
})
describe('Metrics', function () {
describe('Metrics', function() {
let register
beforeEach(function () {
beforeEach(function() {
register = new prometheus.Registry()
})
const url = 'some-url'
it('service response size metric is optional', async function () {
it('service response size metric is optional', async function() {
const metricHelper = MetricHelper.create({
metricInstance: new PrometheusMetrics({ register }),
ServiceClass: DummyServiceWithServiceResponseSizeMetricEnabled,
})
const requestFetcher = async () => ({
const sendAndCacheRequest = async () => ({
buffer: 'x'.repeat(65536 + 1),
res: { statusCode: 200 },
})
const serviceInstance =
new DummyServiceWithServiceResponseSizeMetricEnabled(
{ requestFetcher, metricHelper },
defaultConfig
)
const serviceInstance = new DummyServiceWithServiceResponseSizeMetricEnabled(
{ sendAndCacheRequest, metricHelper },
defaultConfig
)
await serviceInstance._request({ url })
expect(await register.getSingleMetricAsString('service_response_bytes'))
expect(register.getSingleMetricAsString('service_response_bytes'))
.to.contain(
'service_response_bytes_bucket{le="65536",category="other",family="undefined",service="dummy_service_with_service_response_size_metric_enabled"} 0\n'
)
@@ -500,33 +544,35 @@ describe('BaseService', function () {
)
})
it('service response size metric is disabled by default', async function () {
it('service response size metric is disabled by default', async function() {
const metricHelper = MetricHelper.create({
metricInstance: new PrometheusMetrics({ register }),
ServiceClass: DummyService,
})
const requestFetcher = async () => ({
const sendAndCacheRequest = async () => ({
buffer: 'x',
res: { statusCode: 200 },
})
const serviceInstance = new DummyService(
{ requestFetcher, metricHelper },
{ sendAndCacheRequest, metricHelper },
defaultConfig
)
await serviceInstance._request({ url })
expect(
await register.getSingleMetricAsString('service_response_bytes')
register.getSingleMetricAsString('service_response_bytes')
).to.not.contain('service_response_bytes_bucket')
})
})
describe('auth', function () {
describe('auth', function() {
class AuthService extends DummyService {
static auth = {
passKey: 'myci_pass',
serviceKey: 'myci',
isRequired: true,
static get auth() {
return {
passKey: 'myci_pass',
serviceKey: 'myci',
isRequired: true,
}
}
async handle() {
@@ -536,7 +582,7 @@ describe('BaseService', function () {
}
}
it('when auth is configured properly, invoke() sets authHelper', async function () {
it('when auth is configured properly, invoke() sets authHelper', async function() {
expect(
await AuthService.invoke(
{},
@@ -552,7 +598,7 @@ describe('BaseService', function () {
).to.deep.equal({ message: 'The CI password is abc123' })
})
it('when auth is not configured properly, invoke() returns inacessible', async function () {
it('when auth is not configured properly, invoke() returns inacessible', async function() {
expect(
await AuthService.invoke(
{},
@@ -563,7 +609,9 @@ describe('BaseService', function () {
},
private: {},
},
{ namedParamA: 'bar.bar.bar' }
{
namedParamA: 'bar.bar.bar',
}
)
).to.deep.equal({
color: 'lightgray',

View File

@@ -1,11 +1,15 @@
import assert from 'assert'
import Joi from 'joi'
import coalesce from './coalesce.js'
'use strict'
const assert = require('assert')
const Joi = require('@hapi/joi')
const coalesce = require('./coalesce')
const serverStartTimeGMTString = new Date().toGMTString()
const serverStartTimestamp = Date.now()
const isOptionalNonNegativeInteger = Joi.number().integer().min(0)
const isOptionalNonNegativeInteger = Joi.number()
.integer()
.min(0)
const queryParamSchema = Joi.object({
cacheSeconds: isOptionalNonNegativeInteger,
@@ -65,7 +69,7 @@ function setHeadersForCacheLength(res, cacheLengthSeconds) {
cacheControl = 'no-cache, no-store, must-revalidate'
expires = nowGMTString
} else {
cacheControl = `max-age=${cacheLengthSeconds}, s-maxage=${cacheLengthSeconds}`
cacheControl = `max-age=${cacheLengthSeconds}`
expires = new Date(now.getTime() + cacheLengthSeconds * 1000).toGMTString()
}
@@ -90,7 +94,7 @@ function setCacheHeaders({
setHeadersForCacheLength(res, cacheLengthSeconds)
}
const staticCacheControlHeader = `max-age=${24 * 3600}, s-maxage=${24 * 3600}` // 1 day.
const staticCacheControlHeader = `max-age=${24 * 3600}` // 1 day.
function setCacheHeadersForStaticResource(res) {
res.setHeader('Cache-Control', staticCacheControlHeader)
res.setHeader('Last-Modified', serverStartTimeGMTString)
@@ -102,7 +106,7 @@ function serverHasBeenUpSinceResourceCached(req) {
)
}
export {
module.exports = {
coalesceCacheLength,
setCacheHeaders,
setHeadersForCacheLength,

View File

@@ -1,24 +1,27 @@
import { test, given } from 'sazerac'
import chai, { expect } from 'chai'
import sinon from 'sinon'
import httpMocks from 'node-mocks-http'
import chaiDatetime from 'chai-datetime'
import {
'use strict'
const { test, given } = require('sazerac')
const chai = require('chai')
const { expect } = require('chai')
const sinon = require('sinon')
const httpMocks = require('node-mocks-http')
const {
coalesceCacheLength,
setHeadersForCacheLength,
setCacheHeaders,
setCacheHeadersForStaticResource,
serverHasBeenUpSinceResourceCached,
} from './cache-headers.js'
chai.use(chaiDatetime)
} = require('./cache-headers')
describe('Cache header functions', function () {
chai.use(require('chai-datetime'))
describe('Cache header functions', function() {
let res
beforeEach(function () {
beforeEach(function() {
res = httpMocks.createResponse()
})
describe('coalesceCacheLength', function () {
describe('coalesceCacheLength', function() {
const cacheHeaderConfig = { defaultCacheLengthSeconds: 777 }
test(coalesceCacheLength, () => {
given({ cacheHeaderConfig, queryParams: {} }).expect(777)
@@ -98,15 +101,18 @@ describe('Cache header functions', function () {
})
})
describe('setHeadersForCacheLength', function () {
beforeEach(function () {
sinon.useFakeTimers()
describe('setHeadersForCacheLength', function() {
let sandbox
beforeEach(function() {
sandbox = sinon.createSandbox()
sandbox.useFakeTimers()
})
afterEach(function () {
sinon.restore()
afterEach(function() {
sandbox.restore()
sandbox = undefined
})
it('should set the correct Date header', function () {
it('should set the correct Date header', function() {
// Confidence check.
expect(res._headers.date).to.equal(undefined)
@@ -118,42 +124,40 @@ describe('Cache header functions', function () {
expect(res._headers.date).to.equal(now)
})
context('cacheLengthSeconds is zero', function () {
beforeEach(function () {
context('cacheLengthSeconds is zero', function() {
beforeEach(function() {
setHeadersForCacheLength(res, 0)
})
it('should set the expected Cache-Control header', function () {
it('should set the expected Cache-Control header', function() {
expect(res._headers['cache-control']).to.equal(
'no-cache, no-store, must-revalidate'
)
})
it('should set the expected Expires header', function () {
it('should set the expected Expires header', function() {
expect(res._headers.expires).to.equal(new Date().toGMTString())
})
})
context('cacheLengthSeconds is nonzero', function () {
beforeEach(function () {
context('cacheLengthSeconds is nonzero', function() {
beforeEach(function() {
setHeadersForCacheLength(res, 123)
})
it('should set the expected Cache-Control header', function () {
expect(res._headers['cache-control']).to.equal(
'max-age=123, s-maxage=123'
)
it('should set the expected Cache-Control header', function() {
expect(res._headers['cache-control']).to.equal('max-age=123')
})
it('should set the expected Expires header', function () {
it('should set the expected Expires header', function() {
const expires = new Date(Date.now() + 123 * 1000).toGMTString()
expect(res._headers.expires).to.equal(expires)
})
})
})
describe('setCacheHeaders', function () {
it('sets the expected fields', function () {
describe('setCacheHeaders', function() {
it('sets the expected fields', function() {
const expectedFields = ['date', 'cache-control', 'expires']
expectedFields.forEach(field =>
expect(res._headers[field]).to.equal(undefined)
@@ -174,18 +178,16 @@ describe('Cache header functions', function () {
})
})
describe('setCacheHeadersForStaticResource', function () {
beforeEach(function () {
describe('setCacheHeadersForStaticResource', function() {
beforeEach(function() {
setCacheHeadersForStaticResource(res)
})
it('should set the expected Cache-Control header', function () {
expect(res._headers['cache-control']).to.equal(
`max-age=${24 * 3600}, s-maxage=${24 * 3600}`
)
it('should set the expected Cache-Control header', function() {
expect(res._headers['cache-control']).to.equal(`max-age=${24 * 3600}`)
})
it('should set the expected Last-Modified header', function () {
it('should set the expected Last-Modified header', function() {
const lastModified = res._headers['last-modified']
expect(new Date(lastModified)).to.be.withinTime(
// Within the last 60 seconds.
@@ -195,17 +197,17 @@ describe('Cache header functions', function () {
})
})
describe('serverHasBeenUpSinceResourceCached', function () {
describe('serverHasBeenUpSinceResourceCached', function() {
// The stringified req's are hard to understand. I thought Sazerac
// provided a way to override the describe message, though I can't find it.
context('when there is no If-Modified-Since header', function () {
it('returns false', function () {
context('when there is no If-Modified-Since header', function() {
it('returns false', function() {
const req = httpMocks.createRequest()
expect(serverHasBeenUpSinceResourceCached(req)).to.equal(false)
})
})
context('when the If-Modified-Since header is invalid', function () {
it('returns false', function () {
context('when the If-Modified-Since header is invalid', function() {
it('returns false', function() {
const req = httpMocks.createRequest({
headers: { 'If-Modified-Since': 'this-is-not-a-date' },
})
@@ -214,8 +216,8 @@ describe('Cache header functions', function () {
})
context(
'when the If-Modified-Since header is before the process started',
function () {
it('returns false', function () {
function() {
it('returns false', function() {
const req = httpMocks.createRequest({
headers: { 'If-Modified-Since': '2018-02-01T05:00:00.000Z' },
})
@@ -225,8 +227,8 @@ describe('Cache header functions', function () {
)
context(
'when the If-Modified-Since header is after the process started',
function () {
it('returns true', function () {
function() {
it('returns true', function() {
const modifiedTimeStamp = new Date(Date.now() + 1800000)
const req = httpMocks.createRequest({
headers: { 'If-Modified-Since': modifiedTimeStamp.toISOString() },

View File

@@ -1,5 +1,7 @@
import Joi from 'joi'
import categories from '../../services/categories.js'
'use strict'
const Joi = require('@hapi/joi')
const categories = require('../../services/categories')
const isRealCategory = Joi.equal(...categories.map(({ id }) => id)).required()
@@ -11,4 +13,7 @@ function assertValidCategory(category, message = undefined) {
Joi.assert(category, isValidCategory, message)
}
export { isValidCategory, assertValidCategory }
module.exports = {
isValidCategory,
assertValidCategory,
}

View File

@@ -1,11 +1,13 @@
import { NotFound, InvalidResponse, Inaccessible } from './errors.js'
'use strict'
const { NotFound, InvalidResponse, Inaccessible } = require('./errors')
const defaultErrorMessages = {
404: 'not found',
}
export default function checkErrorResponse(errorMessages = {}) {
return async function ({ buffer, res }) {
module.exports = function checkErrorResponse(errorMessages = {}) {
return async function({ buffer, res }) {
let error
errorMessages = { ...defaultErrorMessages, ...errorMessages }
if (res.statusCode === 404) {

View File

@@ -1,12 +1,14 @@
import { expect } from 'chai'
import { NotFound, InvalidResponse, Inaccessible } from './errors.js'
import checkErrorResponse from './check-error-response.js'
'use strict'
describe('async error handler', function () {
const { expect } = require('chai')
const { NotFound, InvalidResponse, Inaccessible } = require('./errors')
const checkErrorResponse = require('./check-error-response')
describe('async error handler', function() {
const buffer = Buffer.from('some stuff')
context('when status is 200', function () {
it('passes through the inputs', async function () {
context('when status is 200', function() {
it('passes through the inputs', async function() {
const res = { statusCode: 200 }
expect(await checkErrorResponse()({ res, buffer })).to.deep.equal({
res,
@@ -15,11 +17,11 @@ describe('async error handler', function () {
})
})
context('when status is 404', function () {
context('when status is 404', function() {
const buffer = Buffer.from('some stuff')
const res = { statusCode: 404 }
it('throws NotFound', async function () {
it('throws NotFound', async function() {
try {
await checkErrorResponse()({ res, buffer })
expect.fail('Expected to throw')
@@ -32,7 +34,7 @@ describe('async error handler', function () {
}
})
it('displays the custom not found message', async function () {
it('displays the custom not found message', async function() {
const notFoundMessage = 'no goblins found'
try {
await checkErrorResponse({ 404: notFoundMessage })({ res, buffer })
@@ -45,8 +47,8 @@ describe('async error handler', function () {
})
})
context('when status is 4xx', function () {
it('throws InvalidResponse', async function () {
context('when status is 4xx', function() {
it('throws InvalidResponse', async function() {
const res = { statusCode: 499 }
try {
await checkErrorResponse()({ res, buffer })
@@ -62,7 +64,7 @@ describe('async error handler', function () {
}
})
it('displays the custom error message', async function () {
it('displays the custom error message', async function() {
const res = { statusCode: 403 }
try {
await checkErrorResponse({ 403: 'access denied' })({ res })
@@ -77,8 +79,8 @@ describe('async error handler', function () {
})
})
context('when status is 5xx', function () {
it('throws Inaccessible', async function () {
context('when status is 5xx', function() {
it('throws Inaccessible', async function() {
const res = { statusCode: 503 }
try {
await checkErrorResponse()({ res, buffer })
@@ -94,7 +96,7 @@ describe('async error handler', function () {
}
})
it('displays the custom error message', async function () {
it('displays the custom error message', async function() {
const res = { statusCode: 500 }
try {
await checkErrorResponse({ 500: 'server overloaded' })({ res, buffer })

View File

@@ -1,10 +1,12 @@
import {
'use strict'
const {
decodeDataUrlFromQueryParam,
prepareNamedLogo,
} from '../../lib/logos.js'
import { svg2base64 } from '../../lib/svg-helpers.js'
import coalesce from './coalesce.js'
import toArray from './to-array.js'
} = require('../../lib/logos')
const { svg2base64 } = require('../../lib/svg-helpers')
const coalesce = require('./coalesce')
const toArray = require('./to-array')
// Translate modern badge data to the legacy schema understood by the badge
// maker. Allow the user to override the label, color, logo, etc. through the
@@ -32,7 +34,7 @@ import toArray from './to-array.js'
// 3. In the case of the `social` style only, the last precedence is the
// service's default logo. The `logoColor` can be overridden by the query
// string.
export default function coalesceBadge(
module.exports = function coalesceBadge(
overrides,
serviceData,
// These two parameters were kept separate to make tests clearer.
@@ -102,23 +104,7 @@ export default function coalesceBadge(
labelColor: defaultLabelColor,
} = defaultBadgeData
let style = coalesce(overrideStyle, serviceStyle)
if (typeof style !== 'string') {
style = 'flat'
}
if (style.startsWith('popout')) {
style = style.replace('popout', 'flat')
}
const styleValues = [
'plastic',
'flat',
'flat-square',
'for-the-badge',
'social',
]
if (!styleValues.includes(style)) {
style = 'flat'
}
const style = coalesce(overrideStyle, serviceStyle)
let namedLogo, namedLogoColor, logoWidth, logoPosition, logoSvgBase64
if (overrideLogo) {
@@ -158,10 +144,12 @@ export default function coalesceBadge(
}
return {
// Use `coalesce()` to support empty labels and messages, as in the static
// badge.
label: coalesce(overrideLabel, serviceLabel, defaultLabel, category),
message: coalesce(serviceMessage, 'n/a'),
text: [
// Use `coalesce()` to support empty labels and messages, as in the
// static badge.
coalesce(overrideLabel, serviceLabel, defaultLabel, category),
coalesce(serviceMessage, 'n/a'),
],
color: coalesce(
// In case of an error, disregard user's color override.
isError ? undefined : overrideColor,
@@ -175,7 +163,7 @@ export default function coalesceBadge(
serviceLabelColor,
defaultLabelColor
),
style,
template: style,
namedLogo,
logo: logoSvgBase64,
logoWidth,

View File

@@ -1,124 +1,127 @@
import { expect } from 'chai'
import { getShieldsIcon, getSimpleIcon } from '../../lib/logos.js'
import coalesceBadge from './coalesce-badge.js'
'use strict'
describe('coalesceBadge', function () {
describe('Label', function () {
it('uses the default label', function () {
expect(coalesceBadge({}, {}, { label: 'heyo' })).to.include({
label: 'heyo',
})
const { expect } = require('chai')
const { getShieldsIcon, getSimpleIcon } = require('../../lib/logos')
const coalesceBadge = require('./coalesce-badge')
describe('coalesceBadge', function() {
describe('Label', function() {
it('uses the default label', function() {
expect(coalesceBadge({}, {}, { label: 'heyo' }).text).to.deep.equal([
'heyo',
'n/a',
])
})
// This behavior isn't great and we might want to remove it.
it('uses the category as a default label', function () {
expect(coalesceBadge({}, {}, {}, { category: 'cat' })).to.include({
label: 'cat',
})
})
it('preserves an empty label', function () {
expect(coalesceBadge({}, { label: '', message: '10k' }, {})).to.include({
label: '',
})
})
it('overrides the label', function () {
it('uses the category as a default label', function() {
expect(
coalesceBadge({ label: 'purr count' }, { label: 'purrs' }, {})
).to.include({ label: 'purr count' })
coalesceBadge({}, {}, {}, { category: 'cat' }).text
).to.deep.equal(['cat', 'n/a'])
})
it('preserves an empty label', function() {
expect(
coalesceBadge({}, { label: '', message: '10k' }, {}).text
).to.deep.equal(['', '10k'])
})
it('overrides the label', function() {
expect(
coalesceBadge({ label: 'purr count' }, { label: 'purrs' }, {}).text
).to.deep.equal(['purr count', 'n/a'])
})
})
describe('Message', function () {
it('applies the service message', function () {
expect(coalesceBadge({}, { message: '10k' }, {})).to.include({
message: '10k',
})
describe('Message', function() {
it('applies the service message', function() {
expect(coalesceBadge({}, { message: '10k' }, {}).text).to.deep.equal([
undefined,
'10k',
])
})
// https://github.com/badges/shields/issues/1280
it('converts a number to a string', function () {
it('applies a numeric service message', function() {
// While a number of badges use this, in the long run we may want
// `render()` to always return a string.
expect(coalesceBadge({}, { message: 10 }, {})).to.include({
message: 10,
})
expect(coalesceBadge({}, { message: 10 }, {}).text).to.deep.equal([
undefined,
10,
])
})
})
describe('Right color', function () {
it('uses the default color', function () {
expect(coalesceBadge({}, {}, {})).to.include({ color: 'lightgrey' })
describe('Right color', function() {
it('uses the default color', function() {
expect(coalesceBadge({}, {}, {}).color).to.equal('lightgrey')
})
it('overrides the color', function () {
it('overrides the color', function() {
expect(
coalesceBadge({ color: '10ADED' }, { color: 'red' }, {})
).to.include({ color: '10ADED' })
coalesceBadge({ color: '10ADED' }, { color: 'red' }, {}).color
).to.equal('10ADED')
// also expected for legacy name
expect(
coalesceBadge({ colorB: 'B0ADED' }, { color: 'red' }, {})
).to.include({ color: 'B0ADED' })
coalesceBadge({ colorB: 'B0ADED' }, { color: 'red' }, {}).color
).to.equal('B0ADED')
})
context('In case of an error', function () {
it('does not override the color', function () {
context('In case of an error', function() {
it('does not override the color', function() {
expect(
coalesceBadge(
{ color: '10ADED' },
{ isError: true, color: 'lightgray' },
{}
)
).to.include({ color: 'lightgray' })
).color
).to.equal('lightgray')
// also expected for legacy name
expect(
coalesceBadge(
{ colorB: 'B0ADED' },
{ isError: true, color: 'lightgray' },
{}
)
).to.include({ color: 'lightgray' })
).color
).to.equal('lightgray')
})
})
it('applies the service color', function () {
expect(coalesceBadge({}, { color: 'red' }, {})).to.include({
color: 'red',
})
it('applies the service color', function() {
expect(coalesceBadge({}, { color: 'red' }, {}).color).to.equal('red')
})
})
describe('Left color', function () {
it('provides no default label color', function () {
describe('Left color', function() {
it('provides no default label color', function() {
expect(coalesceBadge({}, {}, {}).labelColor).to.be.undefined
})
it('applies the service label color', function () {
expect(coalesceBadge({}, { labelColor: 'red' }, {})).to.include({
labelColor: 'red',
})
it('applies the service label color', function() {
expect(coalesceBadge({}, { labelColor: 'red' }, {}).labelColor).to.equal(
'red'
)
})
it('overrides the label color', function () {
it('overrides the label color', function() {
expect(
coalesceBadge({ labelColor: '42f483' }, { color: 'green' }, {})
).to.include({ labelColor: '42f483' })
.labelColor
).to.equal('42f483')
// also expected for legacy name
expect(
coalesceBadge({ colorA: 'B2f483' }, { color: 'green' }, {})
).to.include({ labelColor: 'B2f483' })
coalesceBadge({ colorA: 'B2f483' }, { color: 'green' }, {}).labelColor
).to.equal('B2f483')
})
it('converts a query-string numeric color to a string', function () {
it('converts a query-string numeric color to a string', function() {
expect(
coalesceBadge(
// Scoutcamp converts numeric query params to numbers.
{ color: 123 },
{ color: 'green' },
{}
)
).to.include({ color: '123' })
).color
).to.equal('123')
// also expected for legacy name
expect(
coalesceBadge(
@@ -126,47 +129,47 @@ describe('coalesceBadge', function () {
{ colorB: 123 },
{ color: 'green' },
{}
)
).to.include({ color: '123' })
).color
).to.equal('123')
})
})
describe('Named logos', function () {
it('when not a social badge, ignores the default named logo', function () {
describe('Named logos', function() {
it('when not a social badge, ignores the default named logo', function() {
expect(coalesceBadge({}, {}, { namedLogo: 'appveyor' }).logo).to.be
.undefined
})
it('when a social badge, uses the default named logo', function () {
it('when a social badge, uses the default named logo', function() {
// .not.be.empty for confidence that nothing has changed with `getShieldsIcon()`.
expect(
coalesceBadge({ style: 'social' }, {}, { namedLogo: 'appveyor' }).logo
).to.equal(getSimpleIcon({ name: 'appveyor' })).and.not.be.empty
})
it('applies the named logo', function () {
expect(coalesceBadge({}, { namedLogo: 'npm' }, {})).to.include({
namedLogo: 'npm',
})
it('applies the named logo', function() {
expect(coalesceBadge({}, { namedLogo: 'npm' }, {}).namedLogo).to.equal(
'npm'
)
expect(coalesceBadge({}, { namedLogo: 'npm' }, {}).logo).to.equal(
getShieldsIcon({ name: 'npm' })
).and.not.to.be.empty
})
it('applies the named logo with color', function () {
it('applies the named logo with color', function() {
expect(
coalesceBadge({}, { namedLogo: 'npm', logoColor: 'blue' }, {}).logo
).to.equal(getShieldsIcon({ name: 'npm', color: 'blue' })).and.not.to.be
.empty
})
it('overrides the logo', function () {
it('overrides the logo', function() {
expect(
coalesceBadge({ logo: 'npm' }, { namedLogo: 'appveyor' }, {}).logo
).to.equal(getShieldsIcon({ name: 'npm' })).and.not.be.empty
})
it('overrides the logo with a color', function () {
it('overrides the logo with a color', function() {
expect(
coalesceBadge(
{ logo: 'npm', logoColor: 'blue' },
@@ -177,7 +180,7 @@ describe('coalesceBadge', function () {
.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() {
expect(
coalesceBadge(
{ logo: 'npm' },
@@ -192,7 +195,7 @@ describe('coalesceBadge', function () {
).to.equal(getShieldsIcon({ name: 'npm' })).and.not.be.empty
})
it("overrides the service logo's color", function () {
it("overrides the service logo's color", function() {
expect(
coalesceBadge(
{ logoColor: 'blue' },
@@ -204,7 +207,7 @@ describe('coalesceBadge', function () {
})
// https://github.com/badges/shields/issues/2998
it('overrides logoSvg', function () {
it('overrides logoSvg', function() {
const logoSvg = 'data:image/svg+xml;base64,PHN2ZyB4bWxu'
expect(coalesceBadge({ logo: 'npm' }, { logoSvg }, {}).logo).to.equal(
getShieldsIcon({ name: 'npm' })
@@ -212,61 +215,61 @@ describe('coalesceBadge', function () {
})
})
describe('Custom logos', function () {
it('overrides the logo with custom svg', function () {
describe('Custom logos', function() {
it('overrides the logo with custom svg', function() {
const logoSvg = 'data:image/svg+xml;base64,PHN2ZyB4bWxu'
expect(
coalesceBadge({ logo: logoSvg }, { namedLogo: 'appveyor' }, {})
).to.include({ logo: logoSvg })
coalesceBadge({ logo: logoSvg }, { namedLogo: 'appveyor' }, {}).logo
).to.equal(logoSvg)
})
it('ignores the color when custom svg is provided', function () {
it('ignores the color when custom svg is provided', function() {
const logoSvg = 'data:image/svg+xml;base64,PHN2ZyB4bWxu'
expect(
coalesceBadge(
{ logo: logoSvg, logoColor: 'brightgreen' },
{ namedLogo: 'appveyor' },
{}
)
).to.include({ logo: logoSvg })
).logo
).to.equal(logoSvg)
})
})
describe('Logo width', function () {
it('overrides the logoWidth', function () {
expect(coalesceBadge({ logoWidth: 20 }, {}, {})).to.include({
logoWidth: 20,
})
describe('Logo width', function() {
it('overrides the logoWidth', function() {
expect(coalesceBadge({ logoWidth: 20 }, {}, {}).logoWidth).to.equal(20)
})
it('applies the logo width', function () {
it('applies the logo width', function() {
expect(
coalesceBadge({}, { namedLogo: 'npm', logoWidth: 275 }, {})
).to.include({ logoWidth: 275 })
coalesceBadge({}, { namedLogo: 'npm', logoWidth: 275 }, {}).logoWidth
).to.equal(275)
})
})
describe('Logo position', function () {
it('overrides the logoPosition', function () {
expect(coalesceBadge({ logoPosition: -10 }, {}, {})).to.include({
logoPosition: -10,
})
describe('Logo position', function() {
it('overrides the logoPosition', function() {
expect(
coalesceBadge({ logoPosition: -10 }, {}, {}).logoPosition
).to.equal(-10)
})
it('applies the logo position', function () {
it('applies the logo position', function() {
expect(
coalesceBadge({}, { namedLogo: 'npm', logoPosition: -10 }, {})
).to.include({ logoPosition: -10 })
.logoPosition
).to.equal(-10)
})
})
describe('Links', function () {
it('overrides the links', function () {
describe('Links', function() {
it('overrides the links', function() {
expect(
coalesceBadge(
{ link: 'https://circleci.com/gh/badges/daily-tests' },
{
link: 'https://circleci.com/workflow-run/184ef3de-4836-4805-a2e4-0ceba099f92d',
link:
'https://circleci.com/workflow-run/184ef3de-4836-4805-a2e4-0ceba099f92d',
},
{}
).links
@@ -274,34 +277,18 @@ describe('coalesceBadge', function () {
})
})
describe('Style', function () {
it('falls back to flat with invalid style', function () {
expect(coalesceBadge({ style: 'pill' }, {}, {})).to.include({
style: 'flat',
})
expect(coalesceBadge({ style: 7 }, {}, {})).to.include({
style: 'flat',
})
expect(coalesceBadge({ style: undefined }, {}, {})).to.include({
style: 'flat',
})
})
it('replaces legacy popout styles', function () {
expect(coalesceBadge({ style: 'popout' }, {}, {})).to.include({
style: 'flat',
})
expect(coalesceBadge({ style: 'popout-square' }, {}, {})).to.include({
style: 'flat-square',
})
describe('Style', function() {
it('overrides the template', function() {
expect(coalesceBadge({ style: 'pill' }, {}, {}).template).to.equal('pill')
})
})
describe('Cache length', function () {
it('overrides the cache length', function () {
describe('Cache length', function() {
it('overrides the cache length', function() {
expect(
coalesceBadge({ style: 'pill' }, { cacheSeconds: 123 }, {})
).to.include({ cacheLengthSeconds: 123 })
.cacheLengthSeconds
).to.equal(123)
})
})
})

View File

@@ -1,3 +1,5 @@
export default function coalesce(...candidates) {
'use strict'
module.exports = function coalesce(...candidates) {
return candidates.find(c => c !== undefined && c !== null)
}

View File

@@ -1,12 +1,14 @@
import { test, given } from 'sazerac'
import coalesce from './coalesce.js'
'use strict'
const { test, given } = require('sazerac')
const coalesce = require('./coalesce')
// Sticking with our one-line spread implementation, and defaulting to
// `undefined` instead of `null`, though h/t to
// https://github.com/royriojas/coalescy for these tests!
describe('coalesce', function () {
test(coalesce, function () {
describe('coalesce', function() {
test(coalesce, function() {
given().expect(undefined)
given(null, []).expect([])
given(null, [], {}).expect([])

View File

@@ -1,9 +1,11 @@
import Joi from 'joi'
import camelcase from 'camelcase'
import BaseService from './base.js'
import { isValidCategory } from './categories.js'
import { Deprecated } from './errors.js'
import { isValidRoute } from './route.js'
'use strict'
const Joi = require('@hapi/joi')
const camelcase = require('camelcase')
const BaseService = require('./base')
const { isValidCategory } = require('./categories')
const { Deprecated } = require('./errors')
const { isValidRoute } = require('./route')
const attrSchema = Joi.object({
route: isValidRoute,
@@ -24,17 +26,33 @@ function deprecatedService(attrs) {
)
return class DeprecatedService extends BaseService {
static name = name
? `Deprecated${name}`
: `Deprecated${camelcase(route.base.replace(/\//g, '_'), {
pascalCase: true,
})}`
static get name() {
return name
? `Deprecated${name}`
: `Deprecated${camelcase(route.base.replace(/\//g, '_'), {
pascalCase: true,
})}`
}
static category = category
static isDeprecated = true
static route = route
static examples = examples
static defaultBadgeData = { label }
static get category() {
return category
}
static get isDeprecated() {
return true
}
static get route() {
return route
}
static get examples() {
return examples
}
static get defaultBadgeData() {
return { label }
}
async handle() {
throw new Deprecated({ prettyMessage: message })
@@ -42,4 +60,4 @@ function deprecatedService(attrs) {
}
}
export default deprecatedService
module.exports = deprecatedService

View File

@@ -1,7 +1,9 @@
import { expect } from 'chai'
import deprecatedService from './deprecated-service.js'
'use strict'
describe('DeprecatedService', function () {
const { expect } = require('chai')
const deprecatedService = require('./deprecated-service')
describe('DeprecatedService', function() {
const route = {
base: 'service/that/no/longer/exists',
format: '(?:.+)',
@@ -10,33 +12,33 @@ describe('DeprecatedService', function () {
const dateAdded = new Date()
const commonAttrs = { route, category, dateAdded }
it('returns true on isDeprecated', function () {
it('returns true on isDeprecated', function() {
const service = deprecatedService({ ...commonAttrs })
expect(service.isDeprecated).to.be.true
})
it('has the expected name', function () {
it('has the expected name', function() {
const service = deprecatedService({ ...commonAttrs })
expect(service.name).to.equal('DeprecatedServiceThatNoLongerExists')
})
it('sets specified route', function () {
it('sets specified route', function() {
const service = deprecatedService({ ...commonAttrs })
expect(service.route).to.deep.equal(route)
})
it('sets specified label', function () {
it('sets specified label', function() {
const label = 'coverity'
const service = deprecatedService({ ...commonAttrs, label })
expect(service.defaultBadgeData.label).to.equal(label)
})
it('sets specified category', function () {
it('sets specified category', function() {
const service = deprecatedService({ ...commonAttrs })
expect(service.category).to.equal(category)
})
it('sets specified examples', function () {
it('sets specified examples', function() {
const examples = [
{
title: 'Not sure we would have examples',
@@ -46,7 +48,7 @@ describe('DeprecatedService', function () {
expect(service.examples).to.deep.equal(examples)
})
it('uses default deprecation message when no message specified', async function () {
it('uses default deprecation message when no message specified', async function() {
const service = deprecatedService({ ...commonAttrs })
expect(await service.invoke()).to.deep.equal({
isError: true,
@@ -55,7 +57,7 @@ describe('DeprecatedService', function () {
})
})
it('uses custom deprecation message when specified', async function () {
it('uses custom deprecation message when specified', async function() {
const message = 'extended outage'
const service = deprecatedService({ ...commonAttrs, message })
expect(await service.invoke()).to.deep.equal({

View File

@@ -4,6 +4,8 @@
* @module
*/
'use strict'
/**
* Base error class
*
@@ -208,7 +210,7 @@ class Deprecated extends ShieldsRuntimeError {
* badge when we catch and render the exception (Optional)
*/
export {
module.exports = {
ShieldsRuntimeError,
NotFound,
ImproperlyConfigured,

View File

@@ -1,8 +1,10 @@
import Joi from 'joi'
import { pathToRegexp, compile } from 'path-to-regexp'
import categories from '../../services/categories.js'
import coalesceBadge from './coalesce-badge.js'
import { makeFullUrl } from './route.js'
'use strict'
const Joi = require('@hapi/joi')
const { pathToRegexp, compile } = require('path-to-regexp')
const categories = require('../../services/categories')
const coalesceBadge = require('./coalesce-badge')
const { makeFullUrl } = require('./route')
const optionalObjectOfKeyValues = Joi.object().pattern(
/./,
@@ -19,12 +21,19 @@ const schema = Joi.object({
staticPreview: Joi.object({
label: Joi.string(),
message: Joi.alternatives()
.try(Joi.string().allow('').required(), Joi.number())
.try(
Joi.string()
.allow('')
.required(),
Joi.number()
)
.required(),
color: Joi.string(),
style: Joi.string(),
}).required(),
keywords: Joi.array().items(Joi.string()).default([]),
keywords: Joi.array()
.items(Joi.string())
.default([]),
documentation: Joi.string(), // Valid HTML.
}).required()
@@ -122,7 +131,12 @@ function transformExample(inExample, index, ServiceClass) {
documentation,
} = validateExample(inExample, index, ServiceClass)
const { label, message, color, style, namedLogo } = coalesceBadge(
const {
text: [label, message],
color,
template: style,
namedLogo,
} = coalesceBadge(
{},
staticPreview,
ServiceClass.defaultBadgeData,
@@ -153,4 +167,7 @@ function transformExample(inExample, index, ServiceClass) {
}
}
export { validateExample, transformExample }
module.exports = {
validateExample,
transformExample,
}

View File

@@ -1,9 +1,11 @@
import { expect } from 'chai'
import { test, given } from 'sazerac'
import { validateExample, transformExample } from './examples.js'
'use strict'
describe('validateExample function', function () {
it('passes valid examples', function () {
const { expect } = require('chai')
const { test, given } = require('sazerac')
const { validateExample, transformExample } = require('./examples')
describe('validateExample function', function() {
it('passes valid examples', function() {
const validExamples = [
{
title: 'Package manager versioning badge',
@@ -21,7 +23,7 @@ describe('validateExample function', function () {
})
})
it('rejects invalid examples', function () {
it('rejects invalid examples', function() {
const invalidExamples = [
{},
{ staticPreview: { message: '123' } },
@@ -72,7 +74,7 @@ describe('validateExample function', function () {
})
})
test(transformExample, function () {
test(transformExample, function() {
const ExampleService = {
name: 'ExampleService',
route: {

View File

@@ -1,26 +0,0 @@
import bytes from 'bytes'
import configModule from 'config'
import Joi from 'joi'
import { fileSize } from '../../services/validators.js'
const schema = Joi.object({
fetchLimit: fileSize,
userAgentBase: Joi.string().required(),
}).required()
const config = configModule.util.toObject()
const publicConfig = Joi.attempt(config.public, schema, { allowUnknown: true })
const fetchLimitBytes = bytes(publicConfig.fetchLimit)
function getUserAgent(userAgentBase = publicConfig.userAgentBase) {
let version = 'dev'
if (process.env.DOCKER_SHIELDS_VERSION) {
version = process.env.DOCKER_SHIELDS_VERSION
}
if (process.env.HEROKU_SLUG_COMMIT) {
version = process.env.HEROKU_SLUG_COMMIT.substring(0, 7)
}
return `${userAgentBase}/${version}`
}
export { fetchLimitBytes, getUserAgent }

View File

@@ -1,27 +0,0 @@
import { expect } from 'chai'
import { getUserAgent } from './got-config.js'
describe('getUserAgent function', function () {
afterEach(function () {
delete process.env.HEROKU_SLUG_COMMIT
delete process.env.DOCKER_SHIELDS_VERSION
})
it('uses the default userAgentBase', function () {
expect(getUserAgent()).to.equal('shields (self-hosted)/dev')
})
it('applies custom userAgentBase', function () {
expect(getUserAgent('custom')).to.equal('custom/dev')
})
it('uses short commit SHA from HEROKU_SLUG_COMMIT if available', function () {
process.env.HEROKU_SLUG_COMMIT = '92090bd44742a5fac03bcb117002088fc7485834'
expect(getUserAgent('custom')).to.equal('custom/92090bd')
})
it('uses short commit SHA from DOCKER_SHIELDS_VERSION if available', function () {
process.env.DOCKER_SHIELDS_VERSION = 'server-2021-11-22'
expect(getUserAgent('custom')).to.equal('custom/server-2021-11-22')
})
})

View File

@@ -1,60 +0,0 @@
import got, { CancelError } from 'got'
import { Inaccessible, InvalidResponse } from './errors.js'
import {
fetchLimitBytes as fetchLimitBytesDefault,
getUserAgent,
} from './got-config.js'
const userAgent = getUserAgent()
async function sendRequest(gotWrapper, url, options) {
const gotOptions = Object.assign({}, options)
gotOptions.throwHttpErrors = false
gotOptions.retry = { limit: 0 }
gotOptions.headers = gotOptions.headers || {}
gotOptions.headers['User-Agent'] = userAgent
try {
const resp = await gotWrapper(url, gotOptions)
return { res: resp, buffer: resp.body }
} catch (err) {
if (err instanceof CancelError) {
throw new InvalidResponse({
underlyingError: new Error('Maximum response size exceeded'),
})
}
throw new Inaccessible({ underlyingError: err })
}
}
function _fetchFactory(fetchLimitBytes = fetchLimitBytesDefault) {
const gotWithLimit = got.extend({
handlers: [
(options, next) => {
const promiseOrStream = next(options)
promiseOrStream.on('downloadProgress', progress => {
if (
progress.transferred > fetchLimitBytes &&
// just accept the file if we've already finished downloading
// the entire file before we went over the limit
progress.percent !== 1
) {
/*
TODO: we should be able to pass cancel() a message
https://github.com/sindresorhus/got/blob/main/documentation/advanced-creation.md#examples
but by the time we catch it, err.message is just "Promise was canceled"
*/
promiseOrStream.cancel('Maximum response size exceeded')
}
})
return promiseOrStream
},
],
})
return sendRequest.bind(sendRequest, gotWithLimit)
}
const fetch = _fetchFactory()
export { fetch, _fetchFactory }

View File

@@ -1,67 +0,0 @@
import { expect } from 'chai'
import nock from 'nock'
import { _fetchFactory } from './got.js'
import { Inaccessible, InvalidResponse } from './errors.js'
describe('got wrapper', function () {
it('should not throw an error if the response <= fetchLimitBytes', async function () {
nock('https://www.google.com')
.get('/foo/bar')
.once()
.reply(200, 'x'.repeat(100))
const sendRequest = _fetchFactory(100)
const { res } = await sendRequest('https://www.google.com/foo/bar')
expect(res.statusCode).to.equal(200)
})
it('should throw an InvalidResponse error if the response is > fetchLimitBytes', async function () {
nock('https://www.google.com')
.get('/foo/bar')
.once()
.reply(200, 'x'.repeat(101))
const sendRequest = _fetchFactory(100)
return expect(
sendRequest('https://www.google.com/foo/bar')
).to.be.rejectedWith(InvalidResponse, 'Maximum response size exceeded')
})
it('should throw an Inaccessible error if the request throws a (non-HTTP) error', async function () {
nock('https://www.google.com').get('/foo/bar').replyWithError('oh no')
const sendRequest = _fetchFactory(1024)
return expect(
sendRequest('https://www.google.com/foo/bar')
).to.be.rejectedWith(Inaccessible, 'oh no')
})
it('should throw an Inaccessible error if the host can not be accessed', async function () {
this.timeout(5000)
nock.disableNetConnect()
const sendRequest = _fetchFactory(1024)
return expect(
sendRequest('https://www.google.com/foo/bar')
).to.be.rejectedWith(
Inaccessible,
'Nock: Disallowed net connect for "www.google.com:443/foo/bar"'
)
})
it('should pass a custom user agent header', async function () {
nock('https://www.google.com', {
reqheaders: {
'user-agent': function (agent) {
return agent.startsWith('shields (self-hosted)')
},
},
})
.get('/foo/bar')
.once()
.reply(200)
const sendRequest = _fetchFactory(1024)
await sendRequest('https://www.google.com/foo/bar')
})
afterEach(function () {
nock.cleanAll()
nock.enableNetConnect()
})
})

View File

@@ -1,3 +1,4 @@
'use strict'
/**
* @module
*/
@@ -48,4 +49,4 @@ function mergeQueries(...queries) {
return merged
}
export { mergeQueries }
module.exports = { mergeQueries }

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