Compare commits

..

2 Commits

Author SHA1 Message Date
Paul Melnikow
332a496e84 Match unit test to value used by API 2020-09-22 15:57:24 -04:00
Paul Melnikow
5f28ac34cc [PyPI] When Python version classifiers are absent, fall back to requires_python 2020-09-22 15:54:16 -04:00
1428 changed files with 49283 additions and 75585 deletions

View File

@@ -1,13 +1,74 @@
version: 2 version: 2
main_steps: &main_steps
steps:
- checkout
- run:
name: Install dependencies
command: npm ci
environment:
# https://docs.cypress.io/guides/getting-started/installing-cypress.html#Skipping-installation
# We don't need to install the Cypress binary in jobs that aren't actually running Cypress.
CYPRESS_INSTALL_BINARY: 0
- run:
name: Linter
when: always
command: npm run lint
- run:
name: Core tests
when: always
environment:
mocha_reporter: mocha-junit-reporter
MOCHA_FILE: junit/core/results.xml
command: npm run test:core
- run:
name: Entrypoint tests
when: always
environment:
mocha_reporter: mocha-junit-reporter
MOCHA_FILE: junit/entrypoint/results.xml
command: npm run test:entrypoint
- store_test_results:
path: junit
- run:
name: 'Prettier check (quick fix: `npm run prettier`)'
when: always
command: npm run prettier:check
integration_steps: &integration_steps
steps:
- checkout
- run:
name: Install dependencies
command: npm ci
environment:
CYPRESS_INSTALL_BINARY: 0
- run:
name: Integration tests
when: always
environment:
mocha_reporter: mocha-junit-reporter
MOCHA_FILE: junit/integration/results.xml
command: npm run test:integration
- store_test_results:
path: junit
services_steps: &services_steps services_steps: &services_steps
steps: steps:
- checkout - checkout
- run: - run:
name: Install dependencies name: Install dependencies
command: | command: npm ci
npm ci
environment: environment:
CYPRESS_INSTALL_BINARY: 0 CYPRESS_INSTALL_BINARY: 0
@@ -25,44 +86,286 @@ services_steps: &services_steps
- store_test_results: - store_test_results:
path: junit 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
# install the shields.io dependencies
npm ci
# run the package tests
npm run test:package
npm run check-types:package
# delete the sheilds.io dependencies
rm -rf node_modules/
# run a smoke test (render a badge with the CLI)
# with only the package dependencies installed
cd badge-maker
npm link
badge cactus grown :green @flat
package_steps: &package_steps
steps:
- checkout
- run:
name: Install node and npm
command: |
set +e
export NVM_DIR="/opt/circleci/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
nvm install v12
nvm use v12
npm install -g npm
# Run the package tests on each currently supported node version. See:
# https://github.com/badges/shields/blob/master/badge-maker/README.md#node-version-support
# https://nodejs.org/en/about/releases/
- run:
<<: *run_package_tests
environment:
mocha_reporter: mocha-junit-reporter
MOCHA_FILE: junit/badge-maker/v10/results.xml
NODE_VERSION: v10
CYPRESS_INSTALL_BINARY: 0
name: Run package tests on Node 10
- run:
<<: *run_package_tests
environment:
mocha_reporter: mocha-junit-reporter
MOCHA_FILE: junit/badge-maker/v12/results.xml
NODE_VERSION: v12
CYPRESS_INSTALL_BINARY: 0
name: Run package tests on Node 12
- run:
<<: *run_package_tests
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
- store_test_results:
path: junit
jobs: jobs:
main:
docker:
- image: circleci/node:12
<<: *main_steps
main@node-14:
docker:
- image: circleci/node:14
<<: *main_steps
integration:
docker:
- image: circleci/node:12
- image: redis
<<: *integration_steps
integration@node-14:
docker:
- image: circleci/node:14
- image: redis
<<: *integration_steps
danger:
docker:
- image: circleci/node:12
steps:
- checkout
- run:
name: Install dependencies
command: npm ci
environment:
CYPRESS_INSTALL_BINARY: 0
- run:
name: Danger
when: always
environment:
# https://github.com/gatsbyjs/gatsby/pull/11555
NODE_ENV: test
command: npm run danger ci
frontend:
docker:
- image: circleci/node:12
steps:
- checkout
- run:
name: Install dependencies
command: npm ci
environment:
CYPRESS_INSTALL_BINARY: 0
- run:
name: Prepare frontend tests
command: npm run defs && npm run features
- run:
name: Check types
command: npm run check-types:frontend
- run:
name: Frontend unit tests
environment:
mocha_reporter: mocha-junit-reporter
MOCHA_FILE: junit/frontend/results.xml
when: always
command: npm run test:frontend
- store_test_results:
path: junit
- run:
name: Frontend build completes successfully
when: always
command: npm run build
package:
machine: true
<<: *package_steps
services: services:
docker: docker:
- image: cimg/node:16.15 - image: circleci/node:12
<<: *services_steps <<: *services_steps
services@node-17: services@node-14:
docker: docker:
- image: cimg/node:17.9 - image: circleci/node:14
environment:
NPM_CONFIG_ENGINE_STRICT: 'false'
<<: *services_steps <<: *services_steps
e2e:
docker:
- image: cypress/base:12
steps:
- checkout
- restore_cache:
name: Restore Cypress binary
keys:
- v2-cypress-dependencies-{{ checksum "package-lock.json" }}
- run:
name: Install dependencies
command: npm ci
- run:
name: Frontend build
command: GATSBY_BASE_URL=http://localhost:8080 npm run build
- run:
name: Run tests
environment:
CYPRESS_REPORTER: junit
MOCHA_FILE: junit/e2e/results.xml
command: npm run e2e-on-build
- store_test_results:
path: junit
- store_artifacts:
path: cypress/videos
- store_artifacts:
path: cypress/screenshots
- save_cache:
name: Cache Cypress binary
paths:
# https://docs.cypress.io/guides/getting-started/installing-cypress.html#Binary-cache
- ~/.cache/Cypress
key: v2-cypress-dependencies-{{ checksum "package-lock.json" }}
workflows: workflows:
version: 2 version: 2
on-commit: on-commit:
jobs: jobs:
- main:
filters:
branches:
ignore: gh-pages
- main@node-14:
filters:
branches:
ignore: gh-pages
- integration@node-14:
filters:
branches:
ignore: gh-pages
- frontend:
filters:
branches:
ignore: gh-pages
- package:
filters:
branches:
ignore: gh-pages
- services: - services:
filters: filters:
branches: branches:
ignore: ignore:
- master - master
- gh-pages - gh-pages
- services@node-17: - services@node-14:
filters: filters:
branches: branches:
ignore: ignore:
- master - master
- gh-pages - gh-pages
- danger:
filters:
branches:
ignore:
- master
- gh-pages
- /dependabot\/.*/
- e2e:
filters:
branches:
ignore: gh-pages
# on-commit-with-cache: # on-commit-with-cache:
# jobs: # jobs:
# - npm-install: # - npm-install:
# filters: # filters:
# branches: # branches:
# ignore: gh-pages # ignore: gh-pages
# - main:
# requires:
# - npm-install
# - main@node-latest:
# requires:
# - npm-install
# - frontend:
# requires:
# - npm-install
# - services: # - services:
# requires: # requires:
# - npm-install # - npm-install
@@ -75,3 +378,9 @@ workflows:
# filters: # filters:
# branches: # branches:
# ignore: master # ignore: master
# - danger:
# requires:
# - npm-install
# filters:
# branches:
# ignore: /dependabot\/.*/

36
.dependabot/config.yml Normal file
View File

@@ -0,0 +1,36 @@
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'
# badge-maker package dependencies
- package_manager: 'javascript'
directory: '/badge-maker'
update_schedule: 'weekly'

View File

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

View File

@@ -1,14 +1,13 @@
extends: extends:
- standard - standard
- standard-jsx
- standard-react - standard-react
- plugin:@typescript-eslint/recommended - plugin:@typescript-eslint/recommended
- prettier - prettier
- prettier/@typescript-eslint
- prettier/standard
- prettier/react
- eslint:recommended - eslint:recommended
globals:
JSX: 'readonly'
parserOptions: parserOptions:
# Override eslint-config-standard, which incorrectly sets this to "module", # Override eslint-config-standard, which incorrectly sets this to "module",
# though that setting is only for ES6 modules, not CommonJS modules. # though that setting is only for ES6 modules, not CommonJS modules.
@@ -43,7 +42,6 @@ overrides:
es6: true es6: true
rules: rules:
no-console: 'off' no-console: 'off'
'@typescript-eslint/explicit-module-boundary-types': 'off'
- files: - files:
- '**/*.@(ts|tsx)' - '**/*.@(ts|tsx)'
@@ -59,7 +57,6 @@ overrides:
'@typescript-eslint/no-object-literal-type-assertion': 'off' '@typescript-eslint/no-object-literal-type-assertion': 'off'
'@typescript-eslint/no-explicit-any': 'error' '@typescript-eslint/no-explicit-any': 'error'
'@typescript-eslint/ban-ts-ignore': 'off' '@typescript-eslint/ban-ts-ignore': 'off'
'@typescript-eslint/explicit-module-boundary-types': 'off'
- files: - files:
- core/**/*.ts - core/**/*.ts
@@ -127,15 +124,13 @@ rules:
'@typescript-eslint/no-var-requires': 'off' '@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. # These should be disabled by eslint-config-prettier, but are not.
no-extra-semi: 'off' no-extra-semi: 'off'
# Shields additions. # Shields additions.
no-var: 'error' no-var: 'error'
prefer-const: 'error' prefer-const: 'error'
strict: 'error'
arrow-body-style: ['error', 'as-needed'] arrow-body-style: ['error', 'as-needed']
no-extension-in-require/main: 'error' no-extension-in-require/main: 'error'
object-shorthand: ['error', 'properties'] object-shorthand: ['error', 'properties']
@@ -144,22 +139,6 @@ rules:
func-style: ['error', 'declaration', { 'allowArrowFunctions': true }] func-style: ['error', 'declaration', { 'allowArrowFunctions': true }]
new-cap: ['error', { 'capIsNew': true }] new-cap: ['error', { 'capIsNew': true }]
import/order: ['error', { 'newlines-between': 'never' }] import/order: ['error', { 'newlines-between': 'never' }]
quotes:
['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }]
# Account for destructuring responses from upstream services,
# 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. # Chai friendly.
no-unused-expressions: 'off' no-unused-expressions: 'off'

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
- [ ] [badge-maker NPM package](https://www.npmjs.com/package/badge-maker)
: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. --> <!-- 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. --> <!-- Provide the complete stack trace from the CircleCI test summary. -->

View File

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

View File

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

View File

@@ -1,7 +1,4 @@
contact_links: 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 - name: ❓ Support Question
url: https://github.com/badges/shields/discussions url: https://github.com/badges/shields/discussions
about: Ask a question about Shields.io 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: 'node16'
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,431 +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.10.0",
"@actions/github": "^5.1.1"
}
},
"node_modules/@actions/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz",
"integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==",
"dependencies": {
"@actions/http-client": "^2.0.1",
"uuid": "^8.3.2"
}
},
"node_modules/@actions/github": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-5.1.1.tgz",
"integrity": "sha512-Nk59rMDoJaV+mHCOJPXuvB1zIbomlKS0dmSIqPGxd0enAXBnOfn4VWF+CGtRCwXZG9Epa54tZA7VIRlJDS8A6g==",
"dependencies": {
"@actions/http-client": "^2.0.1",
"@octokit/core": "^3.6.0",
"@octokit/plugin-paginate-rest": "^2.17.0",
"@octokit/plugin-rest-endpoint-methods": "^5.13.0"
}
},
"node_modules/@actions/http-client": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz",
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==",
"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/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"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.10.0",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz",
"integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==",
"requires": {
"@actions/http-client": "^2.0.1",
"uuid": "^8.3.2"
}
},
"@actions/github": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-5.1.1.tgz",
"integrity": "sha512-Nk59rMDoJaV+mHCOJPXuvB1zIbomlKS0dmSIqPGxd0enAXBnOfn4VWF+CGtRCwXZG9Epa54tZA7VIRlJDS8A6g==",
"requires": {
"@actions/http-client": "^2.0.1",
"@octokit/core": "^3.6.0",
"@octokit/plugin-paginate-rest": "^2.17.0",
"@octokit/plugin-rest-endpoint-methods": "^5.13.0"
}
},
"@actions/http-client": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz",
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==",
"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=="
},
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
},
"webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"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.10.0",
"@actions/github": "^5.1.1"
}
}

View File

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

View File

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

View File

@@ -1,20 +0,0 @@
name: 'Integration tests'
description: 'Run integration tests'
inputs:
github-token:
description: 'The GITHUB_TOKEN secret'
required: true
runs:
using: 'composite'
steps:
- name: Integration Tests
if: always()
run: npm run test:integration -- --reporter json --reporter-option 'output=reports/integration-tests.json'
env:
GH_TOKEN: '${{ inputs.github-token }}'
shell: bash
- name: Write Markdown Summary
if: always()
run: node scripts/mocha2md.js Integration reports/integration-tests.json >> $GITHUB_STEP_SUMMARY
shell: bash

View File

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

View File

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

View File

@@ -1,43 +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
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: weekly
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@2.0.1
with:
github-token: '${{ secrets.GITHUB_TOKEN }}'

View File

@@ -1,22 +0,0 @@
name: Auto close
on:
pull_request_target:
types: [opened]
permissions:
pull-requests: write
jobs:
auto-close:
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'
steps:
- name: Checkout
uses: actions/checkout@v3
- 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-docker-image:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Set Git Short SHA
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
- name: Build
uses: docker/build-push-action@v3
with:
context: .
push: false
tags: shieldsio/shields:pr-validation
build-args: |
version=${{ env.SHORT_SHA }}

View File

@@ -1,52 +0,0 @@
name: Create Release
on:
pull_request:
types: [closed]
permissions:
contents: write
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@v3
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@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push snapshot release to DockerHub
uses: docker/build-push-action@v3
with:
context: .
push: true
tags: shieldsio/shields:server-${{ steps.date.outputs.date }}
build-args: |
version=server-${{ steps.date.outputs.date }}

View File

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

View File

@@ -1,29 +0,0 @@
name: Deploy Documentation
on:
push:
branches:
- master
permissions:
contents: write
jobs:
deploy-docs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
persist-credentials: false
- name: Build
run: |
npm ci
npm run build-docs
- name: Deploy
uses: JamesIves/github-pages-deploy-action@v4
with:
branch: gh-pages
folder: api-docs
clean: true

View File

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

View File

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

View File

@@ -1,33 +0,0 @@
name: Build and Publish Next Docker Image
on:
push:
branches:
- master
jobs:
publish-docker-next:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
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@v3
with:
context: .
push: true
tags: shieldsio/shields:next
build-args: |
version=${{ env.SHORT_SHA }}

View File

@@ -1,52 +0,0 @@
name: E2E
on:
pull_request:
types: [opened, edited, reopened, synchronize]
push:
branches-ignore:
- 'gh-pages'
- 'dependabot/**'
jobs:
test-e2e:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Cache Cypress binary
id: cache-cypress
uses: actions/cache@v3
env:
cache-name: cache-cypress
with:
path: ~/.cache/Cypress
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
- name: Setup
uses: ./.github/actions/setup
with:
node-version: 16
cypress: true
- name: Frontend build
run: GATSBY_BASE_URL=http://localhost:8080 npm run build
- name: Run tests
env:
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
run: npm run e2e-on-build
- name: Archive videos
if: always()
uses: actions/upload-artifact@v3
with:
name: videos
path: cypress/videos
- name: Archive screenshots
if: always()
uses: actions/upload-artifact@v3
with:
name: screenshots
path: cypress/screenshots

View File

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

View File

@@ -1,48 +0,0 @@
name: Integration@node 17
on:
pull_request:
types: [opened, edited, reopened, synchronize]
push:
branches-ignore:
- 'gh-pages'
- 'dependabot/**'
jobs:
test-integration-17:
runs-on: ubuntu-latest
env:
PAT_EXISTS: ${{ secrets.GH_PAT != '' }}
services:
redis:
image: redis
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup
uses: ./.github/actions/setup
with:
node-version: 17
env:
NPM_CONFIG_ENGINE_STRICT: 'false'
- name: Integration Tests (with PAT)
if: ${{ env.PAT_EXISTS == 'true' }}
uses: ./.github/actions/integration-tests
with:
github-token: '${{ secrets.GH_PAT }}'
- name: Integration Tests (with workflow token)
if: ${{ env.PAT_EXISTS == 'false' }}
uses: ./.github/actions/integration-tests
with:
github-token: '${{ secrets.GITHUB_TOKEN }}'

View File

@@ -1,46 +0,0 @@
name: Integration
on:
pull_request:
types: [opened, edited, reopened, synchronize]
push:
branches-ignore:
- 'gh-pages'
- 'dependabot/**'
jobs:
test-integration:
runs-on: ubuntu-latest
env:
PAT_EXISTS: ${{ secrets.GH_PAT != '' }}
services:
redis:
image: redis
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup
uses: ./.github/actions/setup
with:
node-version: 16
- name: Integration Tests (with PAT)
if: ${{ env.PAT_EXISTS == 'true' }}
uses: ./.github/actions/integration-tests
with:
github-token: '${{ secrets.GH_PAT }}'
- name: Integration Tests (with workflow token)
if: ${{ env.PAT_EXISTS == 'false' }}
uses: ./.github/actions/integration-tests
with:
github-token: '${{ secrets.GITHUB_TOKEN }}'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

3
.gitignore vendored
View File

@@ -104,8 +104,7 @@ service-definitions.yml
!/config/local*.template.yml !/config/local*.template.yml
# Gatsby # Gatsby
/frontend/.cache /.cache
/frontend/public
/public /public
# Cypress # Cypress

View File

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

2
.npmrc
View File

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

View File

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

View File

@@ -2,10 +2,10 @@ package.json
package-lock.json package-lock.json
/__snapshots__ /__snapshots__
/.next /.next
.cache /.cache
/api-docs /api-docs
/build /build
public /public
/coverage /coverage
private/*.json private/*.json
/.nyc_output /.nyc_output

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,343 +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-2023-01-01
- Breaking change: Routes for GitHub workflows badge have changed. See https://github.com/badges/shields/issues/8671 for more details
- Behaviour change: In this release we fixed a long standing bug. GitHub badges were previously not reading the base URL from the `config.service.baseUri`.
This release fixes that bug, bringing the code into line with the documented behaviour. This should not cause a behaviour change for most users,
but users who had previously set a value in `config.service.baseUri` which was previously ignored could see this now have an effect.
Users who configure their instance using env vars rather than yaml should see no change.
- Send `X-GitHub-Api-Version` when calling [GitHub] v3 API [#8669](https://github.com/badges/shields/issues/8669)
- add [VpmVersion] badge [#8755](https://github.com/badges/shields/issues/8755)
- Add [modrinth] game versions [#8673](https://github.com/badges/shields/issues/8673)
- fix debug logging of undefined query params [#8540](https://github.com/badges/shields/issues/8540), [#8757](https://github.com/badges/shields/issues/8757)
- fall back to classifiers if [pypi] license text is really long [#8690](https://github.com/badges/shields/issues/8690)
- allow passing key to [stackexchange] [#8539](https://github.com/badges/shields/issues/8539)
- Dependency updates
## server-2022-12-01
- fix: support logoColor to shield icons. [#8263](https://github.com/badges/shields/issues/8263)
- handle missing properties array in [VisualStudioMarketplaceVersion] [#8603](https://github.com/badges/shields/issues/8603)
- deprecate [wercker] service [#8642](https://github.com/badges/shields/issues/8642)
- Add [Coincap] Cryptocurrency badges [#8623](https://github.com/badges/shields/issues/8623)
- Add [modrinth] version [#8604](https://github.com/badges/shields/issues/8604)
- [factorio-mod-portal] services [#8625](https://github.com/badges/shields/issues/8625)
- [Coveralls] for GitLab [#8584](https://github.com/badges/shields/issues/8584), [#8644](https://github.com/badges/shields/issues/8644)
- Remove 'suggest badges' feature [#8311](https://github.com/badges/shields/issues/8311)
- Add [modrinth] followers [#8601](https://github.com/badges/shields/issues/8601)
- Update the [modrinth] API to v2 [#8600](https://github.com/badges/shields/issues/8600)
- tidy up [GitHubGist] routes [#8510](https://github.com/badges/shields/issues/8510)
- fix [flathub] version error handling [#8500](https://github.com/badges/shields/issues/8500)
- Dependency updates
## server-2022-11-01
- [Ansible] Add collection badge [#8578](https://github.com/badges/shields/issues/8578)
- [VisualStudioMarketplace] Add support to prerelease extensions version (Issue #8207) [#8561](https://github.com/badges/shields/issues/8561)
- feat: add [GitlabLastCommit] service [#8508](https://github.com/badges/shields/issues/8508)
- fix [swagger] service tests (allow 0 items in array) [#8564](https://github.com/badges/shields/issues/8564)
- fix codecov badge for non-default branch [#8565](https://github.com/badges/shields/issues/8565)
- Add [GitHubLastCommit] by committer badge [#8537](https://github.com/badges/shields/issues/8537)
- [GitHubReleaseDate] - published_at field [#8543](https://github.com/badges/shields/issues/8543)
- Fix [Testspace] with new "untested" value in case_counts array [#8544](https://github.com/badges/shields/issues/8544)
- fix: Support WAITING status for GitHub deployments [#8521](https://github.com/badges/shields/issues/8521)
- [Whatpulse] badge for a user and for a team [#8466](https://github.com/badges/shields/issues/8466)
- deprecate [pkgreview] service [#8499](https://github.com/badges/shields/issues/8499)
- Dependency updates
## server-2022-10-08
- deprecate [criterion] service [#8501](https://github.com/badges/shields/issues/8501)
- fix formatRelativeDate error handling; run [date] [#8497](https://github.com/badges/shields/issues/8497)
- allow/validate bitbucket_username / bitbucket_password in private config schema [#8472](https://github.com/badges/shields/issues/8472)
- fix [pub] points badge test and example [#8498](https://github.com/badges/shields/issues/8498)
- feat: add [GitlabLanguageCount] service [#8377](https://github.com/badges/shields/issues/8377)
- [GitHubGistStars] add GitHub Gist Stars [#8471](https://github.com/badges/shields/issues/8471)
- fix display/search of CII badge examples [#8473](https://github.com/badges/shields/issues/8473)
- feat: add 2022 support to GitHub Hacktoberfest [#8468](https://github.com/badges/shields/issues/8468)
- fix [GitLabCoverage] subgroup bug [#8401](https://github.com/badges/shields/issues/8401)
- implement ruby gems-specific version sort/color functions [#8434](https://github.com/badges/shields/issues/8434)
- Add `rc` to pre-release identifiers [#8435](https://github.com/badges/shields/issues/8435)
- add [GitHub] Number of commits between branches/tags/commits [#8394](https://github.com/badges/shields/issues/8394)
- add [Packagist] dependency version [#8371](https://github.com/badges/shields/issues/8371)
- fix Docker build status invalid response data bug [#8392](https://github.com/badges/shields/issues/8392)
- Dependency updates
## server-2022-09-04
- fix frontend compile for users running on Windows [#8350](https://github.com/badges/shields/issues/8350)
- [DockerSize] Docker image size multi arch [#8290](https://github.com/badges/shields/issues/8290)
- upgrade gatsby [#8334](https://github.com/badges/shields/issues/8334)
- Custom domains for [JitPack] artifacts [#8333](https://github.com/badges/shields/issues/8333)
- fix [dockerstars] service [#8316](https://github.com/badges/shields/issues/8316)
- [BountySource] Fix: Broken Badge generation for decimal activity values [#8315](https://github.com/badges/shields/issues/8315)
- feat: add [gitlabmergerequests] service [#8166](https://github.com/badges/shields/issues/8166)
- Fix terminology for [ROS] version service [#8292](https://github.com/badges/shields/issues/8292)
- feat: add [GitlabStars] service [#8209](https://github.com/badges/shields/issues/8209)
- Fix invalid `rst` format when `alt` or `target` is present [#8275](https://github.com/badges/shields/issues/8275)
- [GithubGistLastCommit] GitHub gist last commit [#8272](https://github.com/badges/shields/issues/8272)
- [GitHub] GitHub file size for a specific branch [#8262](https://github.com/badges/shields/issues/8262)
- Dependency updates
## server-2022-08-01
- [pypi] Add Framework Version Badges support [#8261](https://github.com/badges/shields/issues/8261)
- feat: add [GitlabForks] server [#8208](https://github.com/badges/shields/issues/8208)
- Update PyPI api according to https://warehouse.pypa.io/api-reference/json.html [#8251](https://github.com/badges/shields/issues/8251)
- Add [galaxytoolshed] Activity [#8164](https://github.com/badges/shields/issues/8164)
- [greasyfork] Add Greasy Fork rating badges [#8087](https://github.com/badges/shields/issues/8087)
- refactor(deps): Replace moment with dayjs [#8192](https://github.com/badges/shields/issues/8192)
- add spaces round pipe in [conda] badge [#8189](https://github.com/badges/shields/issues/8189)
- Add [ROS] version service [#8169](https://github.com/badges/shields/issues/8169)
- feat: add [gitlabissues] service [#8108](https://github.com/badges/shields/issues/8108)
- Dependency updates
## server-2022-07-03
- Add [galaxytoolshed] services [#8114](https://github.com/badges/shields/issues/8114)
- fix [gitlab] auth [#8145](https://github.com/badges/shields/issues/8145) [#8162](https://github.com/badges/shields/issues/8162)
- increase cache length on AUR version badge, run [AUR] [#8110](https://github.com/badges/shields/issues/8110)
- Use GraphQL to fix GitHub file count badges [github] [#8112](https://github.com/badges/shields/issues/8112)
- feat: add [gitlab] contributors service [#8084](https://github.com/badges/shields/issues/8084)
- [greasyfork] Add Greasy Fork service badges [#8080](https://github.com/badges/shields/issues/8080)
- Add [gitlablicense] services [#8024](https://github.com/badges/shields/issues/8024)
- [Spack] Package Manager: Update Domain [#8046](https://github.com/badges/shields/issues/8046)
- switch [jitpack] to use latestOk endpoint [#8041](https://github.com/badges/shields/issues/8041)
- Dependency updates
## server-2022-06-01
- Update GitLab logo (2022) [#7984](https://github.com/badges/shields/issues/7984)
- [GitHub] Added milestone property to GitHub issue details service [#7864](https://github.com/badges/shields/issues/7864)
- [Spack] Package Manager: Update Endpoint [#7957](https://github.com/badges/shields/issues/7957)
- Update Chocolatey API endpoint URL [#7952](https://github.com/badges/shields/issues/7952)
- [Flathub]Add downloads badge [#7724](https://github.com/badges/shields/issues/7724)
- replace the outdated Telegram logo with the newest [#7831](https://github.com/badges/shields/issues/7831)
- add [PUB] points badge [#7918](https://github.com/badges/shields/issues/7918)
- add [PUB] popularity badge [#7920](https://github.com/badges/shields/issues/7920)
- add [PUB] likes badge [#7916](https://github.com/badges/shields/issues/7916)
- Dependency updates
## server-2022-05-03
- [OSSFScorecard] Create scorecard badge service [#7687](https://github.com/badges/shields/issues/7687)
- Stringify [githublanguagecount] message [#7881](https://github.com/badges/shields/issues/7881)
- Stringify and trim whitespace from a few services [#7880](https://github.com/badges/shields/issues/7880)
- add labels to Dockerfile [#7862](https://github.com/badges/shields/issues/7862)
- handle missing 'fly-client-ip' [#7814](https://github.com/badges/shields/issues/7814)
- Dependency updates
## server-2022-04-03
- Breaking change: This release updates ioredis from v4 to v5.
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

@@ -101,7 +101,7 @@ There are three places to get help:
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_ - 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. 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_. default_.
- Badges should not obtain data from undocumented or reverse-engineered API endpoints. - 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. - Badges should not obtain data by scraping web pages - these are likely to break frequently.
@@ -131,10 +131,16 @@ Prettier before a commit by default.
### Tests ### 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. When changing other code, please add unit tests.
To run the integration tests, you must have Redis installed and in your PATH. To run the integration tests, you must have redis installed and in your PATH.
Use `brew install redis`, `yum install redis`, etc. The test runner will Use `brew install redis`, `yum install redis`, etc. The test runner will
start the server automatically. start the server automatically.
@@ -147,35 +153,3 @@ There is a [High-level code walkthrough](doc/code-walkthrough.md) describing the
### Logos ### Logos
We have [documentation for logo usage](doc/logos.md) which includes [contribution guidance](doc/logos.md#contributing-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,4 +1,4 @@
FROM node:16-alpine AS Builder FROM node:12-alpine
RUN mkdir -p /usr/src/app RUN mkdir -p /usr/src/app
RUN mkdir /usr/src/app/private RUN mkdir /usr/src/app/private
@@ -8,8 +8,6 @@ COPY package.json package-lock.json /usr/src/app/
# Without the badge-maker package.json and CLI script in place, `npm ci` will fail. # Without the badge-maker package.json and CLI script in place, `npm ci` will fail.
COPY badge-maker /usr/src/app/badge-maker/ COPY badge-maker /usr/src/app/badge-maker/
RUN apk add python3 make g++
RUN npm install -g "npm@>=8"
# We need dev deps to build the front end. We don't need Cypress, though. # We need dev deps to build the front end. We don't need Cypress, though.
RUN NODE_ENV=development CYPRESS_INSTALL_BINARY=0 npm ci RUN NODE_ENV=development CYPRESS_INSTALL_BINARY=0 npm ci
@@ -18,20 +16,9 @@ RUN npm run build
RUN npm prune --production RUN npm prune --production
RUN npm cache clean --force 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. # Run the server using production configs.
ENV NODE_ENV production ENV NODE_ENV production
WORKDIR /usr/src/app
COPY --from=Builder /usr/src/app /usr/src/app
CMD node server CMD node server
EXPOSE 80 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

View File

@@ -1,5 +1,5 @@
<p align="center"> <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"> height="130">
</p> </p>
<p align="center"> <p align="center">
@@ -19,6 +19,12 @@
<a href="https://coveralls.io/github/badges/shields"> <a href="https://coveralls.io/github/badges/shields">
<img src="https://img.shields.io/coveralls/github/badges/shields" <img src="https://img.shields.io/coveralls/github/badges/shields"
alt="coverage"></a> alt="coverage"></a>
<a href="https://lgtm.com/projects/g/badges/shields/alerts/">
<img src="https://img.shields.io/lgtm/alerts/g/badges/shields"
alt="Total alerts"/></a>
<a href="https://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"> <a href="https://discord.gg/HjJCwm5">
<img src="https://img.shields.io/discord/308323056592486420?logo=discord" <img src="https://img.shields.io/discord/308323056592486420?logo=discord"
alt="chat on Discord"></a> alt="chat on Discord"></a>
@@ -32,13 +38,7 @@ 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 GitHub readmes or any other web page. The service supports dozens of
continuous integration services, package registries, distributions, app continuous integration services, package registries, distributions, app
stores, social networks, code coverage services, and code analysis services. 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 Every month it serves over 470 million images.
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
This repo hosts: This repo hosts:
@@ -70,7 +70,7 @@ This repo hosts:
[Make your own badges!][custom badges] [Make your own badges!][custom badges]
(Quick example: `https://img.shields.io/badge/left-right-f39f37`) (Quick example: `https://img.shields.io/badge/left-right-f39f37`)
[custom badges]: https://shields.io/#your-badge [custom badges]: http://shields.io/#your-badge
### Quickstart ### Quickstart
@@ -86,20 +86,20 @@ and pull requests! You can peruse the [contributing guidelines][contributing].
When adding or changing a service [please add tests][service-tests]. 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, 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. 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].
[![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) [![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 [service-tests]: https://github.com/badges/shields/blob/master/doc/service-tests.md
[tutorial]: https://github.com/badges/shields/blob/master/doc/TUTORIAL.md [tutorial]: doc/TUTORIAL.md
[contributing]: https://github.com/badges/shields/blob/master/CONTRIBUTING.md [contributing]: CONTRIBUTING.md
## Development ## Development
1. Install Node 16 or later. You can use the [package manager][] of your choice. 1. Install Node 12 or later. You can use the [package manager][] of your choice.
Tests need to pass in Node 16 and 17. Tests need to pass in Node 12 and 14.
2. Clone this repository. 2. Clone this repository.
3. Run `npm ci` to install the dependencies. 3. Run `npm ci` to install the dependencies.
4. Run `npm start` to start the badge server and the frontend dev server. 4. Run `npm start` to start the badge server and the frontend dev server.
@@ -139,9 +139,9 @@ Daily tests, including a full run of the service tests and overall code coverage
[gitpod]: https://www.gitpod.io/ [gitpod]: https://www.gitpod.io/
[snapshot tests]: https://glebbahmutov.com/blog/snapshot-testing/ [snapshot tests]: https://glebbahmutov.com/blog/snapshot-testing/
[prometheus]: https://prometheus.io/ [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]: 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 [daily-tests]: https://github.com/badges/daily-tests
[nodemon]: https://nodemon.io/ [nodemon]: https://nodemon.io/
[nodemon debug]: https://github.com/Microsoft/vscode-recipes/tree/master/nodemon [nodemon debug]: https://github.com/Microsoft/vscode-recipes/tree/master/nodemon
@@ -151,22 +151,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]. There is documentation about [hosting your own server][self-hosting].
[self-hosting]: https://github.com/badges/shields/blob/master/doc/self-hosting.md [self-hosting]: 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
## History ## History
@@ -191,8 +176,8 @@ You can read more about [the project's inception][thread],
[olivierlacan]: https://github.com/olivierlacan [olivierlacan]: https://github.com/olivierlacan
[espadrine]: https://github.com/espadrine [espadrine]: https://github.com/espadrine
[old-gh-badges]: https://github.com/badges/gh-badges [old-gh-badges]: https://github.com/badges/gh-badges
[motivation]: https://github.com/badges/shields/blob/master/spec/motivation.md [motivation]: spec/motivation.md
[spec]: https://github.com/badges/shields/blob/master/spec/SPECIFICATION.md [spec]: spec/SPECIFICATION.md
[thread]: https://github.com/h5bp/lazyweb-requests/issues/150 [thread]: https://github.com/h5bp/lazyweb-requests/issues/150
## Project leaders ## Project leaders
@@ -219,6 +204,14 @@ Alumni:
- [espadrine](https://github.com/espadrine) - [espadrine](https://github.com/espadrine)
- [olivierlacan](https://github.com/olivierlacan) - [olivierlacan](https://github.com/olivierlacan)
## Related projects
- [poser PHP library][poser]
- [pybadges python library][pybadges]
[poser]: https://github.com/badges/poser
[pybadges]: https://github.com/google/pybadges
## License ## License
All assets and code are under the [CC0 LICENSE](LICENSE) and in the public All assets and code are under the [CC0 LICENSE](LICENSE) and in the public

View File

@@ -7,10 +7,10 @@ Please follow this guidance when reporting security issues affecting:
- [Shields.io](https://shields.io) - [Shields.io](https://shields.io)
- [Raster.shields.io](https://raster.shields.io) - [Raster.shields.io](https://raster.shields.io)
- Self-hosted Shields instances - Self-hosted Shields instances
- The [squint](https://github.com/badges/squint) raster proxy - The [svg-to-image-proxy](https://www.npmjs.com/package/svg-to-image-proxy) NPM package
- The [badge-maker](https://www.npmjs.com/package/badge-maker) NPM package - 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. The [gh-badges](https://www.npmjs.com/package/gh-badges) NPM package is now deprecated and will no longer receive fixes for bugs or security issues.
## Reporting a Vulnerability ## Reporting a Vulnerability

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,20 +1,5 @@
# Changelog # Changelog
## 4.0.0 [WIP]
- Drop compatibility with Node < 14
## 3.3.1
- Improve font measuring in for-the-badge and social styles
- Make for-the-badge letter spacing more predictable
## 3.3.0
- Readability improvements: a dark font color is automatically used when the badge's background is too light. For example: ![](https://img.shields.io/badge/hello-world-white)
- Better CSS color compliance: thanks to a switch from _is-css-color_ to _[css-color-converter](https://www.npmjs.com/package/css-color-converter)_, you can use a wider range of color formats from the latest CSS specification, for example `rgb(0 255 0)`
- Less dependencies: _badge-maker_ no longer depends on _camelcase_
## 3.2.0 ## 3.2.0
- Accessibility improvements: Help users of assistive technologies to read the badges when used inline - Accessibility improvements: Help users of assistive technologies to read the badges when used inline

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
'use strict' 'use strict'
const { fromString } = require('css-color-converter') const cssColorConverter = require('css-color-converter')
// When updating these, be sure also to update the list in `badge-maker/README.md`. // When updating these, be sure also to update the list in `badge-maker/README.md`.
const namedColors = { const namedColors = {
@@ -38,7 +38,10 @@ function isHexColor(s = '') {
} }
function isCSSColor(color) { function isCSSColor(color) {
return typeof color === 'string' && fromString(color.trim()) return (
typeof color === 'string' &&
typeof cssColorConverter(color.trim()).toRgbaArray() !== 'undefined'
)
} }
function normalizeColor(color) { function normalizeColor(color) {
@@ -70,9 +73,8 @@ function toSvgColor(color) {
function brightness(color) { function brightness(color) {
if (color) { if (color) {
const cssColor = fromString(color) const rgb = cssColorConverter(color).toRgbaArray()
if (cssColor) { if (rgb) {
const rgb = cssColor.toRgbaArray()
return +((rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 255000).toFixed(2) return +((rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 255000).toFixed(2)
} }
} }

View File

@@ -37,7 +37,7 @@ test(normalizeColor, () => {
given(' blue ').expect(' blue ') given(' blue ').expect(' blue ')
given('rgb(100%, 200%, 222%)').expect('rgb(100%, 200%, 222%)') given('rgb(100%, 200%, 222%)').expect('rgb(100%, 200%, 222%)')
given('rgb(122, 200, 222)').expect('rgb(122, 200, 222)') given('rgb(122, 200, 222)').expect('rgb(122, 200, 222)')
given('rgb(122, 200, 222, 1)').expect('rgb(122, 200, 222, 1)') given('rgb(100%, 200, 222)').expect('rgb(100%, 200, 222)')
given('rgba(100, 20, 111, 1)').expect('rgba(100, 20, 111, 1)') given('rgba(100, 20, 111, 1)').expect('rgba(100, 20, 111, 1)')
given('hsl(122, 200%, 222%)').expect('hsl(122, 200%, 222%)') given('hsl(122, 200%, 222%)').expect('hsl(122, 200%, 222%)')
given('hsla(122, 200%, 222%, 1)').expect('hsla(122, 200%, 222%, 1)') given('hsla(122, 200%, 222%, 1)').expect('hsla(122, 200%, 222%, 1)')
@@ -46,8 +46,8 @@ test(normalizeColor, () => {
given(''), given(''),
given('not-a-color'), given('not-a-color'),
given('#ABCFGH'), given('#ABCFGH'),
given('rgb(122, 200, 222, 1)'),
given('rgb(-100, 20, 111)'), given('rgb(-100, 20, 111)'),
given('rgb(100%, 200, 222)'),
given('rgba(-100, 20, 111, 1.1)'), given('rgba(-100, 20, 111, 1.1)'),
given('hsl(122, 200, 222, 1)'), given('hsl(122, 200, 222, 1)'),
given('hsl(122, 200, 222)'), given('hsl(122, 200, 222)'),

View File

@@ -51,8 +51,14 @@ function _clean(format) {
} }
}) })
// Legacy. // convert "public" format to "internal" format
cleaned.label = cleaned.label || '' cleaned.text = [cleaned.label || '', cleaned.message]
delete cleaned.label
delete cleaned.message
if ('style' in cleaned) {
cleaned.template = cleaned.style
delete cleaned.style
}
return cleaned return cleaned
} }

View File

@@ -2,7 +2,6 @@
const { normalizeColor, toSvgColor } = require('./color') const { normalizeColor, toSvgColor } = require('./color')
const badgeRenderers = require('./badge-renderers') const badgeRenderers = require('./badge-renderers')
const { stripXmlWhitespace } = require('./xml')
/* /*
note: makeBadge() is fairly thinly wrapped so if we are making changes here note: makeBadge() is fairly thinly wrapped so if we are making changes here
@@ -10,9 +9,8 @@ it is likely this will impact on the package's public interface in index.js
*/ */
module.exports = function makeBadge({ module.exports = function makeBadge({
format, format,
style = 'flat', template = 'flat',
label, text,
message,
color, color,
labelColor, labelColor,
logo, logo,
@@ -21,8 +19,9 @@ module.exports = function makeBadge({
links = ['', ''], links = ['', ''],
}) { }) {
// String coercion and whitespace removal. // String coercion and whitespace removal.
label = `${label}`.trim() text = text.map(value => `${value}`.trim())
message = `${message}`.trim()
const [label, message] = text
// This ought to be the responsibility of the server, not `makeBadge`. // This ought to be the responsibility of the server, not `makeBadge`.
if (format === 'json') { if (format === 'json') {
@@ -40,24 +39,23 @@ module.exports = function makeBadge({
}) })
} }
const render = badgeRenderers[style] const render = badgeRenderers[template]
if (!render) { if (!render) {
throw new Error(`Unknown badge style: '${style}'`) throw new Error(`Unknown template: '${template}'`)
} }
logoWidth = +logoWidth || (logo ? 14 : 0) logoWidth = +logoWidth || (logo ? 14 : 0)
return stripXmlWhitespace( return render({
render({ label,
label, message,
message, links,
links, logo,
logo, logoPosition,
logoPosition, logoWidth,
logoWidth, logoPadding: logo && label.length ? 3 : 0,
logoPadding: logo && label.length ? 3 : 0, color: toSvgColor(color),
color: toSvgColor(color), labelColor: toSvgColor(labelColor),
labelColor: toSvgColor(labelColor), minify: true,
}) })
)
} }

View File

@@ -4,18 +4,12 @@ const { test, given, forCases } = require('sazerac')
const { expect } = require('chai') const { expect } = require('chai')
const snapshot = require('snap-shot-it') const snapshot = require('snap-shot-it')
const isSvg = require('is-svg') const isSvg = require('is-svg')
const prettier = require('prettier')
const makeBadge = require('./make-badge') const makeBadge = require('./make-badge')
function expectBadgeToMatchSnapshot(format) {
snapshot(prettier.format(makeBadge(format), { parser: 'html' }))
}
function testColor(color = '', colorAttr = 'color') { function testColor(color = '', colorAttr = 'color') {
return JSON.parse( return JSON.parse(
makeBadge({ makeBadge({
label: 'name', text: ['name', 'Bob'],
message: 'Bob',
[colorAttr]: color, [colorAttr]: color,
format: 'json', format: 'json',
}) })
@@ -40,14 +34,10 @@ describe('The badge generator', function () {
]).expect('#abc123') ]).expect('#abc123')
// valid rgb(a) // valid rgb(a)
given('rgb(0,128,255)').expect('rgb(0,128,255)') given('rgb(0,128,255)').expect('rgb(0,128,255)')
given('rgb(220,128,255,0.5)').expect('rgb(220,128,255,0.5)')
given('rgba(0,0,255)').expect('rgba(0,0,255)')
given('rgba(0,128,255,0)').expect('rgba(0,128,255,0)') given('rgba(0,128,255,0)').expect('rgba(0,128,255,0)')
// valid hsl(a) // valid hsl(a)
given('hsl(100, 56%, 10%)').expect('hsl(100, 56%, 10%)') given('hsl(100, 56%, 10%)').expect('hsl(100, 56%, 10%)')
given('hsl(360,50%,50%,0.5)').expect('hsl(360,50%,50%,0.5)')
given('hsla(25,20%,0%,0.1)').expect('hsla(25,20%,0%,0.1)') given('hsla(25,20%,0%,0.1)').expect('hsla(25,20%,0%,0.1)')
given('hsla(0,50%,101%)').expect('hsla(0,50%,101%)')
// CSS named color. // CSS named color.
given('papayawhip').expect('papayawhip') given('papayawhip').expect('papayawhip')
// Shields named color. // Shields named color.
@@ -63,6 +53,12 @@ describe('The badge generator', function () {
// invalid hex // invalid hex
given('#123red'), // contains letter above F given('#123red'), // contains letter above F
given('#red'), // contains letter above F given('#red'), // contains letter above F
// invalid rgb(a)
given('rgb(220,128,255,0.5)'), // has alpha
given('rgba(0,0,255)'), // no alpha
// invalid hsl(a)
given('hsl(360,50%,50%,0.5)'), // has alpha
given('hsla(0,50%,101%)'), // no alpha
// neither a css named color nor colorscheme // neither a css named color nor colorscheme
given('notacolor'), given('notacolor'),
given('bluish'), given('bluish'),
@@ -81,26 +77,23 @@ describe('The badge generator', function () {
describe('SVG', function () { describe('SVG', function () {
it('should produce SVG', function () { it('should produce SVG', function () {
expect(makeBadge({ label: 'cactus', message: 'grown', format: 'svg' })) const svg = makeBadge({ text: ['cactus', 'grown'], format: 'svg' })
expect(svg)
.to.satisfy(isSvg) .to.satisfy(isSvg)
.and.to.include('cactus') .and.to.include('cactus')
.and.to.include('grown') .and.to.include('grown')
}) })
it('should match snapshot', function () { it('should match snapshot', function () {
expectBadgeToMatchSnapshot({ const svg = makeBadge({ text: ['cactus', 'grown'], format: 'svg' })
label: 'cactus', snapshot(svg)
message: 'grown',
format: 'svg',
})
}) })
}) })
describe('JSON', function () { describe('JSON', function () {
it('should produce the expected JSON', function () { it('should produce the expected JSON', function () {
const json = makeBadge({ const json = makeBadge({
label: 'cactus', text: ['cactus', 'grown'],
message: 'grown',
format: 'json', format: 'json',
links: ['https://example.com/', 'https://other.example.com/'], links: ['https://example.com/', 'https://other.example.com/'],
}) })
@@ -113,535 +106,484 @@ describe('The badge generator', function () {
}) })
}) })
it('should replace undefined svg badge style with "flat"', function () { it('should replace undefined svg template with "flat"', function () {
const jsonBadgeWithUnknownStyle = makeBadge({ const jsonBadgeWithUnknownStyle = makeBadge({
label: 'name', text: ['name', 'Bob'],
message: 'Bob',
format: 'svg', format: 'svg',
}) })
const jsonBadgeWithDefaultStyle = makeBadge({ const jsonBadgeWithDefaultStyle = makeBadge({
label: 'name', text: ['name', 'Bob'],
message: 'Bob',
format: 'svg', format: 'svg',
style: 'flat', template: 'flat',
}) })
expect(jsonBadgeWithUnknownStyle) expect(jsonBadgeWithUnknownStyle)
.to.equal(jsonBadgeWithDefaultStyle) .to.equal(jsonBadgeWithDefaultStyle)
.and.to.satisfy(isSvg) .and.to.satisfy(isSvg)
}) })
it('should fail with unknown svg badge style', function () { it('should fail with unknown svg template', function () {
expect(() => expect(() =>
makeBadge({ makeBadge({
label: 'name', text: ['name', 'Bob'],
message: 'Bob',
format: 'svg', format: 'svg',
style: 'unknown_style', template: 'unknown_style',
}) })
).to.throw(Error, "Unknown badge style: 'unknown_style'") ).to.throw(Error, "Unknown template: 'unknown_style'")
}) })
}) })
describe('"flat" template badge generation', function () { describe('"flat" template badge generation', function () {
it('should match snapshots: message/label, no logo', function () { it('should match snapshots: message/label, no logo', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: 'cactus', makeBadge({
message: 'grown', text: ['cactus', 'grown'],
format: 'svg', format: 'svg',
style: 'flat', template: 'flat',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
}) })
)
}) })
it('should match snapshots: message/label, with logo', function () { it('should match snapshots: message/label, with logo', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: 'cactus', makeBadge({
message: 'grown', text: ['cactus', 'grown'],
format: 'svg', format: 'svg',
style: 'flat', template: 'flat',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu', logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
}) })
)
}) })
it('should match snapshots: message only, no logo', function () { it('should match snapshots: message only, no logo', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: '', makeBadge({
message: 'grown', text: ['', 'grown'],
format: 'svg', format: 'svg',
style: 'flat', template: 'flat',
color: '#b3e', color: '#b3e',
}) })
)
}) })
it('should match snapshots: message only, with logo', function () { it('should match snapshots: message only, with logo', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: '', makeBadge({
message: 'grown', text: ['', 'grown'],
format: 'svg', format: 'svg',
style: 'flat', template: 'flat',
color: '#b3e', color: '#b3e',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu', logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
}) })
)
}) })
it('should match snapshots: message only, with logo and labelColor', function () { it('should match snapshots: message only, with logo and labelColor', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: '', makeBadge({
message: 'grown', text: ['', 'grown'],
format: 'svg', format: 'svg',
style: 'flat', template: 'flat',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu', logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
}) })
)
}) })
it('should match snapshots: message/label, with links', function () { it('should match snapshots: message/label, with links', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: 'cactus', makeBadge({
message: 'grown', text: ['cactus', 'grown'],
format: 'svg', format: 'svg',
style: 'flat', template: 'flat',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
links: ['https://shields.io/', 'https://www.google.co.uk/'], 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',
format: 'svg',
style: 'flat',
color: '#000',
labelColor: '#f3f3f3',
})
})
it('should match snapshots: black text when the message color is light', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
style: 'flat',
color: '#e2ffe1',
labelColor: '#000',
})
}) })
}) })
describe('"flat-square" template badge generation', function () { describe('"flat-square" template badge generation', function () {
it('should match snapshots: message/label, no logo', function () { it('should match snapshots: message/label, no logo', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: 'cactus', makeBadge({
message: 'grown', text: ['cactus', 'grown'],
format: 'svg', format: 'svg',
style: 'flat-square', template: 'flat-square',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
}) })
)
}) })
it('should match snapshots: message/label, with logo', function () { it('should match snapshots: message/label, with logo', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: 'cactus', makeBadge({
message: 'grown', text: ['cactus', 'grown'],
format: 'svg', format: 'svg',
style: 'flat-square', template: 'flat-square',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu', logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
}) })
)
}) })
it('should match snapshots: message only, no logo', function () { it('should match snapshots: message only, no logo', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: '', makeBadge({
message: 'grown', text: ['', 'grown'],
format: 'svg', format: 'svg',
style: 'flat-square', template: 'flat-square',
color: '#b3e', color: '#b3e',
}) })
)
}) })
it('should match snapshots: message only, with logo', function () { it('should match snapshots: message only, with logo', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: '', makeBadge({
message: 'grown', text: ['', 'grown'],
format: 'svg', format: 'svg',
style: 'flat-square', template: 'flat-square',
color: '#b3e', color: '#b3e',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu', logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
}) })
)
}) })
it('should match snapshots: message only, with logo and labelColor', function () { it('should match snapshots: message only, with logo and labelColor', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: '', makeBadge({
message: 'grown', text: ['', 'grown'],
format: 'svg', format: 'svg',
style: 'flat-square', template: 'flat-square',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu', logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
}) })
)
}) })
it('should match snapshots: message/label, with links', function () { it('should match snapshots: message/label, with links', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: 'cactus', makeBadge({
message: 'grown', text: ['cactus', 'grown'],
format: 'svg', format: 'svg',
style: 'flat-square', template: 'flat-square',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
links: ['https://shields.io/', 'https://www.google.co.uk/'], 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',
format: 'svg',
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',
format: 'svg',
style: 'flat-square',
color: '#e2ffe1',
labelColor: '#000',
})
}) })
}) })
describe('"plastic" template badge generation', function () { describe('"plastic" template badge generation', function () {
it('should match snapshots: message/label, no logo', function () { it('should match snapshots: message/label, no logo', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: 'cactus', makeBadge({
message: 'grown', text: ['cactus', 'grown'],
format: 'svg', format: 'svg',
style: 'plastic', template: 'plastic',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
}) })
)
}) })
it('should match snapshots: message/label, with logo', function () { it('should match snapshots: message/label, with logo', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: 'cactus', makeBadge({
message: 'grown', text: ['cactus', 'grown'],
format: 'svg', format: 'svg',
style: 'plastic', template: 'plastic',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu', logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
}) })
)
}) })
it('should match snapshots: message only, no logo', function () { it('should match snapshots: message only, no logo', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: '', makeBadge({
message: 'grown', text: ['', 'grown'],
format: 'svg', format: 'svg',
style: 'plastic', template: 'plastic',
color: '#b3e', color: '#b3e',
}) })
)
}) })
it('should match snapshots: message only, with logo', function () { it('should match snapshots: message only, with logo', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: '', makeBadge({
message: 'grown', text: ['', 'grown'],
format: 'svg', format: 'svg',
style: 'plastic', template: 'plastic',
color: '#b3e', color: '#b3e',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu', logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
}) })
)
}) })
it('should match snapshots: message only, with logo and labelColor', function () { it('should match snapshots: message only, with logo and labelColor', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: '', makeBadge({
message: 'grown', text: ['', 'grown'],
format: 'svg', format: 'svg',
style: 'plastic', template: 'plastic',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu', logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
}) })
)
}) })
it('should match snapshots: message/label, with links', function () { it('should match snapshots: message/label, with links', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: 'cactus', makeBadge({
message: 'grown', text: ['cactus', 'grown'],
format: 'svg', format: 'svg',
style: 'plastic', template: 'plastic',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
links: ['https://shields.io/', 'https://www.google.co.uk/'], 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',
format: 'svg',
style: 'plastic',
color: '#000',
labelColor: '#f3f3f3',
})
})
it('should match snapshots: black text when the message color is light', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
style: 'plastic',
color: '#e2ffe1',
labelColor: '#000',
})
}) })
}) })
describe('"for-the-badge" template badge generation', function () { describe('"for-the-badge" template badge generation', function () {
// https://github.com/badges/shields/issues/1280 // https://github.com/badges/shields/issues/1280
it('numbers should produce a string', function () { it('numbers should produce a string', function () {
expect( const svg = makeBadge({
makeBadge({ text: [1998, 1999],
label: 1998, format: 'svg',
message: 1999, template: 'for-the-badge',
format: 'svg', })
style: 'for-the-badge', expect(svg).to.include('1998').and.to.include('1999')
})
)
.to.include('1998')
.and.to.include('1999')
}) })
it('lowercase/mixedcase string should produce uppercase string', function () { it('lowercase/mixedcase string should produce uppercase string', function () {
expect( const svg = makeBadge({
makeBadge({ text: ['Label', '1 string'],
label: 'Label', format: 'svg',
message: '1 string', template: 'for-the-badge',
format: 'svg', })
style: 'for-the-badge', expect(svg).to.include('LABEL').and.to.include('1 STRING')
})
)
.to.include('LABEL')
.and.to.include('1 STRING')
}) })
it('should match snapshots: message/label, no logo', function () { it('should match snapshots: message/label, no logo', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: 'cactus', makeBadge({
message: 'grown', text: ['cactus', 'grown'],
format: 'svg', format: 'svg',
style: 'for-the-badge', template: 'for-the-badge',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
}) })
)
}) })
it('should match snapshots: message/label, with logo', function () { it('should match snapshots: message/label, with logo', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: 'cactus', makeBadge({
message: 'grown', text: ['cactus', 'grown'],
format: 'svg', format: 'svg',
style: 'for-the-badge', template: 'for-the-badge',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu', logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
}) })
)
}) })
it('should match snapshots: message only, no logo', function () { it('should match snapshots: message only, no logo', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: '', makeBadge({
message: 'grown', text: ['', 'grown'],
format: 'svg', format: 'svg',
style: 'for-the-badge', template: 'for-the-badge',
color: '#b3e', color: '#b3e',
}) })
)
}) })
it('should match snapshots: message only, with logo', function () { it('should match snapshots: message only, with logo', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: '', makeBadge({
message: 'grown', text: ['', 'grown'],
format: 'svg', format: 'svg',
style: 'for-the-badge', template: 'for-the-badge',
color: '#b3e', color: '#b3e',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu', logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
}) })
)
}) })
it('should match snapshots: message only, with logo and labelColor', function () { it('should match snapshots: message only, with logo and labelColor', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: '', makeBadge({
message: 'grown', text: ['', 'grown'],
format: 'svg', format: 'svg',
style: 'for-the-badge', template: 'for-the-badge',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu', logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
}) })
)
}) })
it('should match snapshots: message/label, with links', function () { it('should match snapshots: message/label, with links', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: 'cactus', makeBadge({
message: 'grown', text: ['cactus', 'grown'],
format: 'svg', format: 'svg',
style: 'for-the-badge', template: 'for-the-badge',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
links: ['https://shields.io/', 'https://www.google.co.uk/'], 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',
format: 'svg',
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',
format: 'svg',
style: 'for-the-badge',
color: '#e2ffe1',
labelColor: '#000',
})
}) })
}) })
describe('"social" template badge generation', function () { describe('"social" template badge generation', function () {
it('should produce capitalized string for badge key', function () { it('should produce capitalized string for badge key', function () {
expect( const svg = makeBadge({
makeBadge({ text: ['some-key', 'some-value'],
label: 'some-key', format: 'svg',
message: 'some-value', template: 'social',
format: 'svg', })
style: 'social', expect(svg).to.include('Some-key').and.to.include('some-value')
})
)
.to.include('Some-key')
.and.to.include('some-value')
}) })
// https://github.com/badges/shields/issues/1606 // https://github.com/badges/shields/issues/1606
it('should handle empty strings used as badge keys', function () { it('should handle empty strings used as badge keys', function () {
expect( const svg = makeBadge({
makeBadge({ text: ['', 'some-value'],
label: '', format: 'json',
message: 'some-value', template: 'social',
format: 'json', })
style: 'social', expect(svg).to.include('""').and.to.include('some-value')
})
)
.to.include('""')
.and.to.include('some-value')
}) })
it('should match snapshots: message/label, no logo', function () { it('should match snapshots: message/label, no logo', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: 'cactus', makeBadge({
message: 'grown', text: ['cactus', 'grown'],
format: 'svg', format: 'svg',
style: 'social', template: 'social',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
}) })
)
}) })
it('should match snapshots: message/label, with logo', function () { it('should match snapshots: message/label, with logo', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: 'cactus', makeBadge({
message: 'grown', text: ['cactus', 'grown'],
format: 'svg', format: 'svg',
style: 'social', template: 'social',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu', logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
}) })
)
}) })
it('should match snapshots: message only, no logo', function () { it('should match snapshots: message only, no logo', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: '', makeBadge({
message: 'grown', text: ['', 'grown'],
format: 'svg', format: 'svg',
style: 'social', template: 'social',
color: '#b3e', color: '#b3e',
}) })
)
}) })
it('should match snapshots: message only, with logo', function () { it('should match snapshots: message only, with logo', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: '', makeBadge({
message: 'grown', text: ['', 'grown'],
format: 'svg', format: 'svg',
style: 'social', template: 'social',
color: '#b3e', color: '#b3e',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu', logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
}) })
)
}) })
it('should match snapshots: message only, with logo and labelColor', function () { it('should match snapshots: message only, with logo and labelColor', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: '', makeBadge({
message: 'grown', text: ['', 'grown'],
format: 'svg', format: 'svg',
style: 'social', template: 'social',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu', logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
}) })
)
}) })
it('should match snapshots: message/label, with links', function () { it('should match snapshots: message/label, with links', function () {
expectBadgeToMatchSnapshot({ snapshot(
label: 'cactus', makeBadge({
message: 'grown', text: ['cactus', 'grown'],
format: 'svg', format: 'svg',
style: 'social', template: 'social',
color: '#b3e', color: '#b3e',
labelColor: '#0f0', labelColor: '#0f0',
links: ['https://shields.io/', 'https://www.google.co.uk/'], links: ['https://shields.io/', 'https://www.google.co.uk/'],
}) })
)
}) })
}) })
describe('badges with logos should always produce the same badge', function () { describe('badges with logos should always produce the same badge', function () {
it('badge with logo', function () { it('badge with logo', function () {
expectBadgeToMatchSnapshot({ const svg = makeBadge({
label: 'label', text: ['label', 'message'],
message: 'message',
format: 'svg', format: 'svg',
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu', logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
}) })
snapshot(svg)
})
})
describe('text colors', function () {
it('should use black text when the label color is light', function () {
snapshot(
makeBadge({
text: ['cactus', 'grown'],
format: 'svg',
template: 'flat',
color: '#000',
labelColor: '#f3f3f3',
})
)
})
it('should use black text when the message color is light', function () {
snapshot(
makeBadge({
text: ['cactus', 'grown'],
format: 'svg',
template: 'for-the-badge',
color: '#e2ffe1',
labelColor: '#000',
})
)
}) })
}) })
}) })

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

@@ -1,6 +1,6 @@
{ {
"name": "badge-maker", "name": "badge-maker",
"version": "3.3.1", "version": "3.2.0",
"description": "Shields.io badge library", "description": "Shields.io badge library",
"keywords": [ "keywords": [
"GitHub", "GitHub",
@@ -21,13 +21,13 @@
"bugs": { "bugs": {
"url": "https://github.com/badges/shields/issues" "url": "https://github.com/badges/shields/issues"
}, },
"homepage": "https://shields.io", "homepage": "http://shields.io",
"bin": { "bin": {
"badge": "lib/badge-cli.js" "badge": "lib/badge-cli.js"
}, },
"engines": { "engines": {
"node": ">= 14", "node": ">= 10",
"npm": ">= 6" "npm": ">= 5"
}, },
"collective": { "collective": {
"type": "opencollective", "type": "opencollective",
@@ -35,8 +35,8 @@
"logo": "https://opencollective.com/opencollective/logo.txt" "logo": "https://opencollective.com/opencollective/logo.txt"
}, },
"dependencies": { "dependencies": {
"anafanafo": "2.0.0", "anafanafo": "^1.0.0",
"css-color-converter": "^2.0.0" "css-color-converter": "^1.1.1"
}, },
"scripts": { "scripts": {
"test": "echo 'Run tests from parent dir'; false" "test": "echo 'Run tests from parent dir'; false"

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,6 @@ private:
discord_bot_token: ... discord_bot_token: ...
gh_client_id: ... gh_client_id: ...
gh_client_secret: ... gh_client_secret: ...
gitlab_token: ...
redis_url: ... redis_url: ...
sentry_dsn: ... sentry_dsn: ...
shields_secret: ... shields_secret: ...
@@ -11,6 +10,5 @@ private:
sl_insight_apiToken: ... sl_insight_apiToken: ...
twitch_client_id: ... twitch_client_id: ...
twitch_client_secret: ... twitch_client_secret: ...
weblate_api_key: ...
wheelmap_token: ... wheelmap_token: ...
youtube_api_key: ... youtube_api_key: ...

View File

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

View File

@@ -6,20 +6,19 @@ public:
enabled: true enabled: true
url: https://metrics.shields.io/telegraf url: https://metrics.shields.io/telegraf
instanceIdFrom: env-var instanceIdFrom: env-var
instanceIdEnvVarName: FLY_ALLOC_ID instanceIdEnvVarName: HEROKU_DYNO_ID
envLabel: shields-production envLabel: shields-production
ssl: ssl:
isSecure: false isSecure: true
cors: cors:
allowedOrigin: ['http://shields.io', 'https://shields.io'] allowedOrigin: ['http://shields.io', 'https://shields.io']
services: redirectUrl: 'https://shields.io/'
gitlab:
authorizedOrigins: 'https://gitlab.com'
rasterUrl: 'https://raster.shields.io' rasterUrl: 'https://raster.shields.io'
userAgentBase: 'Shields.io'
requireCloudflare: true private:
requestTimeoutSeconds: 20 # 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' address: 'localhost'
port: 1111 port: 1111
rateLimit: false
redirectUrl: 'http://frontend.example.test' redirectUrl: 'http://frontend.example.test'
rasterUrl: 'http://raster.example.test' rasterUrl: 'http://raster.example.test'

View File

@@ -14,6 +14,24 @@ export function badgeUrlFromPath({
longCache?: boolean longCache?: boolean
}): string }): string
export function badgeUrlFromPattern({
baseUrl,
pattern,
namedParams,
queryParams,
style,
format,
longCache,
}: {
baseUrl?: string
pattern: string
namedParams: { [k: string]: string }
queryParams: { [k: string]: string | number | boolean }
style?: string
format?: string
longCache?: boolean
}): string
export function encodeField(s: string): string export function encodeField(s: string): string
export function staticBadgeUrl({ export function staticBadgeUrl({

View File

@@ -1,6 +1,8 @@
// Avoid "Attempted import error: 'URL' is not exported from 'url'" in frontend. 'use strict'
import url from 'url'
import queryString from 'query-string' const { URL } = require('url')
const queryString = require('query-string')
const { compile } = require('path-to-regexp')
function badgeUrlFromPath({ function badgeUrlFromPath({
baseUrl = '', baseUrl = '',
@@ -22,6 +24,33 @@ function badgeUrlFromPath({
return `${baseUrl}${path}${outExt}${suffix}` return `${baseUrl}${path}${outExt}${suffix}`
} }
function badgeUrlFromPattern({
baseUrl = '',
pattern,
namedParams,
queryParams,
style,
format = '',
longCache = false,
}) {
const toPath = compile(pattern, {
strict: true,
sensitive: true,
encode: encodeURIComponent,
})
const path = toPath(namedParams)
return badgeUrlFromPath({
baseUrl,
path,
queryParams,
style,
format,
longCache,
})
}
function encodeField(s) { function encodeField(s) {
return encodeURIComponent(s.replace(/-/g, '--').replace(/_/g, '__')) return encodeURIComponent(s.replace(/-/g, '--').replace(/_/g, '__'))
} }
@@ -118,14 +147,15 @@ function dynamicBadgeUrl({
function rasterRedirectUrl({ rasterUrl }, badgeUrl) { function rasterRedirectUrl({ rasterUrl }, badgeUrl) {
// Ensure we're always using the `rasterUrl` by using just the path from // Ensure we're always using the `rasterUrl` by using just the path from
// the request URL. // the request URL.
const { pathname, search } = new url.URL(badgeUrl, 'https://bogus.test') const { pathname, search } = new URL(badgeUrl, 'https://bogus.test')
const result = new url.URL(pathname, rasterUrl) const result = new URL(pathname, rasterUrl)
result.search = search result.search = search
return result return result
} }
export { module.exports = {
badgeUrlFromPath, badgeUrlFromPath,
badgeUrlFromPattern,
encodeField, encodeField,
staticBadgeUrl, staticBadgeUrl,
queryStringStaticBadgeUrl, queryStringStaticBadgeUrl,

View File

@@ -1,11 +1,14 @@
import { test, given } from 'sazerac' 'use strict'
import {
const { test, given } = require('sazerac')
const {
badgeUrlFromPath, badgeUrlFromPath,
badgeUrlFromPattern,
encodeField, encodeField,
staticBadgeUrl, staticBadgeUrl,
queryStringStaticBadgeUrl, queryStringStaticBadgeUrl,
dynamicBadgeUrl, dynamicBadgeUrl,
} from './make-badge-url.js' } = require('./make-badge-url')
describe('Badge URL generation functions', function () { describe('Badge URL generation functions', function () {
test(badgeUrlFromPath, () => { test(badgeUrlFromPath, () => {
@@ -19,6 +22,18 @@ describe('Badge URL generation functions', function () {
) )
}) })
test(badgeUrlFromPattern, () => {
given({
baseUrl: 'http://example.com',
pattern: '/npm/v/:packageName',
namedParams: { packageName: 'gh-badges' },
style: 'flat-square',
longCache: true,
}).expect(
'http://example.com/npm/v/gh-badges?cacheSeconds=2592000&style=flat-square'
)
})
test(encodeField, () => { test(encodeField, () => {
given('foo').expect('foo') given('foo').expect('foo')
given('').expect('') given('').expect('')

View File

@@ -1,14 +1,21 @@
'use strict'
// Escapes `t` using the format specified in // Escapes `t` using the format specified in
// <https://github.com/espadrine/gh-badges/issues/12#issuecomment-31518129> // <https://github.com/espadrine/gh-badges/issues/12#issuecomment-31518129>
function escapeFormat(t) { function escapeFormat(t) {
return ( return (
t t
// Single underscore. // Inline single underscore.
.replace(/(^|[^_])((?:__)*)_(?!_)/g, '$1$2 ') .replace(/([^_])_([^_])/g, '$1 $2')
// Leading or trailing underscore.
.replace(/([^_])_$/, '$1 ')
.replace(/^_([^_])/, ' $1')
// Double underscore and double dash. // Double underscore and double dash.
.replace(/__/g, '_') .replace(/__/g, '_')
.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' 'use strict'
import { InvalidParameter } from './errors.js'
const { URL } = require('url')
const { InvalidParameter } = require('./errors')
class AuthHelper { class AuthHelper {
constructor( constructor(
@@ -74,7 +76,7 @@ class AuthHelper {
} }
static _isInsecureSslRequest({ options = {} }) { static _isInsecureSslRequest({ options = {} }) {
const strictSSL = options?.https?.rejectUnauthorized ?? true const { strictSSL = true } = options
return strictSSL !== true return strictSSL !== true
} }
@@ -107,10 +109,8 @@ class AuthHelper {
} }
get _basicAuth() { get _basicAuth() {
const { _user: username, _pass: password } = this const { _user: user, _pass: pass } = this
return this.isConfigured return this.isConfigured ? { user, pass } : undefined
? { username: username || '', password: password || '' }
: undefined
} }
/* /*
@@ -133,7 +133,7 @@ class AuthHelper {
const { options, ...rest } = requestParams const { options, ...rest } = requestParams
return { return {
options: { options: {
...auth, auth,
...options, ...options,
}, },
...rest, ...rest,
@@ -184,12 +184,12 @@ class AuthHelper {
static _mergeQueryParams(requestParams, query) { static _mergeQueryParams(requestParams, query) {
const { const {
options: { searchParams: existingQuery, ...restOptions } = {}, options: { qs: existingQuery, ...restOptions } = {},
...rest ...rest
} = requestParams } = requestParams
return { return {
options: { options: {
searchParams: { qs: {
...existingQuery, ...existingQuery,
...query, ...query,
}, },
@@ -209,4 +209,4 @@ class AuthHelper {
} }
} }
export { AuthHelper } module.exports = { AuthHelper }

View File

@@ -1,7 +1,9 @@
import { expect } from 'chai' 'use strict'
import { test, given, forCases } from 'sazerac'
import { AuthHelper } from './auth-helper.js' const { expect } = require('chai')
import { InvalidParameter } from './errors.js' const { test, given, forCases } = require('sazerac')
const { AuthHelper } = require('./auth-helper')
const { InvalidParameter } = require('./errors')
describe('AuthHelper', function () { describe('AuthHelper', function () {
describe('constructor checks', function () { describe('constructor checks', function () {
@@ -104,14 +106,14 @@ describe('AuthHelper', function () {
{ userKey: 'myci_user', passKey: 'myci_pass' }, { userKey: 'myci_user', passKey: 'myci_pass' },
{ myci_user: 'admin', myci_pass: 'abc123' } { myci_user: 'admin', myci_pass: 'abc123' }
), ),
]).expect({ username: 'admin', password: 'abc123' }) ]).expect({ user: 'admin', pass: 'abc123' })
given({ userKey: 'myci_user' }, { myci_user: 'admin' }).expect({ given({ userKey: 'myci_user' }, { myci_user: 'admin' }).expect({
username: 'admin', user: 'admin',
password: '', pass: undefined,
}) })
given({ passKey: 'myci_pass' }, { myci_pass: 'abc123' }).expect({ given({ passKey: 'myci_pass' }, { myci_pass: 'abc123' }).expect({
username: '', user: undefined,
password: 'abc123', pass: 'abc123',
}) })
given({ userKey: 'myci_user', passKey: 'myci_pass' }, {}).expect( given({ userKey: 'myci_user', passKey: 'myci_pass' }, {}).expect(
undefined undefined
@@ -120,8 +122,8 @@ describe('AuthHelper', function () {
{ passKey: 'myci_pass', defaultToEmptyStringForUser: true }, { passKey: 'myci_pass', defaultToEmptyStringForUser: true },
{ myci_pass: 'abc123' } { myci_pass: 'abc123' }
).expect({ ).expect({
username: '', user: '',
password: 'abc123', pass: 'abc123',
}) })
}) })
}) })
@@ -131,18 +133,15 @@ describe('AuthHelper', function () {
forCases([ forCases([
given({ url: 'http://example.test' }), given({ url: 'http://example.test' }),
given({ url: 'http://example.test', options: {} }), given({ url: 'http://example.test', options: {} }),
given({ url: 'http://example.test', options: { strictSSL: true } }),
given({ given({
url: 'http://example.test', url: 'http://example.test',
options: { https: { rejectUnauthorized: true } }, options: { strictSSL: undefined },
}),
given({
url: 'http://example.test',
options: { https: { rejectUnauthorized: undefined } },
}), }),
]).expect(false) ]).expect(false)
given({ given({
url: 'http://example.test', url: 'http://example.test',
options: { https: { rejectUnauthorized: false } }, options: { strictSSL: false },
}).expect(true) }).expect(true)
}) })
}) })
@@ -166,9 +165,7 @@ describe('AuthHelper', function () {
}) })
it('throws for insecure requests', function () { it('throws for insecure requests', function () {
expect(() => expect(() =>
authHelper.enforceStrictSsl({ authHelper.enforceStrictSsl({ options: { strictSSL: false } })
options: { https: { rejectUnauthorized: false } },
})
).to.throw(InvalidParameter) ).to.throw(InvalidParameter)
}) })
}) })
@@ -190,9 +187,7 @@ describe('AuthHelper', function () {
}) })
it('does not throw for insecure requests', function () { it('does not throw for insecure requests', function () {
expect(() => expect(() =>
authHelper.enforceStrictSsl({ authHelper.enforceStrictSsl({ options: { strictSSL: false } })
options: { https: { rejectUnauthorized: false } },
})
).not.to.throw() ).not.to.throw()
}) })
}) })
@@ -227,7 +222,7 @@ describe('AuthHelper', function () {
test(shouldAuthenticateRequest, () => { test(shouldAuthenticateRequest, () => {
given({ given({
url: 'https://myci.test/api', url: 'https://myci.test/api',
options: { https: { rejectUnauthorized: false } }, options: { strictSSL: false },
}).expect(false) }).expect(false)
}) })
}) })
@@ -265,7 +260,7 @@ describe('AuthHelper', function () {
test(shouldAuthenticateRequest, () => { test(shouldAuthenticateRequest, () => {
given({ given({
url: 'https://myci.test', url: 'https://myci.test',
options: { https: { rejectUnauthorized: false } }, options: { strictSSL: false },
}).expect(true) }).expect(true)
}) })
}) })
@@ -330,8 +325,7 @@ describe('AuthHelper', function () {
}).expect({ }).expect({
url: 'https://myci.test/api', url: 'https://myci.test/api',
options: { options: {
username: 'admin', auth: { user: 'admin', pass: 'abc123' },
password: 'abc123',
}, },
}) })
given({ given({
@@ -343,8 +337,7 @@ describe('AuthHelper', function () {
url: 'https://myci.test/api', url: 'https://myci.test/api',
options: { options: {
headers: { Accept: 'application/json' }, headers: { Accept: 'application/json' },
username: 'admin', auth: { user: 'admin', pass: 'abc123' },
password: 'abc123',
}, },
}) })
}) })
@@ -375,7 +368,7 @@ describe('AuthHelper', function () {
expect(() => expect(() =>
withBasicAuth({ withBasicAuth({
url: 'https://myci.test/api', url: 'https://myci.test/api',
options: { https: { rejectUnauthorized: false } }, options: { strictSSL: false },
}) })
).to.throw(InvalidParameter) ).to.throw(InvalidParameter)
}) })

View File

@@ -2,10 +2,12 @@
* @module * @module
*/ */
import { print } from 'graphql/language/printer.js' 'use strict'
import BaseService from './base.js'
import { InvalidResponse, ShieldsRuntimeError } from './errors.js' const { print } = require('graphql/language/printer')
import { parseJson } from './json.js' const BaseService = require('./base')
const { InvalidResponse, ShieldsRuntimeError } = require('./errors')
const { parseJson } = require('./json')
function defaultTransformErrors(errors) { function defaultTransformErrors(errors) {
return new InvalidResponse({ prettyMessage: errors[0].message }) return new InvalidResponse({ prettyMessage: errors[0].message })
@@ -38,8 +40,8 @@ class BaseGraphqlService extends BaseService {
* representing the query clause of GraphQL POST body * representing the query clause of GraphQL POST body
* e.g. gql`{ query { ... } }` * e.g. gql`{ query { ... } }`
* @param {object} attrs.variables Variables clause of GraphQL POST body * @param {object} attrs.variables Variables clause of GraphQL POST body
* @param {object} [attrs.options={}] Options to pass to got. See * @param {object} [attrs.options={}] Options to pass to request. See
* [documentation](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md) * [documentation](https://github.com/request/request#requestoptions-callback)
* @param {object} [attrs.httpErrorMessages={}] Key-value map of HTTP status codes * @param {object} [attrs.httpErrorMessages={}] Key-value map of HTTP status codes
* and custom error messages e.g: `{ 404: 'package not found' }`. * and custom error messages e.g: `{ 404: 'package not found' }`.
* This can be used to extend or override the * This can be used to extend or override the
@@ -53,7 +55,7 @@ class BaseGraphqlService extends BaseService {
* The default is to return the first entry of the `errors` array as * The default is to return the first entry of the `errors` array as
* an InvalidResponse. * an InvalidResponse.
* @returns {object} Parsed response * @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({ async _requestGraphql({
schema, schema,
@@ -91,4 +93,4 @@ class BaseGraphqlService extends BaseService {
} }
} }
export default BaseGraphqlService module.exports = BaseGraphqlService

View File

@@ -1,9 +1,11 @@
import Joi from 'joi' 'use strict'
import { expect } from 'chai'
import gql from 'graphql-tag' const Joi = require('@hapi/joi')
import sinon from 'sinon' const { expect } = require('chai')
import BaseGraphqlService from './base-graphql.js' const gql = require('graphql-tag')
import { InvalidResponse } from './errors.js' const sinon = require('sinon')
const BaseGraphqlService = require('./base-graphql')
const { InvalidResponse } = require('./errors')
const dummySchema = Joi.object({ const dummySchema = Joi.object({
requiredString: Joi.string().required(), requiredString: Joi.string().required(),
@@ -29,9 +31,9 @@ class DummyGraphqlService extends BaseGraphqlService {
describe('BaseGraphqlService', function () { describe('BaseGraphqlService', function () {
describe('Making requests', function () { describe('Making requests', function () {
let requestFetcher let sendAndCacheRequest
beforeEach(function () { beforeEach(function () {
requestFetcher = sinon.stub().returns( sendAndCacheRequest = sinon.stub().returns(
Promise.resolve({ Promise.resolve({
buffer: '{"some": "json"}', buffer: '{"some": "json"}',
res: { statusCode: 200 }, res: { statusCode: 200 },
@@ -39,13 +41,13 @@ describe('BaseGraphqlService', function () {
) )
}) })
it('invokes _requestFetcher', async function () { it('invokes _sendAndCacheRequest', async function () {
await DummyGraphqlService.invoke( await DummyGraphqlService.invoke(
{ requestFetcher }, { sendAndCacheRequest },
{ handleInternalErrors: false } { handleInternalErrors: false }
) )
expect(requestFetcher).to.have.been.calledOnceWith( expect(sendAndCacheRequest).to.have.been.calledOnceWith(
'http://example.com/graphql', 'http://example.com/graphql',
{ {
body: '{"query":"{\\n requiredString\\n}\\n","variables":{}}', body: '{"query":"{\\n requiredString\\n}\\n","variables":{}}',
@@ -55,7 +57,7 @@ describe('BaseGraphqlService', function () {
) )
}) })
it('forwards options to _requestFetcher', async function () { it('forwards options to _sendAndCacheRequest', async function () {
class WithOptions extends DummyGraphqlService { class WithOptions extends DummyGraphqlService {
async handle() { async handle() {
const { value } = await this._requestGraphql({ const { value } = await this._requestGraphql({
@@ -66,24 +68,24 @@ describe('BaseGraphqlService', function () {
requiredString requiredString
} }
`, `,
options: { searchParams: { queryParam: 123 } }, options: { qs: { queryParam: 123 } },
}) })
return { message: value } return { message: value }
} }
} }
await WithOptions.invoke( await WithOptions.invoke(
{ requestFetcher }, { sendAndCacheRequest },
{ handleInternalErrors: false } { handleInternalErrors: false }
) )
expect(requestFetcher).to.have.been.calledOnceWith( expect(sendAndCacheRequest).to.have.been.calledOnceWith(
'http://example.com/graphql', 'http://example.com/graphql',
{ {
body: '{"query":"{\\n requiredString\\n}\\n","variables":{}}', body: '{"query":"{\\n requiredString\\n}\\n","variables":{}}',
headers: { Accept: 'application/json' }, headers: { Accept: 'application/json' },
method: 'POST', method: 'POST',
searchParams: { queryParam: 123 }, qs: { queryParam: 123 },
} }
) )
}) })
@@ -91,13 +93,13 @@ describe('BaseGraphqlService', function () {
describe('Making badges', function () { describe('Making badges', function () {
it('handles valid json responses', async function () { it('handles valid json responses', async function () {
const requestFetcher = async () => ({ const sendAndCacheRequest = async () => ({
buffer: '{"requiredString": "some-string"}', buffer: '{"requiredString": "some-string"}',
res: { statusCode: 200 }, res: { statusCode: 200 },
}) })
expect( expect(
await DummyGraphqlService.invoke( await DummyGraphqlService.invoke(
{ requestFetcher }, { sendAndCacheRequest },
{ handleInternalErrors: false } { handleInternalErrors: false }
) )
).to.deep.equal({ ).to.deep.equal({
@@ -106,13 +108,13 @@ describe('BaseGraphqlService', function () {
}) })
it('handles json responses which do not match the schema', async function () { it('handles json responses which do not match the schema', async function () {
const requestFetcher = async () => ({ const sendAndCacheRequest = async () => ({
buffer: '{"unexpectedKey": "some-string"}', buffer: '{"unexpectedKey": "some-string"}',
res: { statusCode: 200 }, res: { statusCode: 200 },
}) })
expect( expect(
await DummyGraphqlService.invoke( await DummyGraphqlService.invoke(
{ requestFetcher }, { sendAndCacheRequest },
{ handleInternalErrors: false } { handleInternalErrors: false }
) )
).to.deep.equal({ ).to.deep.equal({
@@ -123,13 +125,13 @@ describe('BaseGraphqlService', function () {
}) })
it('handles unparseable json responses', async function () { it('handles unparseable json responses', async function () {
const requestFetcher = async () => ({ const sendAndCacheRequest = async () => ({
buffer: 'not json', buffer: 'not json',
res: { statusCode: 200 }, res: { statusCode: 200 },
}) })
expect( expect(
await DummyGraphqlService.invoke( await DummyGraphqlService.invoke(
{ requestFetcher }, { sendAndCacheRequest },
{ handleInternalErrors: false } { handleInternalErrors: false }
) )
).to.deep.equal({ ).to.deep.equal({
@@ -142,13 +144,13 @@ describe('BaseGraphqlService', function () {
describe('Error handling', function () { describe('Error handling', function () {
it('handles generic error', async function () { it('handles generic error', async function () {
const requestFetcher = async () => ({ const sendAndCacheRequest = async () => ({
buffer: '{ "errors": [ { "message": "oh noes!!" } ] }', buffer: '{ "errors": [ { "message": "oh noes!!" } ] }',
res: { statusCode: 200 }, res: { statusCode: 200 },
}) })
expect( expect(
await DummyGraphqlService.invoke( await DummyGraphqlService.invoke(
{ requestFetcher }, { sendAndCacheRequest },
{ handleInternalErrors: false } { handleInternalErrors: false }
) )
).to.deep.equal({ ).to.deep.equal({
@@ -181,13 +183,13 @@ describe('BaseGraphqlService', function () {
} }
} }
const requestFetcher = async () => ({ const sendAndCacheRequest = async () => ({
buffer: '{ "errors": [ { "message": "oh noes!!" } ] }', buffer: '{ "errors": [ { "message": "oh noes!!" } ] }',
res: { statusCode: 200 }, res: { statusCode: 200 },
}) })
expect( expect(
await WithErrorHandler.invoke( await WithErrorHandler.invoke(
{ requestFetcher }, { sendAndCacheRequest },
{ handleInternalErrors: false } { handleInternalErrors: false }
) )
).to.deep.equal({ ).to.deep.equal({

View File

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

View File

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

View File

@@ -0,0 +1,74 @@
'use strict'
const makeBadge = require('../../badge-maker/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,15 +1,17 @@
import makeBadge from '../../badge-maker/lib/make-badge.js' 'use strict'
import BaseService from './base.js'
import { const makeBadge = require('../../badge-maker/lib/make-badge')
const BaseService = require('./base')
const {
serverHasBeenUpSinceResourceCached, serverHasBeenUpSinceResourceCached,
setCacheHeadersForStaticResource, setCacheHeadersForStaticResource,
} from './cache-headers.js' } = require('./cache-headers')
import { makeSend } from './legacy-result-sender.js' const { makeSend } = require('./legacy-result-sender')
import { MetricHelper } from './metric-helper.js' const { MetricHelper } = require('./metric-helper')
import coalesceBadge from './coalesce-badge.js' const coalesceBadge = require('./coalesce-badge')
import { prepareRoute, namedParamsForMatch } from './route.js' const { prepareRoute, namedParamsForMatch } = require('./route')
export default class BaseStaticService extends BaseService { module.exports = class BaseStaticService extends BaseService {
static register({ camp, metricInstance }, serviceConfig) { static register({ camp, metricInstance }, serviceConfig) {
const { regex, captureNames } = prepareRoute(this.route) const { regex, captureNames } = prepareRoute(this.route)

View File

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

View File

@@ -1,8 +1,14 @@
import { expect } from 'chai' 'use strict'
import sinon from 'sinon'
import Joi from 'joi' const { expect } = require('chai')
import makeBadge from '../../badge-maker/lib/make-badge.js' const sinon = require('sinon')
import BaseSvgScrapingService from './base-svg-scraping.js' const Joi = require('@hapi/joi')
const makeBadge = require('../../badge-maker/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({ const schema = Joi.object({
message: Joi.string().required(), message: Joi.string().required(),
@@ -23,7 +29,10 @@ class DummySvgScrapingService extends BaseSvgScrapingService {
describe('BaseSvgScrapingService', function () { describe('BaseSvgScrapingService', function () {
const exampleLabel = 'this is the label' const exampleLabel = 'this is the label'
const exampleMessage = 'this is the result!' const exampleMessage = 'this is the result!'
const exampleSvg = makeBadge({ label: exampleLabel, message: exampleMessage }) const exampleSvg = makeExampleSvg({
label: exampleLabel,
message: exampleMessage,
})
describe('valueFromSvgBadge', function () { describe('valueFromSvgBadge', function () {
it('should find the correct value', function () { it('should find the correct value', function () {
@@ -34,9 +43,9 @@ describe('BaseSvgScrapingService', function () {
}) })
describe('Making requests', function () { describe('Making requests', function () {
let requestFetcher let sendAndCacheRequest
beforeEach(function () { beforeEach(function () {
requestFetcher = sinon.stub().returns( sendAndCacheRequest = sinon.stub().returns(
Promise.resolve({ Promise.resolve({
buffer: exampleSvg, buffer: exampleSvg,
res: { statusCode: 200 }, res: { statusCode: 200 },
@@ -44,13 +53,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( await DummySvgScrapingService.invoke(
{ requestFetcher }, { sendAndCacheRequest },
{ handleInternalErrors: false } { handleInternalErrors: false }
) )
expect(requestFetcher).to.have.been.calledOnceWith( expect(sendAndCacheRequest).to.have.been.calledOnceWith(
'http://example.com/foo.svg', 'http://example.com/foo.svg',
{ {
headers: { Accept: 'image/svg+xml' }, headers: { Accept: 'image/svg+xml' },
@@ -58,7 +67,7 @@ describe('BaseSvgScrapingService', function () {
) )
}) })
it('forwards options to _requestFetcher', async function () { it('forwards options to _sendAndCacheRequest', async function () {
class WithCustomOptions extends DummySvgScrapingService { class WithCustomOptions extends DummySvgScrapingService {
async handle() { async handle() {
const { message } = await this._requestSvg({ const { message } = await this._requestSvg({
@@ -66,7 +75,7 @@ describe('BaseSvgScrapingService', function () {
url: 'http://example.com/foo.svg', url: 'http://example.com/foo.svg',
options: { options: {
method: 'POST', method: 'POST',
searchParams: { queryParam: 123 }, qs: { queryParam: 123 },
}, },
}) })
return { message } return { message }
@@ -74,16 +83,16 @@ describe('BaseSvgScrapingService', function () {
} }
await WithCustomOptions.invoke( await WithCustomOptions.invoke(
{ requestFetcher }, { sendAndCacheRequest },
{ handleInternalErrors: false } { handleInternalErrors: false }
) )
expect(requestFetcher).to.have.been.calledOnceWith( expect(sendAndCacheRequest).to.have.been.calledOnceWith(
'http://example.com/foo.svg', 'http://example.com/foo.svg',
{ {
method: 'POST', method: 'POST',
headers: { Accept: 'image/svg+xml' }, headers: { Accept: 'image/svg+xml' },
searchParams: { queryParam: 123 }, qs: { queryParam: 123 },
} }
) )
}) })
@@ -91,13 +100,13 @@ describe('BaseSvgScrapingService', function () {
describe('Making badges', function () { describe('Making badges', function () {
it('handles valid svg responses', async function () { it('handles valid svg responses', async function () {
const requestFetcher = async () => ({ const sendAndCacheRequest = async () => ({
buffer: exampleSvg, buffer: exampleSvg,
res: { statusCode: 200 }, res: { statusCode: 200 },
}) })
expect( expect(
await DummySvgScrapingService.invoke( await DummySvgScrapingService.invoke(
{ requestFetcher }, { sendAndCacheRequest },
{ handleInternalErrors: false } { handleInternalErrors: false }
) )
).to.deep.equal({ ).to.deep.equal({
@@ -117,13 +126,13 @@ describe('BaseSvgScrapingService', function () {
}) })
} }
} }
const requestFetcher = async () => ({ const sendAndCacheRequest = async () => ({
buffer: '<desc>a different message</desc>', buffer: '<desc>a different message</desc>',
res: { statusCode: 200 }, res: { statusCode: 200 },
}) })
expect( expect(
await WithValueMatcher.invoke( await WithValueMatcher.invoke(
{ requestFetcher }, { sendAndCacheRequest },
{ handleInternalErrors: false } { handleInternalErrors: false }
) )
).to.deep.equal({ ).to.deep.equal({
@@ -132,13 +141,13 @@ describe('BaseSvgScrapingService', function () {
}) })
it('handles unparseable svg responses', async function () { it('handles unparseable svg responses', async function () {
const requestFetcher = async () => ({ const sendAndCacheRequest = async () => ({
buffer: 'not svg yo', buffer: 'not svg yo',
res: { statusCode: 200 }, res: { statusCode: 200 },
}) })
expect( expect(
await DummySvgScrapingService.invoke( await DummySvgScrapingService.invoke(
{ requestFetcher }, { sendAndCacheRequest },
{ handleInternalErrors: false } { handleInternalErrors: false }
) )
).to.deep.equal({ ).to.deep.equal({

View File

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

View File

@@ -1,7 +1,9 @@
import Joi from 'joi' 'use strict'
import { expect } from 'chai'
import sinon from 'sinon' const Joi = require('@hapi/joi')
import BaseXmlService from './base-xml.js' const { expect } = require('chai')
const sinon = require('sinon')
const BaseXmlService = require('./base-xml')
const dummySchema = Joi.object({ const dummySchema = Joi.object({
requiredString: Joi.string().required(), requiredString: Joi.string().required(),
@@ -22,9 +24,9 @@ class DummyXmlService extends BaseXmlService {
describe('BaseXmlService', function () { describe('BaseXmlService', function () {
describe('Making requests', function () { describe('Making requests', function () {
let requestFetcher let sendAndCacheRequest
beforeEach(function () { beforeEach(function () {
requestFetcher = sinon.stub().returns( sendAndCacheRequest = sinon.stub().returns(
Promise.resolve({ Promise.resolve({
buffer: '<requiredString>some-string</requiredString>', buffer: '<requiredString>some-string</requiredString>',
res: { statusCode: 200 }, res: { statusCode: 200 },
@@ -32,13 +34,13 @@ describe('BaseXmlService', function () {
) )
}) })
it('invokes _requestFetcher', async function () { it('invokes _sendAndCacheRequest', async function () {
await DummyXmlService.invoke( await DummyXmlService.invoke(
{ requestFetcher }, { sendAndCacheRequest },
{ handleInternalErrors: false } { handleInternalErrors: false }
) )
expect(requestFetcher).to.have.been.calledOnceWith( expect(sendAndCacheRequest).to.have.been.calledOnceWith(
'http://example.com/foo.xml', 'http://example.com/foo.xml',
{ {
headers: { Accept: 'application/xml, text/xml' }, headers: { Accept: 'application/xml, text/xml' },
@@ -46,7 +48,7 @@ describe('BaseXmlService', function () {
) )
}) })
it('forwards options to _requestFetcher', async function () { it('forwards options to _sendAndCacheRequest', async function () {
class WithCustomOptions extends BaseXmlService { class WithCustomOptions extends BaseXmlService {
static route = {} static route = {}
@@ -54,23 +56,23 @@ describe('BaseXmlService', function () {
const { requiredString } = await this._requestXml({ const { requiredString } = await this._requestXml({
schema: dummySchema, schema: dummySchema,
url: 'http://example.com/foo.xml', url: 'http://example.com/foo.xml',
options: { method: 'POST', searchParams: { queryParam: 123 } }, options: { method: 'POST', qs: { queryParam: 123 } },
}) })
return { message: requiredString } return { message: requiredString }
} }
} }
await WithCustomOptions.invoke( await WithCustomOptions.invoke(
{ requestFetcher }, { sendAndCacheRequest },
{ handleInternalErrors: false } { handleInternalErrors: false }
) )
expect(requestFetcher).to.have.been.calledOnceWith( expect(sendAndCacheRequest).to.have.been.calledOnceWith(
'http://example.com/foo.xml', 'http://example.com/foo.xml',
{ {
headers: { Accept: 'application/xml, text/xml' }, headers: { Accept: 'application/xml, text/xml' },
method: 'POST', method: 'POST',
searchParams: { queryParam: 123 }, qs: { queryParam: 123 },
} }
) )
}) })
@@ -78,13 +80,13 @@ describe('BaseXmlService', function () {
describe('Making badges', function () { describe('Making badges', function () {
it('handles valid xml responses', async function () { it('handles valid xml responses', async function () {
const requestFetcher = async () => ({ const sendAndCacheRequest = async () => ({
buffer: '<requiredString>some-string</requiredString>', buffer: '<requiredString>some-string</requiredString>',
res: { statusCode: 200 }, res: { statusCode: 200 },
}) })
expect( expect(
await DummyXmlService.invoke( await DummyXmlService.invoke(
{ requestFetcher }, { sendAndCacheRequest },
{ handleInternalErrors: false } { handleInternalErrors: false }
) )
).to.deep.equal({ ).to.deep.equal({
@@ -104,14 +106,14 @@ describe('BaseXmlService', function () {
return { message: requiredString } return { message: requiredString }
} }
} }
const requestFetcher = async () => ({ const sendAndCacheRequest = async () => ({
buffer: buffer:
'<requiredString>some-string with trailing whitespace </requiredString>', '<requiredString>some-string with trailing whitespace </requiredString>',
res: { statusCode: 200 }, res: { statusCode: 200 },
}) })
expect( expect(
await DummyXmlServiceWithParserOption.invoke( await DummyXmlServiceWithParserOption.invoke(
{ requestFetcher }, { sendAndCacheRequest },
{ handleInternalErrors: false } { handleInternalErrors: false }
) )
).to.deep.equal({ ).to.deep.equal({
@@ -120,13 +122,13 @@ describe('BaseXmlService', function () {
}) })
it('handles xml responses which do not match the schema', async function () { it('handles xml responses which do not match the schema', async function () {
const requestFetcher = async () => ({ const sendAndCacheRequest = async () => ({
buffer: '<unexpectedAttribute>some-string</unexpectedAttribute>', buffer: '<unexpectedAttribute>some-string</unexpectedAttribute>',
res: { statusCode: 200 }, res: { statusCode: 200 },
}) })
expect( expect(
await DummyXmlService.invoke( await DummyXmlService.invoke(
{ requestFetcher }, { sendAndCacheRequest },
{ handleInternalErrors: false } { handleInternalErrors: false }
) )
).to.deep.equal({ ).to.deep.equal({
@@ -137,13 +139,13 @@ describe('BaseXmlService', function () {
}) })
it('handles unparseable xml responses', async function () { it('handles unparseable xml responses', async function () {
const requestFetcher = async () => ({ const sendAndCacheRequest = async () => ({
buffer: 'not xml', buffer: 'not xml',
res: { statusCode: 200 }, res: { statusCode: 200 },
}) })
expect( expect(
await DummyXmlService.invoke( await DummyXmlService.invoke(
{ requestFetcher }, { sendAndCacheRequest },
{ handleInternalErrors: false } { handleInternalErrors: false }
) )
).to.deep.equal({ ).to.deep.equal({

View File

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

View File

@@ -1,7 +1,9 @@
import Joi from 'joi' 'use strict'
import { expect } from 'chai'
import sinon from 'sinon' const Joi = require('@hapi/joi')
import BaseYamlService from './base-yaml.js' const { expect } = require('chai')
const sinon = require('sinon')
const BaseYamlService = require('./base-yaml')
const dummySchema = Joi.object({ const dummySchema = Joi.object({
requiredString: Joi.string().required(), requiredString: Joi.string().required(),
@@ -38,9 +40,9 @@ foo: baz
describe('BaseYamlService', function () { describe('BaseYamlService', function () {
describe('Making requests', function () { describe('Making requests', function () {
let requestFetcher let sendAndCacheRequest
beforeEach(function () { beforeEach(function () {
requestFetcher = sinon.stub().returns( sendAndCacheRequest = sinon.stub().returns(
Promise.resolve({ Promise.resolve({
buffer: expectedYaml, buffer: expectedYaml,
res: { statusCode: 200 }, res: { statusCode: 200 },
@@ -48,13 +50,13 @@ describe('BaseYamlService', function () {
) )
}) })
it('invokes _requestFetcher', async function () { it('invokes _sendAndCacheRequest', async function () {
await DummyYamlService.invoke( await DummyYamlService.invoke(
{ requestFetcher }, { sendAndCacheRequest },
{ handleInternalErrors: false } { handleInternalErrors: false }
) )
expect(requestFetcher).to.have.been.calledOnceWith( expect(sendAndCacheRequest).to.have.been.calledOnceWith(
'http://example.com/foo.yaml', 'http://example.com/foo.yaml',
{ {
headers: { headers: {
@@ -65,24 +67,24 @@ describe('BaseYamlService', function () {
) )
}) })
it('forwards options to _requestFetcher', async function () { it('forwards options to _sendAndCacheRequest', async function () {
class WithOptions extends DummyYamlService { class WithOptions extends DummyYamlService {
async handle() { async handle() {
const { requiredString } = await this._requestYaml({ const { requiredString } = await this._requestYaml({
schema: dummySchema, schema: dummySchema,
url: 'http://example.com/foo.yaml', url: 'http://example.com/foo.yaml',
options: { method: 'POST', searchParams: { queryParam: 123 } }, options: { method: 'POST', qs: { queryParam: 123 } },
}) })
return { message: requiredString } return { message: requiredString }
} }
} }
await WithOptions.invoke( await WithOptions.invoke(
{ requestFetcher }, { sendAndCacheRequest },
{ handleInternalErrors: false } { handleInternalErrors: false }
) )
expect(requestFetcher).to.have.been.calledOnceWith( expect(sendAndCacheRequest).to.have.been.calledOnceWith(
'http://example.com/foo.yaml', 'http://example.com/foo.yaml',
{ {
headers: { headers: {
@@ -90,7 +92,7 @@ describe('BaseYamlService', function () {
'text/x-yaml, text/yaml, application/x-yaml, application/yaml, text/plain', 'text/x-yaml, text/yaml, application/x-yaml, application/yaml, text/plain',
}, },
method: 'POST', method: 'POST',
searchParams: { queryParam: 123 }, qs: { queryParam: 123 },
} }
) )
}) })
@@ -98,13 +100,13 @@ describe('BaseYamlService', function () {
describe('Making badges', function () { describe('Making badges', function () {
it('handles valid yaml responses', async function () { it('handles valid yaml responses', async function () {
const requestFetcher = async () => ({ const sendAndCacheRequest = async () => ({
buffer: expectedYaml, buffer: expectedYaml,
res: { statusCode: 200 }, res: { statusCode: 200 },
}) })
expect( expect(
await DummyYamlService.invoke( await DummyYamlService.invoke(
{ requestFetcher }, { sendAndCacheRequest },
{ handleInternalErrors: false } { handleInternalErrors: false }
) )
).to.deep.equal({ ).to.deep.equal({
@@ -113,13 +115,13 @@ describe('BaseYamlService', function () {
}) })
it('handles yaml responses which do not match the schema', async function () { it('handles yaml responses which do not match the schema', async function () {
const requestFetcher = async () => ({ const sendAndCacheRequest = async () => ({
buffer: unexpectedYaml, buffer: unexpectedYaml,
res: { statusCode: 200 }, res: { statusCode: 200 },
}) })
expect( expect(
await DummyYamlService.invoke( await DummyYamlService.invoke(
{ requestFetcher }, { sendAndCacheRequest },
{ handleInternalErrors: false } { handleInternalErrors: false }
) )
).to.deep.equal({ ).to.deep.equal({
@@ -130,13 +132,13 @@ describe('BaseYamlService', function () {
}) })
it('handles unparseable yaml responses', async function () { it('handles unparseable yaml responses', async function () {
const requestFetcher = async () => ({ const sendAndCacheRequest = async () => ({
buffer: invalidYaml, buffer: invalidYaml,
res: { statusCode: 200 }, res: { statusCode: 200 },
}) })
expect( expect(
await DummyYamlService.invoke( await DummyYamlService.invoke(
{ requestFetcher }, { sendAndCacheRequest },
{ handleInternalErrors: false } { handleInternalErrors: false }
) )
).to.deep.equal({ ).to.deep.equal({

View File

@@ -1,36 +1,36 @@
'use strict'
/** /**
* @module * @module
*/ */
// See available emoji at http://emoji.muan.co/ // See available emoji at http://emoji.muan.co/
import emojic from 'emojic' const emojic = require('emojic')
import Joi from 'joi' const Joi = require('@hapi/joi')
import log from '../server/log.js' const log = require('../server/log')
import { AuthHelper } from './auth-helper.js' const { AuthHelper } = require('./auth-helper')
import { MetricHelper, MetricNames } from './metric-helper.js' const { MetricHelper, MetricNames } = require('./metric-helper')
import { assertValidCategory } from './categories.js' const { assertValidCategory } = require('./categories')
import checkErrorResponse from './check-error-response.js' const checkErrorResponse = require('./check-error-response')
import coalesceBadge from './coalesce-badge.js' const coalesceBadge = require('./coalesce-badge')
import { const {
NotFound, NotFound,
InvalidResponse, InvalidResponse,
Inaccessible, Inaccessible,
ImproperlyConfigured, ImproperlyConfigured,
InvalidParameter, InvalidParameter,
Deprecated, Deprecated,
} from './errors.js' } = require('./errors')
import { validateExample, transformExample } from './examples.js' const { validateExample, transformExample } = require('./examples')
import { fetch } from './got.js' const {
import {
makeFullUrl, makeFullUrl,
assertValidRoute, assertValidRoute,
prepareRoute, prepareRoute,
namedParamsForMatch, namedParamsForMatch,
getQueryParamNames, getQueryParamNames,
} from './route.js' } = require('./route')
import { assertValidServiceDefinition } from './service-definitions.js' const { assertValidServiceDefinition } = require('./service-definitions')
import trace from './trace.js' const trace = require('./trace')
import validate from './validate.js' const validate = require('./validate')
const defaultBadgeDataSchema = Joi.object({ const defaultBadgeDataSchema = Joi.object({
label: Joi.string(), label: Joi.string(),
@@ -108,14 +108,11 @@ class BaseService {
* *
* See also the config schema in `./server.js` and `doc/server-secrets.md`. * 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 * To use the configured auth in the handler or fetch method, pass the
* _request() input params in a call to one of: * credentials to the request. For example:
* - this.authHelper.withBasicAuth() * - `{ options: { auth: this.authHelper.basicAuth } }`
* - this.authHelper.withBearerAuthHeader() * - `{ options: { headers: this.authHelper.bearerAuthHeader } }`
* - this.authHelper.withQueryStringAuth() * - `{ options: { qs: { token: this.authHelper._pass } } }`
*
* For example:
* this._request(this.authHelper.withBasicAuth({ url, schema, options }))
* *
* @abstract * @abstract
* @type {module:core/base-service/base~Auth} * @type {module:core/base-service/base~Auth}
@@ -146,9 +143,6 @@ class BaseService {
license: 3600, license: 3600,
version: 300, version: 300,
debug: 60, debug: 60,
downloads: 900,
rating: 900,
social: 900,
} }
return cacheLengths[this.category] return cacheLengths[this.category]
} }
@@ -208,10 +202,10 @@ class BaseService {
} }
constructor( constructor(
{ requestFetcher, authHelper, metricHelper }, { sendAndCacheRequest, authHelper, metricHelper },
{ handleInternalErrors } { handleInternalErrors }
) { ) {
this._requestFetcher = requestFetcher this._requestFetcher = sendAndCacheRequest
this.authHelper = authHelper this.authHelper = authHelper
this._handleInternalErrors = handleInternalErrors this._handleInternalErrors = handleInternalErrors
this._metricHelper = metricHelper this._metricHelper = metricHelper
@@ -219,24 +213,7 @@ class BaseService {
async _request({ url, options = {}, errorMessages = {} }) { async _request({ url, options = {}, errorMessages = {} }) {
const logTrace = (...args) => trace.logTrace('fetch', ...args) const logTrace = (...args) => trace.logTrace('fetch', ...args)
let logUrl = url logTrace(emojic.bowAndArrow, 'Request', url, '\n', options)
const logOptions = Object.assign({}, options)
if ('searchParams' in options && options.searchParams != null) {
const params = new URLSearchParams(
Object.fromEntries(
Object.entries(options.searchParams).filter(
([k, v]) => v !== undefined
)
)
)
logUrl = `${url}?${params.toString()}`
delete logOptions.searchParams
}
logTrace(
emojic.bowAndArrow,
'Request',
`${logUrl}\n${JSON.stringify(logOptions, null, 2)}`
)
const { res, buffer } = await this._requestFetcher(url, options) const { res, buffer } = await this._requestFetcher(url, options)
await this._meterResponse(res, buffer) await this._meterResponse(res, buffer)
logTrace(emojic.dart, 'Response status code', res.statusCode) logTrace(emojic.dart, 'Response status code', res.statusCode)
@@ -285,7 +262,7 @@ class BaseService {
/** /**
* Asynchronous function to handle requests for this service. Take the route * Asynchronous function to handle requests for this service. Take the route
* parameters (as defined in the `route` property), perform a request using * 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 * @abstract
* @param {object} namedParams Params parsed from route pattern * @param {object} namedParams Params parsed from route pattern
@@ -430,16 +407,10 @@ class BaseService {
} }
static register( static register(
{ { camp, handleRequest, githubApiProvider, metricInstance },
camp,
handleRequest,
githubApiProvider,
librariesIoApiProvider,
metricInstance,
},
serviceConfig serviceConfig
) { ) {
const { cacheHeaders: cacheHeaderConfig } = serviceConfig const { cacheHeaders: cacheHeaderConfig, fetchLimitBytes } = serviceConfig
const { regex, captureNames } = prepareRoute(this.route) const { regex, captureNames } = prepareRoute(this.route)
const queryParams = getQueryParamNames(this.route) const queryParams = getQueryParamNames(this.route)
@@ -452,15 +423,15 @@ class BaseService {
regex, regex,
handleRequest(cacheHeaderConfig, { handleRequest(cacheHeaderConfig, {
queryParams, queryParams,
handler: async (queryParams, match, sendBadge) => { handler: async (queryParams, match, sendBadge, request) => {
const metricHandle = metricHelper.startRequest() const metricHandle = metricHelper.startRequest()
const namedParams = namedParamsForMatch(captureNames, match, this) const namedParams = namedParamsForMatch(captureNames, match, this)
const serviceData = await this.invoke( const serviceData = await this.invoke(
{ {
requestFetcher: fetch, sendAndCacheRequest: request.asPromise,
sendAndCacheRequestWithCallbacks: request,
githubApiProvider, githubApiProvider,
librariesIoApiProvider,
metricHelper, metricHelper,
}, },
serviceConfig, serviceConfig,
@@ -481,6 +452,7 @@ class BaseService {
metricHandle.noteResponseSent() metricHandle.noteResponseSent()
}, },
cacheLength: this._cacheLength, cacheLength: this._cacheLength,
fetchLimitBytes,
}) })
) )
} }
@@ -580,4 +552,4 @@ class BaseService {
* An HTML string that is included in the badge popup. * An HTML string that is included in the badge popup.
*/ */
export default BaseService module.exports = BaseService

View File

@@ -1,22 +1,23 @@
import Joi from 'joi' 'use strict'
import chai from 'chai'
import sinon from 'sinon' const Joi = require('@hapi/joi')
import prometheus from 'prom-client' const chai = require('chai')
import chaiAsPromised from 'chai-as-promised' const { expect } = chai
import PrometheusMetrics from '../server/prometheus-metrics.js' const sinon = require('sinon')
import trace from './trace.js' const prometheus = require('prom-client')
import { const PrometheusMetrics = require('../server/prometheus-metrics')
const trace = require('./trace')
const {
NotFound, NotFound,
Inaccessible, Inaccessible,
InvalidResponse, InvalidResponse,
InvalidParameter, InvalidParameter,
Deprecated, Deprecated,
} from './errors.js' } = require('./errors')
import BaseService from './base.js' const BaseService = require('./base')
import { MetricHelper, MetricNames } from './metric-helper.js' const { MetricHelper, MetricNames } = require('./metric-helper')
import '../register-chai-plugins.spec.js' require('../register-chai-plugins.spec')
const { expect } = chai chai.use(require('chai-as-promised'))
chai.use(chaiAsPromised)
const queryParamSchema = Joi.object({ const queryParamSchema = Joi.object({
queryParamA: Joi.string(), queryParamA: Joi.string(),
@@ -124,11 +125,15 @@ describe('BaseService', function () {
}) })
describe('Logging', function () { describe('Logging', function () {
let sandbox
beforeEach(function () { beforeEach(function () {
sinon.stub(trace, 'logTrace') sandbox = sinon.createSandbox()
}) })
afterEach(function () { afterEach(function () {
sinon.restore() sandbox.restore()
})
beforeEach(function () {
sandbox.stub(trace, 'logTrace')
}) })
it('Invokes the logger as expected', async function () { it('Invokes the logger as expected', async function () {
await DummyService.invoke( await DummyService.invoke(
@@ -324,7 +329,7 @@ describe('BaseService', function () {
describe('ScoutCamp integration', function () { describe('ScoutCamp integration', function () {
// TODO Strangly, without the useless escape the regexes do not match in Node 12. // TODO Strangly, without the useless escape the regexes do not match in Node 12.
// eslint-disable-next-line no-useless-escape // eslint-disable-next-line no-useless-escape
const expectedRouteRegex = /^\/foo(?:\/([^\/#\?]+?))(|\.svg|\.json)$/ const expectedRouteRegex = /^\/foo\/([^\/]+?)(|\.svg|\.json)$/
let mockCamp let mockCamp
let mockHandleRequest let mockHandleRequest
@@ -348,8 +353,10 @@ describe('BaseService', function () {
it('handles the request', async function () { it('handles the request', async function () {
expect(mockHandleRequest).to.have.been.calledOnce expect(mockHandleRequest).to.have.been.calledOnce
const { queryParams: serviceQueryParams, handler: requestHandler } = const {
mockHandleRequest.getCall(0).args[1] queryParams: serviceQueryParams,
handler: requestHandler,
} = mockHandleRequest.getCall(0).args[1]
expect(serviceQueryParams).to.deep.equal([ expect(serviceQueryParams).to.deep.equal([
'queryParamA', 'queryParamA',
'legacyQueryParamA', 'legacyQueryParamA',
@@ -366,10 +373,9 @@ describe('BaseService', function () {
const expectedFormat = 'svg' const expectedFormat = 'svg'
expect(mockSendBadge).to.have.been.calledOnce expect(mockSendBadge).to.have.been.calledOnce
expect(mockSendBadge).to.have.been.calledWith(expectedFormat, { expect(mockSendBadge).to.have.been.calledWith(expectedFormat, {
label: 'cat', text: ['cat', 'Hello namedParamA: bar with queryParamA: ?'],
message: 'Hello namedParamA: bar with queryParamA: ?',
color: 'lightgrey', color: 'lightgrey',
style: 'flat', template: 'flat',
namedLogo: undefined, namedLogo: undefined,
logo: undefined, logo: undefined,
logoWidth: undefined, logoWidth: undefined,
@@ -383,8 +389,13 @@ describe('BaseService', function () {
describe('getDefinition', function () { describe('getDefinition', function () {
it('returns the expected result', function () { it('returns the expected result', function () {
const { category, name, isDeprecated, route, examples } = const {
DummyService.getDefinition() category,
name,
isDeprecated,
route,
examples,
} = DummyService.getDefinition()
expect({ expect({
category, category,
name, name,
@@ -422,39 +433,38 @@ describe('BaseService', function () {
}) })
describe('request', function () { describe('request', function () {
let sandbox
beforeEach(function () { beforeEach(function () {
sinon.stub(trace, 'logTrace') sandbox = sinon.createSandbox()
}) })
afterEach(function () { afterEach(function () {
sinon.restore() sandbox.restore()
})
beforeEach(function () {
sandbox.stub(trace, 'logTrace')
}) })
it('logs appropriate information', async function () { it('logs appropriate information', async function () {
const requestFetcher = async () => ({ const sendAndCacheRequest = async () => ({
buffer: '', buffer: '',
res: { statusCode: 200 }, res: { statusCode: 200 },
}) })
const serviceInstance = new DummyService( const serviceInstance = new DummyService(
{ requestFetcher }, { sendAndCacheRequest },
defaultConfig defaultConfig
) )
const url = 'some-url' const url = 'some-url'
const options = { const options = { headers: { Cookie: 'some-cookie' } }
headers: { Cookie: 'some-cookie' },
searchParams: { param1: 'foobar', param2: undefined },
}
await serviceInstance._request({ url, options }) await serviceInstance._request({ url, options })
expect(trace.logTrace).to.be.calledWithMatch( expect(trace.logTrace).to.be.calledWithMatch(
'fetch', 'fetch',
sinon.match.string, sinon.match.string,
'Request', 'Request',
`${url}?param1=foobar\n${JSON.stringify( url,
{ headers: options.headers }, '\n',
null, options
2
)}`
) )
expect(trace.logTrace).to.be.calledWithMatch( expect(trace.logTrace).to.be.calledWithMatch(
'fetch', 'fetch',
@@ -465,12 +475,12 @@ describe('BaseService', function () {
}) })
it('handles errors', async function () { it('handles errors', async function () {
const requestFetcher = async () => ({ const sendAndCacheRequest = async () => ({
buffer: '', buffer: '',
res: { statusCode: 404 }, res: { statusCode: 404 },
}) })
const serviceInstance = new DummyService( const serviceInstance = new DummyService(
{ requestFetcher }, { sendAndCacheRequest },
defaultConfig defaultConfig
) )
@@ -497,19 +507,18 @@ describe('BaseService', function () {
metricInstance: new PrometheusMetrics({ register }), metricInstance: new PrometheusMetrics({ register }),
ServiceClass: DummyServiceWithServiceResponseSizeMetricEnabled, ServiceClass: DummyServiceWithServiceResponseSizeMetricEnabled,
}) })
const requestFetcher = async () => ({ const sendAndCacheRequest = async () => ({
buffer: 'x'.repeat(65536 + 1), buffer: 'x'.repeat(65536 + 1),
res: { statusCode: 200 }, res: { statusCode: 200 },
}) })
const serviceInstance = const serviceInstance = new DummyServiceWithServiceResponseSizeMetricEnabled(
new DummyServiceWithServiceResponseSizeMetricEnabled( { sendAndCacheRequest, metricHelper },
{ requestFetcher, metricHelper }, defaultConfig
defaultConfig )
)
await serviceInstance._request({ url }) await serviceInstance._request({ url })
expect(await register.getSingleMetricAsString('service_response_bytes')) expect(register.getSingleMetricAsString('service_response_bytes'))
.to.contain( .to.contain(
'service_response_bytes_bucket{le="65536",category="other",family="undefined",service="dummy_service_with_service_response_size_metric_enabled"} 0\n' 'service_response_bytes_bucket{le="65536",category="other",family="undefined",service="dummy_service_with_service_response_size_metric_enabled"} 0\n'
) )
@@ -523,19 +532,19 @@ describe('BaseService', function () {
metricInstance: new PrometheusMetrics({ register }), metricInstance: new PrometheusMetrics({ register }),
ServiceClass: DummyService, ServiceClass: DummyService,
}) })
const requestFetcher = async () => ({ const sendAndCacheRequest = async () => ({
buffer: 'x', buffer: 'x',
res: { statusCode: 200 }, res: { statusCode: 200 },
}) })
const serviceInstance = new DummyService( const serviceInstance = new DummyService(
{ requestFetcher, metricHelper }, { sendAndCacheRequest, metricHelper },
defaultConfig defaultConfig
) )
await serviceInstance._request({ url }) await serviceInstance._request({ url })
expect( expect(
await register.getSingleMetricAsString('service_response_bytes') register.getSingleMetricAsString('service_response_bytes')
).to.not.contain('service_response_bytes_bucket') ).to.not.contain('service_response_bytes_bucket')
}) })
}) })

View File

@@ -1,6 +1,8 @@
import assert from 'assert' 'use strict'
import Joi from 'joi'
import coalesce from './coalesce.js' const assert = require('assert')
const Joi = require('@hapi/joi')
const coalesce = require('./coalesce')
const serverStartTimeGMTString = new Date().toGMTString() const serverStartTimeGMTString = new Date().toGMTString()
const serverStartTimestamp = Date.now() const serverStartTimestamp = Date.now()
@@ -102,7 +104,7 @@ function serverHasBeenUpSinceResourceCached(req) {
) )
} }
export { module.exports = {
coalesceCacheLength, coalesceCacheLength,
setCacheHeaders, setCacheHeaders,
setHeadersForCacheLength, setHeadersForCacheLength,

View File

@@ -1,16 +1,19 @@
import { test, given } from 'sazerac' 'use strict'
import chai, { expect } from 'chai'
import sinon from 'sinon' const { test, given } = require('sazerac')
import httpMocks from 'node-mocks-http' const chai = require('chai')
import chaiDatetime from 'chai-datetime' const { expect } = require('chai')
import { const sinon = require('sinon')
const httpMocks = require('node-mocks-http')
const {
coalesceCacheLength, coalesceCacheLength,
setHeadersForCacheLength, setHeadersForCacheLength,
setCacheHeaders, setCacheHeaders,
setCacheHeadersForStaticResource, setCacheHeadersForStaticResource,
serverHasBeenUpSinceResourceCached, serverHasBeenUpSinceResourceCached,
} from './cache-headers.js' } = require('./cache-headers')
chai.use(chaiDatetime)
chai.use(require('chai-datetime'))
describe('Cache header functions', function () { describe('Cache header functions', function () {
let res let res
@@ -99,11 +102,14 @@ describe('Cache header functions', function () {
}) })
describe('setHeadersForCacheLength', function () { describe('setHeadersForCacheLength', function () {
let sandbox
beforeEach(function () { beforeEach(function () {
sinon.useFakeTimers() sandbox = sinon.createSandbox()
sandbox.useFakeTimers()
}) })
afterEach(function () { afterEach(function () {
sinon.restore() sandbox.restore()
sandbox = undefined
}) })
it('should set the correct Date header', function () { it('should set the correct Date header', function () {

View File

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

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