Compare commits

..

1 Commits

Author SHA1 Message Date
Paul Melnikow
3dc4857747 When token pool is empty, surface the error to the badge consumer
Should provide better debuggability in self-hosting cases like #4862 but also on the production server.
2020-04-06 13:16:31 -04:00
1710 changed files with 68809 additions and 94991 deletions

382
.circleci/config.yml Normal file
View File

@@ -0,0 +1,382 @@
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
steps:
- checkout
- run:
name: Install dependencies
command: npm ci
environment:
CYPRESS_INSTALL_BINARY: 0
- run:
name: Identify services tagged in the PR title
command: npm run test:services:pr:prepare
- run:
name: Run tests for tagged services
environment:
mocha_reporter: mocha-junit-reporter
MOCHA_FILE: junit/services/results.xml
command: RETRY_COUNT=3 npm run test:services:pr:run
- store_test_results:
path: junit
run_package_tests: &run_package_tests
when: always
command: |
# https://discuss.circleci.com/t/switch-nodejs-version-on-machine-executor-solved/26675/3
set +e
export NVM_DIR="/opt/circleci/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
nvm install $NODE_VERSION
nvm use $NODE_VERSION
node --version
npm run test:package
package_steps: &package_steps
steps:
- checkout
- run:
name: Install dependencies
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
npm ci
environment:
CYPRESS_INSTALL_BINARY: 0
# Run the package tests on each currently supported node version. See:
# https://github.com/badges/shields/blob/master/gh-badges/README.md#node-version-support
- run:
<<: *run_package_tests
environment:
mocha_reporter: mocha-junit-reporter
MOCHA_FILE: junit/gh-badges/v8/results.xml
NODE_VERSION: v8
name: Run package tests on Node 8
- run:
<<: *run_package_tests
environment:
mocha_reporter: mocha-junit-reporter
MOCHA_FILE: junit/gh-badges/v10/results.xml
NODE_VERSION: v10
name: Run package tests on Node 10
- run:
<<: *run_package_tests
environment:
mocha_reporter: mocha-junit-reporter
MOCHA_FILE: junit/gh-badges/v12/results.xml
NODE_VERSION: v12
name: Run package tests on Node 12
- store_test_results:
path: junit
jobs:
npm-install:
docker:
- image: circleci/node:8
steps:
- checkout
- run:
name: Install dependencies
command: npm ci
environment:
CYPRESS_INSTALL_BINARY: 0
main:
docker:
- image: circleci/node:8
<<: *main_steps
main@node-latest:
docker:
- image: circleci/node:latest
<<: *main_steps
integration:
docker:
- image: circleci/node:8
- image: redis
<<: *integration_steps
integration@node-latest:
docker:
- image: circleci/node:latest
- image: redis
<<: *integration_steps
danger:
docker:
- image: circleci/node:8
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:8
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:
docker:
- image: circleci/node:8
<<: *services_steps
services@node-latest:
docker:
- image: circleci/node:latest
<<: *services_steps
e2e:
docker:
- image: cypress/base:8
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:
version: 2
on-commit:
jobs:
- main:
filters:
branches:
ignore: gh-pages
- main@node-latest:
filters:
branches:
ignore: gh-pages
- integration@node-latest:
filters:
branches:
ignore: gh-pages
- frontend:
filters:
branches:
ignore: gh-pages
- package:
filters:
branches:
ignore: gh-pages
- services:
filters:
branches:
ignore:
- master
- gh-pages
- services@node-latest:
filters:
branches:
ignore:
- master
- gh-pages
- danger:
filters:
branches:
ignore:
- master
- gh-pages
- /dependabot\/.*/
- e2e:
filters:
branches:
ignore: gh-pages
# on-commit-with-cache:
# jobs:
# - npm-install:
# filters:
# branches:
# ignore: gh-pages
# - main:
# requires:
# - npm-install
# - main@node-latest:
# requires:
# - npm-install
# - frontend:
# requires:
# - npm-install
# - services:
# requires:
# - npm-install
# filters:
# branches:
# ignore: master
# - services@node-latest:
# requires:
# - npm-install
# filters:
# branches:
# ignore: master
# - danger:
# requires:
# - npm-install
# filters:
# branches:
# ignore: /dependabot\/.*/

103
.dependabot/config.yml Normal file
View File

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

View File

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

6
.eslintignore Normal file
View File

@@ -0,0 +1,6 @@
/api-docs/
/build
/coverage
/__snapshots__
/public
gh-badges/node_modules/

177
.eslintrc.yml Normal file
View File

@@ -0,0 +1,177 @@
extends:
- standard
- standard-react
- plugin:@typescript-eslint/recommended
- prettier
- prettier/@typescript-eslint
- prettier/standard
- prettier/react
- eslint:recommended
parserOptions:
# Override eslint-config-standard, which incorrectly sets this to "module",
# though that setting is only for ES6 modules, not CommonJS modules.
sourceType: 'script'
settings:
react:
version: '16.8'
plugins:
- chai-friendly
- jsdoc
- mocha
- no-extension-in-require
- sort-class-members
- import
- react-hooks
- promise
overrides:
# For simplicity's sake, when possible prefer to add rules to the top-level
# list of rules, even if they only apply to certain files. That way the
# rules listed here are only ones which conflict.
- files:
- '**/*.js'
- '!frontend/**/*.js'
env:
node: true
es6: true
rules:
no-console: 'off'
- files:
- '**/*.@(ts|tsx)'
parserOptions:
sourceType: 'module'
parser: '@typescript-eslint/parser'
rules:
# Argh.
'@typescript-eslint/explicit-function-return-type':
['error', { 'allowExpressions': true }]
'@typescript-eslint/no-empty-function': 'error'
'@typescript-eslint/no-var-requires': 'error'
'@typescript-eslint/no-object-literal-type-assertion': 'off'
'@typescript-eslint/no-explicit-any': 'error'
'@typescript-eslint/ban-ts-ignore': 'off'
- files:
- core/**/*.ts
parserOptions:
sourceType: 'module'
parser: '@typescript-eslint/parser'
- files:
- gatsby-browser.js
- 'frontend/**/*.@(js|ts|tsx)'
parserOptions:
sourceType: 'module'
env:
browser: true
rules:
import/extensions:
['error', 'never', { 'json': 'always', 'yml': 'always' }]
- files:
- 'core/base-service/**/*.js'
- 'services/**/*.js'
rules:
sort-class-members/sort-class-members:
[
'error',
{
order:
[
'name',
'category',
'isDeprecated',
'route',
'auth',
'examples',
'_cacheLength',
'defaultBadgeData',
'render',
'constructor',
'fetch',
'transform',
'handle',
],
},
]
- files:
- '**/*.spec.@(js|ts|tsx)'
- '**/*.integration.js'
- '**/test-helpers.js'
- 'core/service-test-runner/**/*.js'
env:
mocha: true
rules:
mocha/no-exclusive-tests: 'error'
mocha/no-mocha-arrows: 'error'
mocha/prefer-arrow-callback: 'error'
rules:
# Disable some rules from eslint:recommended.
no-empty: ['error', { 'allowEmptyCatch': true }]
# Allow unused parameters. In callbacks, removing them seems to obscure
# what the functions are doing.
'@typescript-eslint/no-unused-vars': ['error', { 'args': 'none' }]
no-unused-vars: 'off'
'@typescript-eslint/no-var-requires': 'off'
# These should be disabled by eslint-config-prettier, but are not.
no-extra-semi: 'off'
# Shields additions.
no-var: 'error'
prefer-const: 'error'
strict: 'error'
arrow-body-style: ['error', 'as-needed']
no-extension-in-require/main: 'error'
object-shorthand: ['error', 'properties']
prefer-template: 'error'
promise/prefer-await-to-then: 'error'
func-style: ['error', 'declaration', { 'allowArrowFunctions': true }]
new-cap: ['error', { 'capIsNew': true }]
import/order: ['error', { 'newlines-between': 'never' }]
# Chai friendly.
no-unused-expressions: 'off'
chai-friendly/no-unused-expressions: 'error'
# jsdoc plugin:
# don't require every class/function to have a docblock
jsdoc/require-jsdoc: 'off'
# allow Joi as an undefined type
jsdoc/no-undefined-types: ['error', { definedTypes: ['Joi'] }]
# all the other reccomended rules as errors (not warnings)
jsdoc/check-alignment: 'error'
jsdoc/check-param-names: 'error'
jsdoc/check-tag-names: 'error'
jsdoc/check-types: 'error'
jsdoc/implements-on-classes: 'error'
jsdoc/newline-after-description: 'error'
jsdoc/require-param: 'error'
jsdoc/require-param-description: 'error'
jsdoc/require-param-name: 'error'
jsdoc/require-param-type: 'error'
jsdoc/require-returns: 'error'
jsdoc/require-returns-check: 'error'
jsdoc/require-returns-description: 'error'
jsdoc/require-returns-type: 'error'
jsdoc/valid-types: 'error'
# Disable some from TypeScript.
'@typescript-eslint/camelcase': off
'@typescript-eslint/explicit-function-return-type': 'off'
'@typescript-eslint/no-empty-function': 'off'
react/jsx-sort-props: 'error'
react-hooks/rules-of-hooks: 'error'
react-hooks/exhaustive-deps: 'error'
jsx-quotes: ['error', 'prefer-double']

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

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

View File

@@ -1,44 +0,0 @@
name: '🐛 Bug Report'
description: Report errors and problems
labels: [question]
body:
- type: dropdown
id: product
attributes:
label: Are you experiencing an issue with...
options:
- shields.io
- My own instance of shields
- badge-maker NPM package
validations:
required: true
- type: textarea
id: description
attributes:
label: '🐞 Description'
description: A clear and concise description of the problem.
validations:
required: true
- type: textarea
id: link
attributes:
label: '🔗 Link to the badge'
description: If you are reporting a problem with a specific badge on shields.io, provide a link to a badge demonstrating the error
validations:
required: false
- type: textarea
id: possible-solution
attributes:
label: '💡 Possible Solution'
description: 'Optional: only if you have suggestions on a fix/reason for the bug'
validations:
required: false
- type: markdown
attributes:
value: |
## :heart: Love Shields?
Please consider donating to sustain our activities: [https://opencollective.com/shields](https://opencollective.com/shields)

View File

@@ -18,7 +18,11 @@ labels: 'keep-service-tests-green'
<!-- Indicate whether or not the live badge is working. -->
:lady_beetle: **Stack trace**
:link: **CircleCI link**
<!-- Provide a link to the failing test in CircleCI. -->
:beetle: **Stack trace**
```
<!-- Provide the complete stack trace from the CircleCI test summary. -->
@@ -28,5 +32,5 @@ labels: 'keep-service-tests-green'
<!--- Optional: only if you have suggestions on a fix/reason for the bug -->
<!-- Love Shields? Please consider donating to sustain our activities:
<!-- Love Shields? Please consider donating $10 to sustain our activities:
👉 https://opencollective.com/shields -->

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://shields.io/docs/static-badges).
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/badges/static-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 to sustain our activities: [https://opencollective.com/shields](https://opencollective.com/shields)

View File

@@ -7,5 +7,5 @@ about: Ideas for other new features or improvements
<!-- A clear and concise description of the new feature. -->
<!-- Love Shields? Please consider donating to sustain our activities:
<!-- Love Shields? Please consider donating $10 to sustain our activities:
👉 https://opencollective.com/shields -->

View File

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

View File

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

View File

@@ -1,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,12 +0,0 @@
name: 'docusaurus-theme-openapi swizzled component changes warning'
description: 'Check for changes in docusaurus-theme-openapi components which are swizzled and prints out a warning'
branding:
icon: 'alert-triangle'
color: 'yellow'
inputs:
github-token:
description: 'The GITHUB_TOKEN secret'
required: true
runs:
using: 'node20'
main: 'index.js'

View File

@@ -1,105 +0,0 @@
'use strict'
/**
* Returns info about all files changed in a PR (max 3000 results)
*
* @param {object} client hydrated octokit ready to use for GitHub Actions
* @param {string} owner repo owner
* @param {string} repo repo name
* @param {number} pullNumber pull request number
* @returns {object[]} array of object that describe pr changed files - see https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#list-pull-requests-files
*/
async function getAllFilesForPullRequest(client, owner, repo, pullNumber) {
const perPage = 100 // Max number of items per page
let page = 1 // Start with the first page
let allFiles = []
while (true) {
const response = await client.rest.pulls.listFiles({
owner,
repo,
pull_number: pullNumber,
per_page: perPage,
page,
})
if (response.data.length === 0) {
// Break the loop if no more results
break
}
allFiles = allFiles.concat(response.data)
page++ // Move to the next page
}
return allFiles
}
/**
* Get a list of files changed betwen two tags for a github repo
*
* @param {object} client hydrated octokit ready to use for GitHub Actions
* @param {string} owner repo owner
* @param {string} repo repo name
* @param {string} baseTag base tag
* @param {string} headTag head tag
* @returns {string[]} Array listing all changed files betwen the base tag and the head tag
*/
async function getChangedFilesBetweenTags(
client,
owner,
repo,
baseTag,
headTag,
) {
const response = await client.rest.repos.compareCommits({
owner,
repo,
base: baseTag,
head: headTag,
})
return response.data.files.map(file => file.filename)
}
function findKeyEndingWith(obj, ending) {
for (const key in obj) {
if (key.endsWith(ending)) {
return key
}
}
}
/**
* Get large (>1MB) JSON file from git repo on at ref as a json object
*
* @param {object} client Hydrated octokit ready to use for GitHub Actions
* @param {string} owner Repo owner
* @param {string} repo Repo name
* @param {string} path Path of the file in repo relative to root directory
* @param {string} ref Git refrence (commit, branch, tag)
* @returns {string[]} Array listing all changed files betwen the base tag and the head tag
*/
async function getLargeJsonAtRef(client, owner, repo, path, ref) {
const fileSha = (
await client.rest.repos.getContent({
owner,
repo,
path,
ref,
})
).data.sha
const fileBlob = (
await client.rest.git.getBlob({
owner,
repo,
file_sha: fileSha,
})
).data.content
return JSON.parse(Buffer.from(fileBlob, 'base64').toString())
}
module.exports = {
getAllFilesForPullRequest,
getChangedFilesBetweenTags,
findKeyEndingWith,
getLargeJsonAtRef,
}

View File

@@ -1,148 +0,0 @@
'use strict'
const core = require('@actions/core')
const github = require('@actions/github')
const {
getAllFilesForPullRequest,
getChangedFilesBetweenTags,
findKeyEndingWith,
getLargeJsonAtRef,
} = 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)
const packageName = 'docusaurus-theme-openapi'
const packageParentName = 'docusaurus-preset-openapi'
const overideComponents = ['Curl', 'Response']
const messageTemplate = `<table><thead><tr><th colspan="2">
⚠️ This PR contains changes to components of ${packageName} we've overridden
</th></tr>
<tr><th colspan="2">
We need to watch out for changes to the ${overideComponents.join(
', ',
)} components
</th></tr></thead>
`
if (
!['dependabot[bot]', 'dependabot-preview[bot]'].includes(pr.user.login)
) {
return
}
const files = await getAllFilesForPullRequest(
client,
github.context.repo.owner,
github.context.repo.repo,
pr.number,
)
const file = files.filter(f => f.filename === 'package-lock.json')[0]
if (file === undefined) {
return
}
const prCommitRefForFile = file.contents_url.split('ref=')[1]
const pkgLockNewJson = await getLargeJsonAtRef(
client,
github.context.repo.owner,
github.context.repo.repo,
file.filename,
prCommitRefForFile,
)
const pkgLockOldJson = await getLargeJsonAtRef(
client,
github.context.repo.owner,
github.context.repo.repo,
file.filename,
'master',
)
const oldVesionModuleKey = findKeyEndingWith(
pkgLockOldJson.packages,
`node_modules/${packageName}`,
)
const newVesionModuleKey = findKeyEndingWith(
pkgLockNewJson.packages,
`node_modules/${packageName}`,
)
let oldVersion = pkgLockOldJson.packages[oldVesionModuleKey].version
let newVersion = pkgLockNewJson.packages[newVesionModuleKey].version
const oldVesionModuleKeyParent = findKeyEndingWith(
pkgLockOldJson.packages,
`node_modules/${packageParentName}`,
)
const newVesionModuleKeyParent = findKeyEndingWith(
pkgLockNewJson.packages,
`node_modules/${packageParentName}`,
)
const oldVersionParent =
pkgLockOldJson.packages[oldVesionModuleKeyParent].dependencies[
packageName
].substring(1)
const newVersionParent =
pkgLockNewJson.packages[newVesionModuleKeyParent].dependencies[
packageName
].substring(1)
// if parent dependency is higher version then existing
// npm install will retrive the newer version from the parent dependency
if (oldVersionParent > oldVersion) {
oldVersion = oldVersionParent
}
if (newVersionParent > newVersion) {
newVersion = newVersionParent
}
core.info(`oldVersion=${oldVersion}`)
core.info(`newVersion=${newVersion}`)
if (newVersion !== oldVersion) {
const pkgChangedFiles = await getChangedFilesBetweenTags(
client,
'cloud-annotations',
'docusaurus-openapi',
`v${oldVersion}`,
`v${newVersion}`,
)
const changedComponents = overideComponents.filter(
componenet =>
pkgChangedFiles.filter(
path =>
path.includes('docusaurus-theme-openapi/src/theme') &&
path.includes(componenet),
).length > 0,
)
const versionReport = `<tbody><tr><td> Old version </td><td> ${oldVersion} </td></tr>
<tr><td> New version </td><td> ${newVersion} </td></tr>
`
const changedComponentsReport = `<tr><td> Overide components changed </td><td> ${changedComponents.join(
', ',
)} </td></tr></tbody></table>
`
const body = messageTemplate + versionReport + changedComponentsReport
await client.rest.issues.createComment({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
issue_number: pr.number,
body,
})
core.info('Found changes and posted comment, done.')
return
}
core.info('No changes found, done.')
} catch (error) {
core.setFailed(error.message)
}
}
run()

View File

@@ -1,243 +0,0 @@
{
"name": "docusaurus-swizzled-warning",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "docusaurus-swizzled-warning",
"version": "0.0.0",
"license": "CC0",
"dependencies": {
"@actions/core": "^1.11.1",
"@actions/github": "^6.0.0"
}
},
"node_modules/@actions/core": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz",
"integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==",
"dependencies": {
"@actions/exec": "^1.1.1",
"@actions/http-client": "^2.0.1"
}
},
"node_modules/@actions/exec": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz",
"integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==",
"dependencies": {
"@actions/io": "^1.0.1"
}
},
"node_modules/@actions/github": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.0.tgz",
"integrity": "sha512-alScpSVnYmjNEXboZjarjukQEzgCRmjMv6Xj47fsdnqGS73bjJNDpiiXmp8jr0UZLdUB6d9jW63IcmddUP+l0g==",
"dependencies": {
"@actions/http-client": "^2.2.0",
"@octokit/core": "^5.0.1",
"@octokit/plugin-paginate-rest": "^9.0.0",
"@octokit/plugin-rest-endpoint-methods": "^10.0.0"
}
},
"node_modules/@actions/http-client": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.0.tgz",
"integrity": "sha512-q+epW0trjVUUHboliPb4UF9g2msf+w61b32tAkFEwL/IwP0DQWgbCMM0Hbe3e3WXSKz5VcUXbzJQgy8Hkra/Lg==",
"dependencies": {
"tunnel": "^0.0.6",
"undici": "^5.25.4"
}
},
"node_modules/@actions/io": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz",
"integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="
},
"node_modules/@fastify/busboy": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.0.0.tgz",
"integrity": "sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ==",
"engines": {
"node": ">=14"
}
},
"node_modules/@octokit/auth-token": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz",
"integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==",
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/core": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.0.1.tgz",
"integrity": "sha512-lyeeeZyESFo+ffI801SaBKmCfsvarO+dgV8/0gD8u1d87clbEdWsP5yC+dSj3zLhb2eIf5SJrn6vDz9AheETHw==",
"dependencies": {
"@octokit/auth-token": "^4.0.0",
"@octokit/graphql": "^7.0.0",
"@octokit/request": "^8.0.2",
"@octokit/request-error": "^5.0.0",
"@octokit/types": "^12.0.0",
"before-after-hook": "^2.2.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/endpoint": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.1.tgz",
"integrity": "sha512-hRlOKAovtINHQPYHZlfyFwaM8OyetxeoC81lAkBy34uLb8exrZB50SQdeW3EROqiY9G9yxQTpp5OHTV54QD+vA==",
"dependencies": {
"@octokit/types": "^12.0.0",
"is-plain-object": "^5.0.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/graphql": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.0.2.tgz",
"integrity": "sha512-OJ2iGMtj5Tg3s6RaXH22cJcxXRi7Y3EBqbHTBRq+PQAqfaS8f/236fUrWhfSn8P4jovyzqucxme7/vWSSZBX2Q==",
"dependencies": {
"@octokit/request": "^8.0.1",
"@octokit/types": "^12.0.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/openapi-types": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.0.0.tgz",
"integrity": "sha512-PclQ6JGMTE9iUStpzMkwLCISFn/wDeRjkZFIKALpvJQNBGwDoYYi2fFvuHwssoQ1rXI5mfh6jgTgWuddeUzfWw=="
},
"node_modules/@octokit/plugin-paginate-rest": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.0.0.tgz",
"integrity": "sha512-oIJzCpttmBTlEhBmRvb+b9rlnGpmFgDtZ0bB6nq39qIod6A5DP+7RkVLMOixIgRCYSHDTeayWqmiJ2SZ6xgfdw==",
"dependencies": {
"@octokit/types": "^12.0.0"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@octokit/core": ">=5"
}
},
"node_modules/@octokit/plugin-rest-endpoint-methods": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.0.1.tgz",
"integrity": "sha512-fgS6HPkPvJiz8CCliewLyym9qAx0RZ/LKh3sATaPfM41y/O2wQ4Z9MrdYeGPVh04wYmHFmWiGlKPC7jWVtZXQA==",
"dependencies": {
"@octokit/types": "^12.0.0"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@octokit/core": ">=5"
}
},
"node_modules/@octokit/request": {
"version": "8.1.4",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.1.4.tgz",
"integrity": "sha512-M0aaFfpGPEKrg7XoA/gwgRvc9MSXHRO2Ioki1qrPDbl1e9YhjIwVoHE7HIKmv/m3idzldj//xBujcFNqGX6ENA==",
"dependencies": {
"@octokit/endpoint": "^9.0.0",
"@octokit/request-error": "^5.0.0",
"@octokit/types": "^12.0.0",
"is-plain-object": "^5.0.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/request-error": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.0.1.tgz",
"integrity": "sha512-X7pnyTMV7MgtGmiXBwmO6M5kIPrntOXdyKZLigNfQWSEQzVxR4a4vo49vJjTWX70mPndj8KhfT4Dx+2Ng3vnBQ==",
"dependencies": {
"@octokit/types": "^12.0.0",
"deprecation": "^2.0.0",
"once": "^1.4.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/types": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.0.0.tgz",
"integrity": "sha512-EzD434aHTFifGudYAygnFlS1Tl6KhbTynEWELQXIbTY8Msvb5nEqTZIm7sbPEt4mQYLZwu3zPKVdeIrw0g7ovg==",
"dependencies": {
"@octokit/openapi-types": "^19.0.0"
}
},
"node_modules/before-after-hook": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="
},
"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/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dependencies": {
"wrappy": "1"
}
},
"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/undici": {
"version": "5.28.5",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.5.tgz",
"integrity": "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==",
"license": "MIT",
"dependencies": {
"@fastify/busboy": "^2.0.0"
},
"engines": {
"node": ">=14.0"
}
},
"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/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
}
}
}

View File

@@ -1,16 +0,0 @@
{
"name": "docusaurus-swizzled-warning",
"version": "0.0.0",
"description": "",
"main": "index.js",
"private": true,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "jNullj",
"license": "CC0",
"dependencies": {
"@actions/core": "^1.11.1",
"@actions/github": "^6.0.0"
}
}

View File

@@ -1,8 +0,0 @@
FROM node:20-bullseye
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,65 +0,0 @@
#!/bin/bash
set -euxo pipefail
# mark workspace dir as 'safe'
git config --system --add safe.directory '/github/workspace'
# Find last server-YYYY-MM-DD tag
git fetch --unshallow --tags
LAST_TAG=$(git tag | grep server | tail -n 1)
# Set up a git user
git config user.name "release[bot]"
git config user.email "actions@users.noreply.github.com"
# 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,28 +0,0 @@
name: 'Integration tests'
description: 'Run integration tests'
inputs:
github-token:
description: 'The GITHUB_TOKEN secret'
required: true
runs:
using: 'composite'
steps:
- name: Migrate DB
if: always()
run: npm run migrate up
env:
POSTGRES_URL: postgresql://postgres:postgres@localhost:5432/ci_test
shell: bash
- name: Integration Tests
if: always()
run: npm run test:integration -- --reporter json --reporter-option 'output=reports/integration-tests.json'
env:
GH_TOKEN: '${{ inputs.github-token }}'
POSTGRES_URL: postgresql://postgres:postgres@localhost:5432/ci_test
shell: bash
- name: Write Markdown Summary
if: always()
run: node scripts/mocha2md.js Integration reports/integration-tests.json >> $GITHUB_STEP_SUMMARY
shell: bash

View File

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

View File

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

View File

@@ -1,44 +0,0 @@
name: 'Set up project'
description: 'Set up project'
inputs:
node-version:
description: 'Version Spec of the node version to use. Examples: 12.x, 10.15.1, >=10.15.0.'
required: true
npm-version:
description: 'Version Spec of the npm version to use. Examples: 9.x, 10.2.3, >=10.1.0.'
required: false
default: '^10'
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@v4
with:
node-version: ${{ inputs.node-version }}
- name: Install NPM ${{ inputs.npm-version }}
run: npm install -g npm@${{ inputs.npm-version }}
shell: bash
- 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,67 +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
rebase-strategy: disabled
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'
groups:
# All official @docusaurus/* packages should have the exact same version as @docusaurus/core.
# From https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#groups:
# "You cannot apply a single grouping set of rules to both version updates and security
# updates [...] you must define two, separately named, grouping sets of rules"
# See https://github.com/badges/shields/issues/10242 for more information.
docusaurus-version-updates:
applies-to: version-updates
patterns:
- '@docusaurus/*'
docusaurus-security-updates:
applies-to: security-updates
patterns:
- '@docusaurus/*'
# badge-maker package dependencies
- package-ecosystem: npm
directory: '/badge-maker'
schedule:
interval: weekly
day: friday
time: '12:00'
open-pull-requests-limit: 99
rebase-strategy: disabled
# GH actions
- package-ecosystem: 'github-actions'
# all composite actions must be individually listed here
# https://github.com/dependabot/dependabot-core/issues/6704
directories:
- '/'
- '/.github/actions/core-tests'
- '/.github/actions/integration-tests'
- '/.github/actions/package-tests'
- '/.github/actions/service-tests'
- '/.github/actions/setup'
schedule:
interval: weekly
open-pull-requests-limit: 99
rebase-strategy: disabled
# docusaurus-swizzled-warning package dependencies
- package-ecosystem: npm
directory: '/.github/actions/docusaurus-swizzled-warning'
schedule:
interval: weekly
day: friday
time: '12:00'
open-pull-requests-limit: 99
rebase-strategy: disabled

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

View File

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

View File

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

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

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

View File

@@ -1,28 +0,0 @@
name: Build Docker Image
on:
pull_request:
push:
branches:
- 'gh-readonly-queue/**'
jobs:
build-docker-image:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set Git Short SHA
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
- name: Build
uses: docker/build-push-action@v6
with:
context: .
push: false
tags: ghcr.io/badges/shields:pr-validation
build-args: |
version=${{ env.SHORT_SHA }}

View File

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

View File

@@ -1,73 +0,0 @@
name: Coveralls Code Coverage
on:
schedule:
- cron: '10 7 * * *'
# At 07:10, daily
workflow_dispatch:
jobs:
coveralls-code-coverage:
runs-on: ubuntu-latest
services:
postgres:
image: postgres
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: ci_test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup
uses: ./.github/actions/setup
with:
# Even though we're currently deploying on Node 20, we run coverage test on Node 22
# to work around https://github.com/bcoe/v8-coverage/pull/2.
node-version: 22
env:
NPM_CONFIG_ENGINE_STRICT: 'false'
- name: Migrate DB
run: npm run migrate up
env:
POSTGRES_URL: postgresql://postgres:postgres@localhost:5432/ci_test
shell: bash
- name: Coverage for Main Tests
run: npm run coverage:test
env:
GH_TOKEN: '${{ secrets.GH_PAT }}'
POSTGRES_URL: postgresql://postgres:postgres@localhost:5432/ci_test
shell: bash
- name: Coverage for Service Tests
run: npm run coverage:test:services
continue-on-error: true
env:
RETRY_COUNT: 3
GH_TOKEN: '${{ secrets.GH_PAT }}'
LIBRARIESIO_TOKENS: '${{ secrets.SERVICETESTS_LIBRARIESIO_TOKENS }}'
OBS_USER: '${{ secrets.SERVICETESTS_OBS_USER }}'
OBS_PASS: '${{ secrets.SERVICETESTS_OBS_PASS }}'
PEPY_KEY: '${{ secrets.SERVICETESTS_PEPY_KEY }}'
REDDIT_CLIENT_ID: '${{ secrets.SERVICETESTS_REDDIT_CLIENT_ID }}'
REDDIT_CLIENT_SECRET: '${{ secrets.SERVICETESTS_REDDIT_CLIENT_SECRET }}'
SL_INSIGHT_USER_UUID: '${{ secrets.SERVICETESTS_SL_INSIGHT_USER_UUID }}'
SL_INSIGHT_API_TOKEN: '${{ secrets.SERVICETESTS_SL_INSIGHT_API_TOKEN }}'
TWITCH_CLIENT_ID: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_ID }}'
TWITCH_CLIENT_SECRET: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_SECRET }}'
YOUTUBE_API_KEY: '${{ secrets.SERVICETESTS_YOUTUBE_API_KEY }}'
shell: bash
- name: Coveralls GitHub Action
uses: coverallsapp/github-action@v2

View File

@@ -1,73 +0,0 @@
name: Create Release
on:
pull_request:
types: [closed]
permissions:
contents: write
packages: 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@v4
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@v3
with:
platforms: linux/amd64,linux/arm64
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push snapshot release to DockerHub
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: shieldsio/shields:server-${{ steps.date.outputs.date }}
build-args: |
version=server-${{ steps.date.outputs.date }}
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push snapshot release to GHCR
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ghcr.io/badges/shields:server-${{ steps.date.outputs.date }}
build-args: |
version=server-${{ steps.date.outputs.date }}

View File

@@ -1,72 +0,0 @@
name: Run Daily Tests
on:
schedule:
- cron: '45 3 * * *'
# At 03:45, daily
workflow_dispatch:
jobs:
daily-tests:
runs-on: ubuntu-latest
services:
postgres:
image: postgres
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: ci_test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup
uses: ./.github/actions/setup
with:
node-version: 20
- name: Core tests
if: always()
uses: ./.github/actions/core-tests
- name: Package tests
if: always()
uses: ./.github/actions/package-tests
- name: Integration Tests (with PAT)
if: always()
uses: ./.github/actions/integration-tests
with:
github-token: '${{ secrets.GH_PAT }}'
- name: Run Service tests
run: npm run test:services -- --reporter json --reporter-option 'output=reports/service-tests.json'
if: always()
env:
RETRY_COUNT: 3
GH_TOKEN: '${{ secrets.GH_PAT }}'
LIBRARIESIO_TOKENS: '${{ secrets.SERVICETESTS_LIBRARIESIO_TOKENS }}'
OBS_USER: '${{ secrets.SERVICETESTS_OBS_USER }}'
OBS_PASS: '${{ secrets.SERVICETESTS_OBS_PASS }}'
PEPY_KEY: '${{ secrets.SERVICETESTS_PEPY_KEY }}'
REDDIT_CLIENT_ID: '${{ secrets.SERVICETESTS_REDDIT_CLIENT_ID }}'
REDDIT_CLIENT_SECRET: '${{ secrets.SERVICETESTS_REDDIT_CLIENT_SECRET }}'
SL_INSIGHT_USER_UUID: '${{ secrets.SERVICETESTS_SL_INSIGHT_USER_UUID }}'
SL_INSIGHT_API_TOKEN: '${{ secrets.SERVICETESTS_SL_INSIGHT_API_TOKEN }}'
TWITCH_CLIENT_ID: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_ID }}'
TWITCH_CLIENT_SECRET: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_SECRET }}'
YOUTUBE_API_KEY: '${{ secrets.SERVICETESTS_YOUTUBE_API_KEY }}'
- name: Write Service Tests Markdown Summary
if: always()
run: |
echo '# Services' >> $GITHUB_STEP_SUMMARY
node scripts/mocha2md.js Report reports/service-tests.json >> $GITHUB_STEP_SUMMARY

View File

@@ -1,29 +0,0 @@
name: Danger
on:
pull_request_target:
types: [opened, reopened, synchronize]
permissions:
checks: write
pull-requests: write
statuses: write
jobs:
danger:
runs-on: ubuntu-latest
if: github.actor != 'dependabot[bot]' && github.actor != 'repo-ranger[bot]'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup
uses: ./.github/actions/setup
with:
node-version: 20
- 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,32 +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@v4
with:
persist-credentials: false
- name: Setup
uses: ./.github/actions/setup
with:
node-version: 20
- name: Build
run: 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,45 +0,0 @@
name: Create/Update Review App
on:
workflow_dispatch:
inputs:
pr_number:
description: 'PR Number to deploy e.g: 1234'
required: true
permissions:
pull-requests: write
jobs:
deploy-review-app:
runs-on: ubuntu-latest
environment: 'Review Apps'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: superfly/flyctl-actions/setup-flyctl@master
- name: install jq
run: |
sudo apt-get -qq update
sudo apt-get install -y jq
- run: .github/scripts/deploy-review-app.sh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
PR_NUMBER: ${{ github.event.inputs.pr_number }}
# credentials to set when we create the review app
SECRETS: |
GH_TOKEN=${{ secrets.GH_PAT }}
LIBRARIESIO_TOKENS=${{ secrets.SERVICETESTS_LIBRARIESIO_TOKENS }}
OBS_USER=${{ secrets.SERVICETESTS_OBS_USER }}
OBS_PASS=${{ secrets.SERVICETESTS_OBS_PASS }}
PEPY_KEY=${{ secrets.SERVICETESTS_PEPY_KEY }}
REDDIT_CLIENT_ID=${{ secrets.SERVICETESTS_REDDIT_CLIENT_ID }}
REDDIT_CLIENT_SECRET=${{ secrets.SERVICETESTS_REDDIT_CLIENT_SECRET }}
SL_INSIGHT_API_TOKEN=${{ secrets.SERVICETESTS_SL_INSIGHT_USER_UUID }}
SL_INSIGHT_USER_UUID=${{ secrets.SERVICETESTS_SL_INSIGHT_API_TOKEN }}
TWITCH_CLIENT_ID=${{ secrets.SERVICETESTS_TWITCH_CLIENT_ID }}
TWITCH_CLIENT_SECRET=${{ secrets.SERVICETESTS_TWITCH_CLIENT_SECRET }}
YOUTUBE_API_KEY=${{ secrets.SERVICETESTS_YOUTUBE_API_KEY }}

View File

@@ -1,22 +0,0 @@
name: Docusaurus swizzled component changes warning
on:
pull_request:
types: [opened]
permissions:
pull-requests: write
jobs:
docusaurus-swizzled-warning:
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install action dependencies
run: cd .github/actions/docusaurus-swizzled-warning && npm ci
- uses: ./.github/actions/docusaurus-swizzled-warning
with:
github-token: '${{ secrets.GITHUB_TOKEN }}'

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@v4
- 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, reopened, synchronize]
jobs:
enforce-dependency-review:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v4
- name: 'Dependency Review'
uses: actions/dependency-review-action@v4

View File

@@ -1,56 +0,0 @@
name: Build and Publish Next Docker Image
on:
push:
branches:
- master
permissions:
packages: write
jobs:
publish-docker-next:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
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 to DockerHub
id: docker_build_push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: shieldsio/shields:next
build-args: |
version=${{ env.SHORT_SHA }}
- name: Output Image Digest
run: echo ${{ steps.docker_build_push.outputs.digest }} >> $GITHUB_STEP_SUMMARY
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push to GHCR
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/badges/shields:next
build-args: |
version=${{ env.SHORT_SHA }}

View File

@@ -1,69 +0,0 @@
name: Test new bug report badge
run-name: Test bug report on issue ${{ github.event.issue.number }}
on:
issues:
types: [opened]
jobs:
extract-bug-badge-url:
if: ${{ contains(github.event.issue.labels.*.name, 'question') }}
runs-on: ubuntu-latest
outputs:
runBadgeTest: ${{ steps.testCondition.outputs.runNext }}
link: ${{ steps.testCondition.outputs.link }}
steps:
- name: Test badge test run conditions
id: testCondition
env:
ISSUE_BODY: '${{ github.event.issue.body }}'
run: |
product=$(echo "$ISSUE_BODY" | grep -A2 "Are you experiencing an issue with.*" | tail -n 1)
link=$(echo "$ISSUE_BODY" | grep -A2 "Link to the badge.*" | tail -n 1)
if [[ "$product" == "shields.io" && "$link" == "https://img.shields.io"* ]]; then
echo "runNext=true" >> "$GITHUB_OUTPUT"
echo "link=$link" >> "$GITHUB_OUTPUT"
else
echo "Conditions not met. Skipping the workflow..."
echo "runNext=false" >> "$GITHUB_OUTPUT"
fi
run-bug-badge-url-test:
needs: extract-bug-badge-url
if: needs.extract-bug-badge-url.outputs.runBadgeTest == 'true'
permissions:
issues: write
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup
uses: ./.github/actions/setup
with:
node-version: 20
cypress: false
- name: Output debug info
env:
TEST_BADGE_LINK: '${{ needs.extract-bug-badge-url.outputs.link }}'
run: npm run badge $TEST_BADGE_LINK
- name: Add Comment to Issue
uses: actions/github-script@v7
with:
script: |
const issueNumber = context.issue.number;
const owner = context.repo.owner;
const repo = context.repo.repo;
const runId = context.runId;
const jobUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}`;
const issueComment = `
Badge tested using \`npm run badge ${{ needs.extract-bug-badge-url.outputs.link }}\`
Output is available [here](${jobUrl})
`;
github.rest.issues.createComment({
issue_number: issueNumber,
owner: owner,
repo: repo,
body: issueComment
});

View File

@@ -1,49 +0,0 @@
name: E2E
on:
pull_request:
types: [opened, reopened, synchronize]
push:
branches-ignore:
- 'gh-pages'
- 'dependabot/**'
jobs:
test-e2e:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Cache Cypress binary
id: cache-cypress
uses: actions/cache@v4
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: 20
cypress: true
- name: Run tests
env:
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
run: npm run e2e
- name: Archive videos
if: always()
uses: actions/upload-artifact@v4
with:
name: videos
path: cypress/videos
- name: Archive screenshots
if: always()
uses: actions/upload-artifact@v4
with:
name: screenshots
path: cypress/screenshots

View File

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

View File

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

View File

@@ -1,28 +0,0 @@
name: Lint
on:
pull_request:
types: [opened, reopened, synchronize]
push:
branches-ignore:
- 'gh-pages'
- 'dependabot/**'
jobs:
test-lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup
uses: ./.github/actions/setup
with:
node-version: 20
- 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 22
on:
pull_request:
types: [opened, reopened, synchronize]
push:
branches-ignore:
- 'gh-pages'
- 'dependabot/**'
jobs:
test-main-22:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup
uses: ./.github/actions/setup
with:
node-version: 22
env:
NPM_CONFIG_ENGINE_STRICT: 'false'
- name: Core tests
uses: ./.github/actions/core-tests

View File

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

View File

@@ -1,41 +0,0 @@
name: Package CLI
on:
pull_request:
types: [opened, reopened, synchronize]
push:
branches-ignore:
- 'gh-pages'
- 'dependabot/**'
# Smoke test (render a badge with the CLI) with only the package
# dependencies installed.
jobs:
test-package-cli:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- node: '16'
- node: '18'
- node: '20'
- node: '22'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Node JS ${{ inputs.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- name: Install dependencies
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,41 +0,0 @@
name: Package Library
on:
pull_request:
types: [opened, reopened, synchronize]
push:
branches-ignore:
- 'gh-pages'
- 'dependabot/**'
jobs:
test-package-lib:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- node: '16'
npm: '^9'
engine-strict: 'false'
- node: '18'
npm: '^9'
engine-strict: 'false'
- node: '20'
npm: '^10'
engine-strict: 'true'
- node: '22'
npm: '^11'
engine-strict: 'false'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup
uses: ./.github/actions/setup
with:
node-version: ${{ matrix.node }}
npm-version: ${{ matrix.npm }}
env:
NPM_CONFIG_ENGINE_STRICT: ${{ matrix.engine-strict }}
- name: Package tests
uses: ./.github/actions/package-tests

View File

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

View File

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

View File

@@ -1,33 +0,0 @@
name: Update GitHub API Version
on:
schedule:
- cron: '0 7 * * 6'
# At 07:00 on Saturday
workflow_dispatch:
permissions:
pull-requests: write
contents: write
jobs:
update-github-api:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup
uses: ./.github/actions/setup
with:
node-version: 20
- 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@v7
with:
token: '${{ secrets.GITHUB_TOKEN }}'
commit-message: Update GitHub API Version
title: Update [GitHub] API Version
branch-suffix: random

22
.gitignore vendored
View File

@@ -7,7 +7,7 @@
/private
/index.html
/shields.env
badge-maker/package-lock.json
gh-badges/package-lock.json
# Folder view configuration files
.DS_Store
@@ -50,6 +50,9 @@ lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
@@ -89,7 +92,10 @@ typings/
# Temporary build artifacts.
/build
frontend/categories/*.yaml
.next
badge-examples.json
supported-features.json
service-definitions.yml
# Local runtime configuration.
/config/local*.yml
@@ -97,6 +103,10 @@ frontend/categories/*.yaml
# Template for the local runtime configuration.
!/config/local*.template.yml
# Gatsby
/.cache
/public
# Cypress
/cypress/videos/
/cypress/screenshots/
@@ -106,11 +116,3 @@ frontend/categories/*.yaml
# Flamebearer
flamegraph.html
# config file for node-pg-migrate
migrations-config.json
# Frontend/Docusaurus
frontend/.docusaurus
frontend/.cache-loader
/public

6
.mocharc-frontend.yml Normal file
View File

@@ -0,0 +1,6 @@
reporter: mocha-env-reporter
require:
- '@babel/polyfill'
- '@babel/register'
- mocha-yaml-loader
- frontend/mocha-ignore-pngs

30
.nowignore Normal file
View File

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

2
.npmrc
View File

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

10
.nycrc-frontend.json Normal file
View File

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

View File

@@ -9,8 +9,9 @@
"**/test-helpers.js",
"**/*-test-helpers.js",
"**/*-fixtures.js",
"**/*.test-d.ts",
"**/mocha-*.js",
"dangerfile.js",
"gatsby-*.js",
"core/service-test-runner",
"core/got-test-client.js",
"services/**/*.tester.js",
@@ -20,10 +21,6 @@
"scripts",
"coverage",
"build",
".github",
"**/public/",
"cypress",
"frontend",
"migrations"
".github"
]
}

View File

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

View File

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

View File

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

View File

@@ -1,703 +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-2025-02-01
- fix badge style when logo only [#10794](https://github.com/badges/shields/issues/10794)
- add tests for dynamic xml with lowercase doctype [#10845](https://github.com/badges/shields/issues/10845)
- pass matching mime type to xmldom; test [dynamicxml] [#10830](https://github.com/badges/shields/issues/10830)
- fix badge-maker package tests [#10809](https://github.com/badges/shields/issues/10809)
- URL validator tidyup; affects [discourse dynamic endpoint gerrit jira maven nexus osslifecycle python vpm website] securityheaders sonar swagger w3c [#10810](https://github.com/badges/shields/issues/10810)
- allow [chromewebstore] size to contain decimal point [#10812](https://github.com/badges/shields/issues/10812)
- fix: cypress video [#10829](https://github.com/badges/shields/issues/10829)
- Add auth support to [Reddit] badges [#10790](https://github.com/badges/shields/issues/10790)
- Fixed mixed up Code climate endpoints [#10813](https://github.com/badges/shields/issues/10813)
- feat: add terraform registry providers and modules downloads [#10793](https://github.com/badges/shields/issues/10793)
- fix missing comma in badge-maker docs example [#10808](https://github.com/badges/shields/issues/10808)
- add support for npm 11 [#10795](https://github.com/badges/shields/issues/10795)
- Renew [Mastodon] docs and improve parameter handling [#10789](https://github.com/badges/shields/issues/10789)
- Support [Matrix] summary endpoint [#10782](https://github.com/badges/shields/issues/10782)
- update monitoring docs/links [#10780](https://github.com/badges/shields/issues/10780)
- Improve donate Call To Action [#10777](https://github.com/badges/shields/issues/10777)
- use metric() in [coderabbit] badge [#10779](https://github.com/badges/shields/issues/10779)
- cache matrix badges for 4 hours [#10778](https://github.com/badges/shields/issues/10778)
- Dependency updates
## server-2025-01-01
- Add [PypiTypes] badge [#10774](https://github.com/badges/shields/issues/10774)
- feat(endpoint-badge): add logoSize support [#10132](https://github.com/badges/shields/issues/10132)
- fix auto-sized logo sizes [#10764](https://github.com/badges/shields/issues/10764)
- Add [Coderabbit] PR Stats service and tests [#10749](https://github.com/badges/shields/issues/10749)
- add [PUB] downloads badge [#10745](https://github.com/badges/shields/issues/10745)
- Add [GitLab] Top Language Badge [#10750](https://github.com/badges/shields/issues/10750)
- provide a non-repository scoped version of [githubcodesearch] [#10733](https://github.com/badges/shields/issues/10733)
- [ReproducibleCentral] add Reproducible Central in Dependencies [#10705](https://github.com/badges/shields/issues/10705)
- Add ability to format bytes as metric or IEC; affects [bundlejs bundlephobia ChromeWebStoreSize CratesSize DockerSize GithubRepoSize GithubCodeSize GithubSize NpmUnpackedSize SpigetDownloadSize steam VisualStudioAppCenterReleasesSize whatpulse] [#10547](https://github.com/badges/shields/issues/10547)
- Dependency updates
## server-2024-12-01
- add [WingetVersion] Badge [#10245](https://github.com/badges/shields/issues/10245)
- Fix broken URL for pingpong.one [#10655](https://github.com/badges/shields/issues/10655)
- [npm] - Last update badge added [#10641](https://github.com/badges/shields/issues/10641)
- reduce overhead of NPM Last Update badge; test [npm] [#10666](https://github.com/badges/shields/issues/10666)
- Add YouTube-specific privacy notes [#10646](https://github.com/badges/shields/issues/10646)
- Dependency updates
## server-2024-11-02
- cleanly handle null or undefined result from jsonpath-plus [#10645](https://github.com/badges/shields/issues/10645)
- add content security policy header to SVG responses [#10642](https://github.com/badges/shields/issues/10642)
- [Scoop] Added scoop-license badge. [#10627](https://github.com/badges/shields/issues/10627)
- [Chromewebstore] Extension size & last updated [#10613](https://github.com/badges/shields/issues/10613)
- Deprecate HackageDeps service [#10618](https://github.com/badges/shields/issues/10618)
- Add [CratesUserDownloads] service and tester [#10619](https://github.com/badges/shields/issues/10619)
- [Snapcraft] - Added snapcraft last update badge [#10610](https://github.com/badges/shields/issues/10610)
- [GitHubHacktoberfest] 2024 support [#10612](https://github.com/badges/shields/issues/10612)
- add [homebrew] cask download badge [#10595](https://github.com/badges/shields/issues/10595)
- remove prefix v for commit hash version [#10597](https://github.com/badges/shields/issues/10597)
- [Maven] Added badge for Maven-Cenral last-update (#10301) [#10585](https://github.com/badges/shields/issues/10585)
- [DynamicXml] parse doc as html if served with text/html content type [#10607](https://github.com/badges/shields/issues/10607)
- Revert "Use old.stats.jenkins.io for JSON data (#10522)" [#10537](https://github.com/badges/shields/issues/10537)
- catch queries that cause TypeError [#10556](https://github.com/badges/shields/issues/10556)
- Dependency updates
## server-2024-09-25
This release includes an important security fix. See
- https://github.com/badges/shields/security/advisories/GHSA-rxvx-x284-4445
- https://github.com/badges/shields/issues/10553
for more details
- [dynamicjson dynamicyaml dynamictoml] switch to jsonpath-plus [#10551](https://github.com/badges/shields/issues/10551)
- [Snapcraft] license [#10520](https://github.com/badges/shields/issues/10520)
- deprecate [wheelmap] service [#10538](https://github.com/badges/shields/issues/10538)
- Use old.stats.jenkins.io for JSON data [#10522](https://github.com/badges/shields/issues/10522)
- catch xml ParseError [#10516](https://github.com/badges/shields/issues/10516)
- migrate [MozillaObservatory] to /scan endpoint [#10491](https://github.com/badges/shields/issues/10491)
- fix incorrect codecov config link [#10511](https://github.com/badges/shields/issues/10511)
- [OSSLifecycle OSSLifecycleRedirect] Add file_url param to pull from non-github sources [#10489](https://github.com/badges/shields/issues/10489)
- perf: improve logoSize performance [#10488](https://github.com/badges/shields/issues/10488)
- perf: faster `resetIconPosition` avoiding to parse path twice [#10497](https://github.com/badges/shields/issues/10497)
- perf: limit logoSize precision to 3 [#10521](https://github.com/badges/shields/issues/10521)
- Dependency updates
## server-2024-09-02
- Publish linux/amd64 docker images for snapshot builds [#10476](https://github.com/badges/shields/issues/10476)
- Fix Gitea not having credentials/authorizedOrigins in Docker environments [#10486](https://github.com/badges/shields/issues/10486)
- fix typo in pepy downloads [#10475](https://github.com/badges/shields/issues/10475)
- ignore a couple of docusaurus warnings [#10469](https://github.com/badges/shields/issues/10469)
- Use Ecologi API to power Treeware badges [#10467](https://github.com/badges/shields/issues/10467)
- move go version badge to platform support category [#10444](https://github.com/badges/shields/issues/10444)
- [Crates] Implement Dependents Badge [#10438](https://github.com/badges/shields/issues/10438)
- [Crates] Added crate size badge [#10421](https://github.com/badges/shields/issues/10421)
- Dependency updates
## server-2024-08-01
- send Cross-Origin-Resource-Policy header on all responses [#10420](https://github.com/badges/shields/issues/10420)
- migrate [MozillaObservatory] to new API [#10402](https://github.com/badges/shields/issues/10402)
- use metric() for [discord] and [revolt] badges [#10406](https://github.com/badges/shields/issues/10406)
- Cache text only static badges for longer [#10403](https://github.com/badges/shields/issues/10403)
- Fix [FreeCodeCampPoints] not found handling [#10377](https://github.com/badges/shields/issues/10377)
- Fix [Gitea] not found message [#10373](https://github.com/badges/shields/issues/10373)
- Deprecate [Bountysource] service [#10371](https://github.com/badges/shields/issues/10371)
- Sunset Shields custom logos [#10347](https://github.com/badges/shields/issues/10347)
- Use ellipsis when many versions returned for [ModrinthGameVersions] [#10350](https://github.com/badges/shields/issues/10350)
- deprecate [tokei] service [#9581](https://github.com/badges/shields/issues/9581)
- Add CF-Ray header value to Sentry errors if available [#10339](https://github.com/badges/shields/issues/10339)
- Use XML for Chocolatey, affects [Chocolatey Resharper PowershellGallery] [#10344](https://github.com/badges/shields/issues/10344)
- include github contributors badge in docs site [#10337](https://github.com/badges/shields/issues/10337)
- Dependency updates
## server-2024-07-01
- Add [AUR] Popularity Badge [#10304](https://github.com/badges/shields/issues/10304)
- fix npm badges when `maintainers` not in response [#10286](https://github.com/badges/shields/issues/10286)
- Expose `logoBase64` and `links` in badge-maker NPM package [#10283](https://github.com/badges/shields/issues/10283)
- Remove `logoPosition` [#10284](https://github.com/badges/shields/issues/10284)
- [MBIN] Add subscribers badge [#10270](https://github.com/badges/shields/issues/10270)
- Add [Docker] support for loong64 arch [#10241](https://github.com/badges/shields/issues/10241)
- Add puppetforge quality score badges [#10201](https://github.com/badges/shields/issues/10201)
- Dependency updates
## server-2024-06-01
- Remove namedLogo from defaultBadgeData of non-social badges [#10195](https://github.com/badges/shields/issues/10195)
- Update number of badges served each month [#10197](https://github.com/badges/shields/issues/10197)
- Delete old deprecated services [#10196](https://github.com/badges/shields/issues/10196)
- handle [BitbucketPipelines] responses with missing result key [#10163](https://github.com/badges/shields/issues/10163)
- Update description of GitHub commit status badge [#10198](https://github.com/badges/shields/issues/10198)
- chore: fix spelling of GitHub in badge descriptions [#10199](https://github.com/badges/shields/issues/10199)
- Add [GithubCheckRuns] service [#7759](https://github.com/badges/shields/issues/7759)
- feat: add Revolt badge [#10093](https://github.com/badges/shields/issues/10093)
- ensure color is string before calling toLowerCase() [#10129](https://github.com/badges/shields/issues/10129)
- instruct dependabot to monitor composite actions [#10139](https://github.com/badges/shields/issues/10139)
- run tests on node 22 [#10127](https://github.com/badges/shields/issues/10127)
- tweaks to libraries.io token pooling code [#10074](https://github.com/badges/shields/issues/10074)
- fix [pypi] status badge when package has no 'Development Status' classifier [#10107](https://github.com/badges/shields/issues/10107)
- clarify yml paths in server-secrets docs [#10106](https://github.com/badges/shields/issues/10106)
- Update region flag name in flyctl deploy command [#10134](https://github.com/badges/shields/issues/10134)
- Dependency updates
## server-2024-05-01
- [Hexpm] Fix badges for pre-release only versions [#10112](https://github.com/badges/shields/issues/10112)
- feat(logos): support auto-sizing mode [#9191](https://github.com/badges/shields/issues/9191) [#10110](https://github.com/badges/shields/issues/10110) [#10125](https://github.com/badges/shields/issues/10125)
- support setting pypiBaseUrl by environment variables and queryParameters; affects [pypi] [#10044](https://github.com/badges/shields/issues/10044)
- Add 0BSD license to licenseTypes and [PypiLicense] [#10092](https://github.com/badges/shields/issues/10092)
- Update Mastodon profile URL [#10082](https://github.com/badges/shields/issues/10082)
- [GitHubGoMod] Ignore comment after version (fixes #10079) [#10080](https://github.com/badges/shields/issues/10080)
- Perf: Librariesio repo dependencies [#10062](https://github.com/badges/shields/issues/10062)
- [Chocolatey Nuget] Fix "not found" error for chocolatey badge [#10060](https://github.com/badges/shields/issues/10060)
- Dependency updates
## server-2024-04-01
- improve performance of [GithubLastCommit] [GitlabLastCommit] [GiteaLastCommit] [#10046](https://github.com/badges/shields/issues/10046)
- [BitbucketLastCommit] Add Bitbucket last commit [#10043](https://github.com/badges/shields/issues/10043)
- [GithubLastCommit] [GitlabLastCommit] [GiteaLastCommit] Support file path for last commit [#10041](https://github.com/badges/shields/issues/10041)
- upgrade to docusaurus 3 [#9820](https://github.com/badges/shields/issues/9820)
- redirect [npm] /dt to /d18m [#10033](https://github.com/badges/shields/issues/10033)
- Add [JSR] version service [#10030](https://github.com/badges/shields/issues/10030)
- Add [snapcraft] version badge [#9976](https://github.com/badges/shields/issues/9976)
- Dependency updates
## server-2024-03-01
- feat(gitea): add last commit badge [#9995](https://github.com/badges/shields/issues/9995)
- [GithubCreatedAt] Add Created At Badge for Github [#9981](https://github.com/badges/shields/issues/9981)
- Added custom bucket url support for [Scoop] [#9984](https://github.com/badges/shields/issues/9984)
- [NpmUnpackedSize] Unpacked Size Badge [#9954](https://github.com/badges/shields/issues/9954)
- [Website] Render `status: down` badge if website is unresponsive [#9966](https://github.com/badges/shields/issues/9966)
- deprecate TAS [#9932](https://github.com/badges/shields/issues/9932)
- [GITEA] add forks, stars, issues and pr badges [#9923](https://github.com/badges/shields/issues/9923)
- tolerate missing short_version in [visualstudioappcenter] [#9951](https://github.com/badges/shields/issues/9951)
- [Crates] Only use non-yanked crate versions (ready for merge) [#9949](https://github.com/badges/shields/issues/9949)
- Dependency updates
## server-2024-02-01
- feat: added up_message and down_message to [uptimerobotstatus] [#9662](https://github.com/badges/shields/issues/9662)
- Add [Hangar] Badges [#9800](https://github.com/badges/shields/issues/9800)
- sort categories by title (except core) [#9888](https://github.com/badges/shields/issues/9888)
- Add Support for [Nostr] Followers [#9870](https://github.com/badges/shields/issues/9870)
- [thunderstore] replace experimental API usage with newly available v1 API [#9886](https://github.com/badges/shields/issues/9886)
- Update [Gitea] defaults to gitea.com [#9872](https://github.com/badges/shields/issues/9872)
- [crates] MSRV Badge [#9871](https://github.com/badges/shields/issues/9871)
- Add [galaxytoolshed] Version [#8249](https://github.com/badges/shields/issues/8249)
- fix default style docs for social badges [#9869](https://github.com/badges/shields/issues/9869)
- Dependency updates
## server-2024-01-01
The most important changes in this release for users hosting their own instance are:
The shields docker image is now based on node 20:
- deploy on node 20 [#9799](https://github.com/badges/shields/issues/9799)
It is now possible to use [authentication for DockerHub](https://github.com/badges/shields/blob/master/doc/server-secrets.md#dockerhub) to allow higher API rate limit or access to private repos:
- call [docker] with auth [#9803](https://github.com/badges/shields/issues/9803)
### New Badges
- [Thunderstore] Add Thunderstore Badges [#9782](https://github.com/badges/shields/issues/9782)
- Add [Raycast] Badge [#9801](https://github.com/badges/shields/issues/9801)
- [GITEA] add new gitea service (release/languages) [#9781](https://github.com/badges/shields/issues/9781)
- Add [NpmStatDownloads] Badge [#9783](https://github.com/badges/shields/issues/9783)
### Frontend Changes
- improve documentation for [dynamicxml] service [#9798](https://github.com/badges/shields/issues/9798)
- add description to interval enums [#9854](https://github.com/badges/shields/issues/9854)
- convert 'style' param to enum [#9853](https://github.com/badges/shields/issues/9853)
- Ensure social category badges are rendered with social style and logo; affects [gitlab keybase lemmy modrinth thunderstore twitch] gist github reddit [#9859](https://github.com/badges/shields/issues/9859)
### Fixes
- [pub] Use official version endpoint for pub-service [#9802](https://github.com/badges/shields/issues/9802)
- cache weblate badges for longer [#9786](https://github.com/badges/shields/issues/9786)
- [Discourse] Update schema keys to use plural form (`topic_count` -> `topics_count`) [#9778](https://github.com/badges/shields/issues/9778)
- cache some badges for longer [#9785](https://github.com/badges/shields/issues/9785)
- increase page size for github release badge by semver [#9818](https://github.com/badges/shields/issues/9818)
- Dependency updates
## server-2023-12-04
- move from @renovate/pep440 to @renovatebot/pep440 [#9614](https://github.com/badges/shields/issues/9614)
- deprecate/fix [ansible] galaxy services [#9648](https://github.com/badges/shields/issues/9648)
- call [pepy] with auth [#9748](https://github.com/badges/shields/issues/9748)
- add meaningful descriptions including keywords [#9715](https://github.com/badges/shields/issues/9715)
- Dependency updates
## server-2023-11-01
- fix greasyfork 404 bug [#9632](https://github.com/badges/shields/issues/9632)
- Hacktoberfest 2023 support - resolves #9636 [#9637](https://github.com/badges/shields/issues/9637)
- switch to fixed OpenCollective images [#9615](https://github.com/badges/shields/issues/9615)
- Dependency updates
## server-2023-10-02
- add python package total downloads from [pepy] badge [#9564](https://github.com/badges/shields/issues/9564)
- deprecate [redmine] plugin rating badges [#9568](https://github.com/badges/shields/issues/9568)
- fix [bower] version badge [#9567](https://github.com/badges/shields/issues/9567)
- Add [PythonVersionFromToml] shield [#9516](https://github.com/badges/shields/issues/9516)
- Add [dub] score badge service [#9549](https://github.com/badges/shields/issues/9549)
- Dependency updates
## server-2023-09-04
- Fix [testspace] badges [#9525](https://github.com/badges/shields/issues/9525)
- fix rSt code example [#9528](https://github.com/badges/shields/issues/9528)
- Add dynamic TOML support via [DynamicToml] Service [#9517](https://github.com/badges/shields/issues/9517)
- cache [pypi] downloads for longer [#9522](https://github.com/badges/shields/issues/9522)
- [twitter] --> x [#9496](https://github.com/badges/shields/issues/9496)
- [bundlejs] add badge for the npm package size [#9055](https://github.com/badges/shields/issues/9055)
- Switch [OpenCollective] badges to use GraphQL and auth [#9387](https://github.com/badges/shields/issues/9387)
- [Pulsar] Add Pulsar Badges for Stargazers & Downloads [#8767](https://github.com/badges/shields/issues/8767)
- Add [CurseForge] badges [#9252](https://github.com/badges/shields/issues/9252)
- deploy on node 18 [#9385](https://github.com/badges/shields/issues/9385)
- allow calling [github] without auth [#9427](https://github.com/badges/shields/issues/9427)
- Dependency updates
## server-2023-08-01
- Convert `examples` arrays to `openApi` objects (part 1) [#9320](https://github.com/badges/shields/issues/9320)
- Migrate from docs.rs' builds API to status API [#9422](https://github.com/badges/shields/issues/9422)
- [OpenVSX] Fix OpenVSX API call for unversioned package URLs [#9408](https://github.com/badges/shields/issues/9408)
- Add support for [Lemmy] [#9368](https://github.com/badges/shields/issues/9368)
- upgrade to npm 9 [#9323](https://github.com/badges/shields/issues/9323)
- Go back to default YouTube cache [#9372](https://github.com/badges/shields/issues/9372)
- Add [GitHubDiscussionsSearch] and GitHubRepoDiscussionsSearch service [#9340](https://github.com/badges/shields/issues/9340)
- Allow user to filter github tags and releases [#9193](https://github.com/badges/shields/issues/9193)
- don't URL encode slash in [githubactionsworkflow] badge [#9322](https://github.com/badges/shields/issues/9322)
- add a bit of border to select boxes [#9348](https://github.com/badges/shields/issues/9348)
- deprecate [snyk] badges [#9349](https://github.com/badges/shields/issues/9349)
- increase max-age on [docker] badges, again [#9350](https://github.com/badges/shields/issues/9350) [#9369](https://github.com/badges/shields/issues/9369)
- Dependency updates
## server-2023-07-02
By far the most significant change in this release is the long-awaited launch of the re-designed frontend:
- migrate frontend to docusaurus [#9014](https://github.com/badges/shields/issues/9014)
- fix a load of spacing issues in frontend content [#9281](https://github.com/badges/shields/issues/9281)
- set a sensible meta description [#9283](https://github.com/badges/shields/issues/9283)
- chore(frontend): open homepage feature links in new tab [#9300](https://github.com/badges/shields/issues/9300)
- adapt opencollective images to theme background [#9298](https://github.com/badges/shields/issues/9298)
- temp fix: wrap code examples tabs in narrow browser windows [#9302](https://github.com/badges/shields/issues/9302)
- add a bit of border to text boxes [#9324](https://github.com/badges/shields/issues/9324)
Other changes in this release:
- cache [dockerpulls] badges for an hour [#9343](https://github.com/badges/shields/issues/9343)
- Mention YouTube API services and link to Google Privacy Policy [#9339](https://github.com/badges/shields/issues/9339)
- allow negative timestamps in relative [date] badge [#9321](https://github.com/badges/shields/issues/9321)
- upgrade to graphql 16 [#9290](https://github.com/badges/shields/issues/9290)
- remove obsolete travis .org examples [#9284](https://github.com/badges/shields/issues/9284)
- increase max age on reddit badges [#9282](https://github.com/badges/shields/issues/9282)
- feat: Add author filter option for [GithubCommitActivity] [#9251](https://github.com/badges/shields/issues/9251)
- Fix: [GithubCommitActivity] invalid branch error handling [#9258](https://github.com/badges/shields/issues/9258)
- Implement a pattern for dealing with upstream APIs which are slow on the first hit; affects [endpoint] [#9233](https://github.com/badges/shields/issues/9233)
- Delete old deprecated services [#9254](https://github.com/badges/shields/issues/9254)
- feat: add 'canceled' status to netlify deploy badge [#9240](https://github.com/badges/shields/issues/9240)
- increase default cache on youtube badges [#9238](https://github.com/badges/shields/issues/9238)
- embiggen youtube cache, again [#9250](https://github.com/badges/shields/issues/9250)
- Dependency updates
## server-2023-06-01
- feat: Add total commits to [GitHubCommitActivity] [#9196](https://github.com/badges/shields/issues/9196)
- set a custom error on 429 [#9159](https://github.com/badges/shields/issues/9159)
- deprecate [travis].org badges [#9171](https://github.com/badges/shields/issues/9171)
- count private sponsors on [GithubSponsors] badge [#9170](https://github.com/badges/shields/issues/9170)
- Dependency updates
## server-2023-05-01
**Removal:** For users who need to maintain a Github Token pool, storage has been provided via the `RedisTokenPersistence` and `REDIS_URL` settings. This feature was deprecated in `server-2023-03-01`. As of this release, the `RedisTokenPersistence` backend is now removed. If you are using this feature, you will need to migrate to using the `SQLTokenPersistence` backend for storage and provide a postgres connection string via the `POSTGRES_URL` setting. [#8922](https://github.com/badges/shields/issues/8922)
- fail to start server if there are duplicate service names [#9099](https://github.com/badges/shields/issues/9099)
- [SourceForge] Added badges for SourceForge [#9078](https://github.com/badges/shields/issues/9078) [#9102](https://github.com/badges/shields/issues/9102)
- crates: Use `?include=` to reduce crates.io backend load [#9081](https://github.com/badges/shields/issues/9081)
- Dependency updates
## server-2023-04-02
- [JenkinsCoverage] Update Jenkins Code Coverage API for new plugin version [#9010](https://github.com/badges/shields/issues/9010)
- [CTAN] fallback to date if version is empty [#9036](https://github.com/badges/shields/issues/9036)
- Update to [CTAN] API version 2.0 [#9016](https://github.com/badges/shields/issues/9016)
- handle missing statistics array in [VisualStudioMarketplace] badges [#8985](https://github.com/badges/shields/issues/8985)
- [Netlify] upgrade colors for SVG parsing [#8971](https://github.com/badges/shields/issues/8971)
- Fix [Vcpkg] version service for different version fields [#8945](https://github.com/badges/shields/issues/8945)
- only try to close pool if one exists [#8947](https://github.com/badges/shields/issues/8947)
- misc minor fixes to [githubsize node pypi] [#8946](https://github.com/badges/shields/issues/8946)
- Dependency updates
## server-2023-03-01
**Deprecation:** For users who need to maintain a Github Token pool, storage has been provided via the `RedisTokenPersistence` and `REDIS_URL` settings. As of this release, the `RedisTokenPersistence` backend is now deprecated and will be removed in a future release. If you are using this feature, you will need to migrate to using the `SQLTokenPersistence` backend for storage and provide a postgres connection string via the `POSTGRES_URL` setting. [#8922](https://github.com/badges/shields/issues/8922)
- fix: for crates.io versions, use max_stable_version if it exists [#8687](https://github.com/badges/shields/issues/8687)
- don't autofocus search [#8927](https://github.com/badges/shields/issues/8927)
- Add [Vcpkg] version service [#8923](https://github.com/badges/shields/issues/8923)
- fix: Set uid/gid in docker image to 0 [#8908](https://github.com/badges/shields/issues/8908)
- expose port 443 in Dockerfile [#8889](https://github.com/badges/shields/issues/8889)
- Dependency updates
## server-2023-02-01
- replace [twitter] badge with static fallback [#8842](https://github.com/badges/shields/issues/8842)
- Add various [Polymart] badges [#8811](https://github.com/badges/shields/issues/8811)
- update [githubpipenv] tests/examples [#8797](https://github.com/badges/shields/issues/8797)
- deprecate [apm] service [#8773](https://github.com/badges/shields/issues/8773)
- deprecate lgtm [#8771](https://github.com/badges/shields/issues/8771)
- Dependency updates
## server-2023-01-01
- Breaking change: Routes for GitHub workflows badge have changed. See https://github.com/badges/shields/issues/8671 for more details
- Behaviour change: In this release we fixed a long standing bug. GitHub badges were previously not reading the base URL from the `config.service.baseUri`.
This release fixes that bug, bringing the code into line with the documented behaviour. This should not cause a behaviour change for most users,
but users who had previously set a value in `config.service.baseUri` which was previously ignored could see this now have an effect.
Users who configure their instance using env vars rather than yaml should see no change.
- Send `X-GitHub-Api-Version` when calling [GitHub] v3 API [#8669](https://github.com/badges/shields/issues/8669)
- add [VpmVersion] badge [#8755](https://github.com/badges/shields/issues/8755)
- Add [modrinth] game versions [#8673](https://github.com/badges/shields/issues/8673)
- fix debug logging of undefined query params [#8540](https://github.com/badges/shields/issues/8540), [#8757](https://github.com/badges/shields/issues/8757)
- fall back to classifiers if [pypi] license text is really long [#8690](https://github.com/badges/shields/issues/8690)
- allow passing key to [stackexchange] [#8539](https://github.com/badges/shields/issues/8539)
- Dependency updates
## server-2022-12-01
- fix: support logoColor to shield icons. [#8263](https://github.com/badges/shields/issues/8263)
- handle missing properties array in [VisualStudioMarketplaceVersion] [#8603](https://github.com/badges/shields/issues/8603)
- deprecate [wercker] service [#8642](https://github.com/badges/shields/issues/8642)
- Add [Coincap] Cryptocurrency badges [#8623](https://github.com/badges/shields/issues/8623)
- Add [modrinth] version [#8604](https://github.com/badges/shields/issues/8604)
- [factorio-mod-portal] services [#8625](https://github.com/badges/shields/issues/8625)
- [Coveralls] for GitLab [#8584](https://github.com/badges/shields/issues/8584), [#8644](https://github.com/badges/shields/issues/8644)
- Remove 'suggest badges' feature [#8311](https://github.com/badges/shields/issues/8311)
- Add [modrinth] followers [#8601](https://github.com/badges/shields/issues/8601)
- Update the [modrinth] API to v2 [#8600](https://github.com/badges/shields/issues/8600)
- tidy up [GitHubGist] routes [#8510](https://github.com/badges/shields/issues/8510)
- fix [flathub] version error handling [#8500](https://github.com/badges/shields/issues/8500)
- Dependency updates
## server-2022-11-01
- [Ansible] Add collection badge [#8578](https://github.com/badges/shields/issues/8578)
- [VisualStudioMarketplace] Add support to prerelease extensions version (Issue #8207) [#8561](https://github.com/badges/shields/issues/8561)
- feat: add [GitlabLastCommit] service [#8508](https://github.com/badges/shields/issues/8508)
- fix [swagger] service tests (allow 0 items in array) [#8564](https://github.com/badges/shields/issues/8564)
- fix codecov badge for non-default branch [#8565](https://github.com/badges/shields/issues/8565)
- Add [GitHubLastCommit] by committer badge [#8537](https://github.com/badges/shields/issues/8537)
- [GitHubReleaseDate] - published_at field [#8543](https://github.com/badges/shields/issues/8543)
- Fix [Testspace] with new "untested" value in case_counts array [#8544](https://github.com/badges/shields/issues/8544)
- fix: Support WAITING status for GitHub deployments [#8521](https://github.com/badges/shields/issues/8521)
- [Whatpulse] badge for a user and for a team [#8466](https://github.com/badges/shields/issues/8466)
- deprecate [pkgreview] service [#8499](https://github.com/badges/shields/issues/8499)
- Dependency updates
## server-2022-10-08
- deprecate [criterion] service [#8501](https://github.com/badges/shields/issues/8501)
- fix formatRelativeDate error handling; run [date] [#8497](https://github.com/badges/shields/issues/8497)
- allow/validate bitbucket_username / bitbucket_password in private config schema [#8472](https://github.com/badges/shields/issues/8472)
- fix [pub] points badge test and example [#8498](https://github.com/badges/shields/issues/8498)
- feat: add [GitlabLanguageCount] service [#8377](https://github.com/badges/shields/issues/8377)
- [GitHubGistStars] add GitHub Gist Stars [#8471](https://github.com/badges/shields/issues/8471)
- fix display/search of CII badge examples [#8473](https://github.com/badges/shields/issues/8473)
- feat: add 2022 support to GitHub Hacktoberfest [#8468](https://github.com/badges/shields/issues/8468)
- fix [GitLabCoverage] subgroup bug [#8401](https://github.com/badges/shields/issues/8401)
- implement ruby gems-specific version sort/color functions [#8434](https://github.com/badges/shields/issues/8434)
- Add `rc` to pre-release identifiers [#8435](https://github.com/badges/shields/issues/8435)
- add [GitHub] Number of commits between branches/tags/commits [#8394](https://github.com/badges/shields/issues/8394)
- add [Packagist] dependency version [#8371](https://github.com/badges/shields/issues/8371)
- fix Docker build status invalid response data bug [#8392](https://github.com/badges/shields/issues/8392)
- Dependency updates
## server-2022-09-04
- fix frontend compile for users running on Windows [#8350](https://github.com/badges/shields/issues/8350)
- [DockerSize] Docker image size multi arch [#8290](https://github.com/badges/shields/issues/8290)
- upgrade gatsby [#8334](https://github.com/badges/shields/issues/8334)
- Custom domains for [JitPack] artifacts [#8333](https://github.com/badges/shields/issues/8333)
- fix [dockerstars] service [#8316](https://github.com/badges/shields/issues/8316)
- [BountySource] Fix: Broken Badge generation for decimal activity values [#8315](https://github.com/badges/shields/issues/8315)
- feat: add [gitlabmergerequests] service [#8166](https://github.com/badges/shields/issues/8166)
- Fix terminology for [ROS] version service [#8292](https://github.com/badges/shields/issues/8292)
- feat: add [GitlabStars] service [#8209](https://github.com/badges/shields/issues/8209)
- Fix invalid `rst` format when `alt` or `target` is present [#8275](https://github.com/badges/shields/issues/8275)
- [GithubGistLastCommit] GitHub gist last commit [#8272](https://github.com/badges/shields/issues/8272)
- [GitHub] GitHub file size for a specific branch [#8262](https://github.com/badges/shields/issues/8262)
- Dependency updates
## server-2022-08-01
- [pypi] Add Framework Version Badges support [#8261](https://github.com/badges/shields/issues/8261)
- feat: add [GitlabForks] server [#8208](https://github.com/badges/shields/issues/8208)
- Update PyPI api according to https://warehouse.pypa.io/api-reference/json.html [#8251](https://github.com/badges/shields/issues/8251)
- Add [galaxytoolshed] Activity [#8164](https://github.com/badges/shields/issues/8164)
- [greasyfork] Add Greasy Fork rating badges [#8087](https://github.com/badges/shields/issues/8087)
- refactor(deps): Replace moment with dayjs [#8192](https://github.com/badges/shields/issues/8192)
- add spaces round pipe in [conda] badge [#8189](https://github.com/badges/shields/issues/8189)
- Add [ROS] version service [#8169](https://github.com/badges/shields/issues/8169)
- feat: add [gitlabissues] service [#8108](https://github.com/badges/shields/issues/8108)
- Dependency updates
## server-2022-07-03
- Add [galaxytoolshed] services [#8114](https://github.com/badges/shields/issues/8114)
- fix [gitlab] auth [#8145](https://github.com/badges/shields/issues/8145) [#8162](https://github.com/badges/shields/issues/8162)
- increase cache length on AUR version badge, run [AUR] [#8110](https://github.com/badges/shields/issues/8110)
- Use GraphQL to fix GitHub file count badges [github] [#8112](https://github.com/badges/shields/issues/8112)
- feat: add [gitlab] contributors service [#8084](https://github.com/badges/shields/issues/8084)
- [greasyfork] Add Greasy Fork service badges [#8080](https://github.com/badges/shields/issues/8080)
- Add [gitlablicense] services [#8024](https://github.com/badges/shields/issues/8024)
- [Spack] Package Manager: Update Domain [#8046](https://github.com/badges/shields/issues/8046)
- switch [jitpack] to use latestOk endpoint [#8041](https://github.com/badges/shields/issues/8041)
- Dependency updates
## server-2022-06-01
- Update GitLab logo (2022) [#7984](https://github.com/badges/shields/issues/7984)
- [GitHub] Added milestone property to GitHub issue details service [#7864](https://github.com/badges/shields/issues/7864)
- [Spack] Package Manager: Update Endpoint [#7957](https://github.com/badges/shields/issues/7957)
- Update Chocolatey API endpoint URL [#7952](https://github.com/badges/shields/issues/7952)
- [Flathub]Add downloads badge [#7724](https://github.com/badges/shields/issues/7724)
- replace the outdated Telegram logo with the newest [#7831](https://github.com/badges/shields/issues/7831)
- add [PUB] points badge [#7918](https://github.com/badges/shields/issues/7918)
- add [PUB] popularity badge [#7920](https://github.com/badges/shields/issues/7920)
- add [PUB] likes badge [#7916](https://github.com/badges/shields/issues/7916)
- Dependency updates
## server-2022-05-03
- [OSSFScorecard] Create scorecard badge service [#7687](https://github.com/badges/shields/issues/7687)
- Stringify [githublanguagecount] message [#7881](https://github.com/badges/shields/issues/7881)
- Stringify and trim whitespace from a few services [#7880](https://github.com/badges/shields/issues/7880)
- add labels to Dockerfile [#7862](https://github.com/badges/shields/issues/7862)
- handle missing 'fly-client-ip' [#7814](https://github.com/badges/shields/issues/7814)
- Dependency updates
## server-2022-04-03
- Breaking change: This release updates ioredis from v4 to v5.
If you are using redis for GitHub token pooling, redis connection strings of the form
`redis://junkusername:authpassword@example.com:1234` will need to be updated to
`redis://:authpassword@example.com:1234`. See the
[ioredis upgrade guide](https://github.com/luin/ioredis/wiki/Upgrading-from-v4-to-v5)
for further details.
- fix installation issue on npm >= 8.5.5 [#7809](https://github.com/badges/shields/issues/7809)
- two fixes for [packagist] schemas [#7782](https://github.com/badges/shields/issues/7782)
- allow requireCloudflare setting to work when hosted on fly.io [#7781](https://github.com/badges/shields/issues/7781)
- fix [pypi] badges when package has null license [#7761](https://github.com/badges/shields/issues/7761)
- Add a [pub] publisher badge [#7715](https://github.com/badges/shields/issues/7715)
- Switch Steam file size badge to informational color [#7722](https://github.com/badges/shields/issues/7722)
- Make W3C and Youtube documentation links clickable [#7721](https://github.com/badges/shields/issues/7721)
- Improve Wercker examples [#7720](https://github.com/badges/shields/issues/7720)
- Improve Cirrus CI examples [#7719](https://github.com/badges/shields/issues/7719)
- Support [CodeClimate] responses with multiple data items [#7716](https://github.com/badges/shields/issues/7716)
- Delete [TeamCityCoverage] and [BowerVersion] redirectors [#7718](https://github.com/badges/shields/issues/7718)
- Deprecate [Shippable] service [#7717](https://github.com/badges/shields/issues/7717)
- fix: restore version comparison updates from #4173 [#4254](https://github.com/badges/shields/issues/4254)
- [piwheels], filter out versions with no files [#7696](https://github.com/badges/shields/issues/7696)
- set a longer cacheLength on [librariesio] badges [#7692](https://github.com/badges/shields/issues/7692)
- improve python version formatting [#7682](https://github.com/badges/shields/issues/7682)
- Clarify GitHub All Contributors badge [#7690](https://github.com/badges/shields/issues/7690)
- Support [HexPM] packages with no stable release [#7685](https://github.com/badges/shields/issues/7685)
- Add Test at Scale Badge [#7612](https://github.com/badges/shields/issues/7612)
- [packagist] api v2 support [#7681](https://github.com/badges/shields/issues/7681)
- Add [piwheels] version badge [#7656](https://github.com/badges/shields/issues/7656)
- Dependency updates
## server-2022-03-01
- Add [Conan] version service (#7460)
- remove suspended [github] tokens from the pool [#7654](https://github.com/badges/shields/issues/7654)
- generate links without trailing : if port not set [#7655](https://github.com/badges/shields/issues/7655)
- Use the latest build status when checking docs.rs [#7613](https://github.com/badges/shields/issues/7613)
- Remove no download handling and add API warning to [Wordpress] badges [#7606](https://github.com/badges/shields/issues/7606)
- set a higher default cacheLength on rating/star category [#7587](https://github.com/badges/shields/issues/7587)
- Update [amo] to use v4 API, set custom `cacheLength`s [#7586](https://github.com/badges/shields/issues/7586)
- fix(amo): include trailing slash in API call [#7585](https://github.com/badges/shields/issues/7585)
- fix docker image user agent [#7582](https://github.com/badges/shields/issues/7582)
- Delete deprecated Codetally and continuousphp services [#7572](https://github.com/badges/shields/issues/7572)
- Deprecate [Requires] service [#7571](https://github.com/badges/shields/issues/7571)
- [AUR] Fix RPC URL [#7570](https://github.com/badges/shields/issues/7570)
- Dependency updates
## server-2022-02-01
- [Depfu] Add support for Gitlab [#7475](https://github.com/badges/shields/issues/7475)
- replace label in hn-user-karma with U/ [#7500](https://github.com/badges/shields/issues/7500)
- Support [Feedz] response with multiple pages without items [#7476](https://github.com/badges/shields/issues/7476)
- revert decamelize and humanize-string to old versions [#7449](https://github.com/badges/shields/issues/7449)
- Dependency updates
## server-2022-01-01
- minor [reddit] improvements [#7436](https://github.com/badges/shields/issues/7436)
- [HackerNews] Show User Karma [#7411](https://github.com/badges/shields/issues/7411)
- [YouTube] Drop support for removed dislikes [#7410](https://github.com/badges/shields/issues/7410)
- change closed GitHub issue color to purple [#7374](https://github.com/badges/shields/issues/7374)
- restore cors header injection from #4171 [#4255](https://github.com/badges/shields/issues/4255)
- [GithubPackageJson] Get version from monorepo subfolder package.json [#7350](https://github.com/badges/shields/issues/7350)
- Dependency updates
## server-2021-12-01
- Send better user-agent values [#7309](https://github.com/badges/shields/issues/7309)
Self-hosting users now send a user agent which indicates the server version and starts `shields (self-hosted)/` by default.
This can be configured using the env var `USER_AGENT_BASE`
- upgrade to node 16 [#7271](https://github.com/badges/shields/issues/7271)
- feat: deprecate dependabot badges [#7274](https://github.com/badges/shields/issues/7274)
- fix: npmversion tagged service test [#7269](https://github.com/badges/shields/issues/7269)
- feat: create new Test Results category [#7218](https://github.com/badges/shields/issues/7218)
- Migration from Request to Got for all HTTP requests is completed in this release
- Dependency updates
## server-2021-11-04
- migrate regularUpdate() from request-->got [#7215](https://github.com/badges/shields/issues/7215)
- migrate github badges to use got instead of request; affects [github librariesio] [#7212](https://github.com/badges/shields/issues/7212)
- deprecate David badges [#7197](https://github.com/badges/shields/issues/7197)
- fix: ensure libraries.io header values are processed numerically [#7196](https://github.com/badges/shields/issues/7196)
- Add authentication for Libraries.io-based badges, run [Libraries Bower] [#7080](https://github.com/badges/shields/issues/7080)
- fixes and tests for pipenv helpers [#7194](https://github.com/badges/shields/issues/7194)
- add GitLab Release badge, run all [GitLab] [#7021](https://github.com/badges/shields/issues/7021)
- set content-length header on badge responses [#7179](https://github.com/badges/shields/issues/7179)
- fix [github] release/tag/download schema [#7170](https://github.com/badges/shields/issues/7170)
- Supported nested groups on [GitLabPipeline] badge [#7159](https://github.com/badges/shields/issues/7159)
- Support nested groups on [GitLabTag] badge [#7158](https://github.com/badges/shields/issues/7158)
- Fixing incorrect JetBrains Plugin rating values for [JetBrainsRating] [#7140](https://github.com/badges/shields/issues/7140)
- support using release or tag name in [GitHub] Release version badge [#7075](https://github.com/badges/shields/issues/7075)
- feat: support branches in sonar badges [#7065](https://github.com/badges/shields/issues/7065)
- Add [Modrinth] total downloads badge [#7132](https://github.com/badges/shields/issues/7132)
- remove [github] admin routes [#7105](https://github.com/badges/shields/issues/7105)
- Dependency updates
## server-2021-10-04
- feat: add 2021 support to GitHub Hacktoberfest [#7086](https://github.com/badges/shields/issues/7086)
- Add [ClearlyDefined] service [#6944](https://github.com/badges/shields/issues/6944)
- handle null licenses in crates.io response schema, run [crates] [#7074](https://github.com/badges/shields/issues/7074)
- [OBS] add Open Build Service service-badge [#6993](https://github.com/badges/shields/issues/6993)
- Correction of badges url in self-hosting configuration with a custom port. Issue 7025 [#7036](https://github.com/badges/shields/issues/7036)
- fix: support gitlab token via env var [#7023](https://github.com/badges/shields/issues/7023)
- Add API-based support for [GitLab] badges, add new GitLab Tag badge [#6988](https://github.com/badges/shields/issues/6988)
- [freecodecamp]: allow + symbol in username [#7016](https://github.com/badges/shields/issues/7016)
- Rename Riot to Element in Matrix badge help [#6996](https://github.com/badges/shields/issues/6996)
- Fixed Reddit Negative Karma Issue [#6992](https://github.com/badges/shields/issues/6992)
- Dependency updates
## server-2021-09-01
- use multi-stage build to reduce size of docker images [#6938](https://github.com/badges/shields/issues/6938)
- remove disableStrictSsl param from [jenkins] [#6887](https://github.com/badges/shields/issues/6887)
- refactor(GitHubCommitActivity): switch to v4/GraphQL API [#6959](https://github.com/badges/shields/issues/6959)
- feat: add freecodecamp badge [#6958](https://github.com/badges/shields/issues/6958)
- use the right version of NPM in docker build [#6941](https://github.com/badges/shields/issues/6941)
- [TwitchExtensionVersion] New badge [#6900](https://github.com/badges/shields/issues/6900)
- enforce strict SSL checking for [coverity] [#6886](https://github.com/badges/shields/issues/6886)
- Update self hosting docs [#6877](https://github.com/badges/shields/issues/6877)
- Support optionalDependencies in [GithubPackageJson] [#6749](https://github.com/badges/shields/issues/6749)
- Dependency updates
## server-2021-08-01
- use v5 API for [AUR] badges [#6836](https://github.com/badges/shields/issues/6836)
- [Sonar] Fix invalid fetch query to sonarqube >=6.6 [#6636](https://github.com/badges/shields/issues/6636)
- Delegate discord logo to simple-icons, which matches the current branding [#6764](https://github.com/badges/shields/issues/6764)
- Re-apply 'Migrate request to got (part 1)' [#6755](https://github.com/badges/shields/issues/6755)
- Delete old deprecated badges [#6756](https://github.com/badges/shields/issues/6756)
- Replace opn-cli with open-cli [#6747](https://github.com/badges/shields/issues/6747)
- Verify that Node 14 is installed in development [#6748](https://github.com/badges/shields/issues/6748)
- Migrate from CommonJS to ESM [#6651](https://github.com/badges/shields/issues/6651)
- Add Wikiapiary Extension Badge [WikiapiaryInstalls] [#6678](https://github.com/badges/shields/issues/6678)
- deprecate [beerpay] [#6708](https://github.com/badges/shields/issues/6708)
- deprecate [microbadger] [#6709](https://github.com/badges/shields/issues/6709)
- [npmsioscore] Support npm score [#6630](https://github.com/badges/shields/issues/6630)
- Add [Weblate] badges [#6677](https://github.com/badges/shields/issues/6677)
- Dependency updates
## server-2021-07-01
- improve [MavenCentral], [MavenMetadata], and [GradlePluginPortal] [#6628](https://github.com/badges/shields/issues/6628)
- fix: fix regex to match [codecov]'s flags [#6655](https://github.com/badges/shields/issues/6655)
- fix usage style [#6638](https://github.com/badges/shields/issues/6638)
- update simple-icons to v5 with by-name lookup backwards compatibility [#6591](https://github.com/badges/shields/issues/6591)
- [GradlePluginPortal] add gradle plugin portal [#6449](https://github.com/badges/shields/issues/6449)
- upgrade some vulnerable packages [#6569](https://github.com/badges/shields/issues/6569)
- increase max-age for download and social badges [#6567](https://github.com/badges/shields/issues/6567)
- Dependency updates
## server-2021-06-01
- Changed creating badges to open a new Window/Tab [#6536](https://github.com/badges/shields/issues/6536)
- Make for-the-badge letter spacing more predictable, and rewrite layout logic [#5754](https://github.com/badges/shields/issues/5754)
- deprecate DockerBuild service [#6529](https://github.com/badges/shields/issues/6529)
- Remove rate limiting functionality [#6513](https://github.com/badges/shields/issues/6513)
- [GitHub] Move to 'funding' category [#5846](https://github.com/badges/shields/issues/5846)
- Add GitHub discussions total badge [GithubTotalDiscussions] [#6472](https://github.com/badges/shields/issues/6472)
- Add optional query parameter (include_prereleases) to [GemVersion] [#6451](https://github.com/badges/shields/issues/6451)
- Add [PingPong] Service [#6327](https://github.com/badges/shields/issues/6327)
- Dependency updates
## server-2021-05-01
- Add setting which allows setting a timeout on HTTP requests
This is configured with the new `REQUEST_TIMEOUT_SECONDS` setting. If a request takes longer
than this number of seconds a `408 Request Timeout` response will be returned.
- Deprecate [Bintray] service [#6423](https://github.com/badges/shields/issues/6423)
- Support git hash in [nexus] SNAPSHOT version [#6369](https://github.com/badges/shields/issues/6369)
- Replace 4183C4 with blue [#6366](https://github.com/badges/shields/issues/6366)
- [Youtube] Added channel view count and subscriber count badges [#6333](https://github.com/badges/shields/issues/6333)
- Fix Netlify badge by adding new color schema [#6340](https://github.com/badges/shields/issues/6340)
- [REUSE] Add service badges [#6330](https://github.com/badges/shields/issues/6330)
- Dependency updates
## server-2021-04-01
- Use NPM packages to provide fonts instead of Google Fonts [#6274](https://github.com/badges/shields/issues/6274)
- Prevent duplication of parameters in badge examples [#6272](https://github.com/badges/shields/issues/6272)
- Add docs for all types of releases [#6210](https://github.com/badges/shields/issues/6210)
- refresh self-hosting docs [#6273](https://github.com/badges/shields/issues/6273)
- allow missing 'goal' key in [liberapay] badges [#6258](https://github.com/badges/shields/issues/6258)
- use got to push influx metrics [#6257](https://github.com/badges/shields/issues/6257)
- Dependency updates
## server-2021-03-01
- ensure redirect target path is correctly encoded [#6229](https://github.com/badges/shields/issues/6229)
- [SecurityHeaders] Added a possibility for no follow redirects [#6212](https://github.com/badges/shields/issues/6212)
- catch URL parse error in [endpoint] badge [#6214](https://github.com/badges/shields/issues/6214)
- [Homebrew] Add homebrew downloads badge [#6209](https://github.com/badges/shields/issues/6209)
- update [pkgreview] url [#6189](https://github.com/badges/shields/issues/6189)
- Make [Twitch] a social badge [#6183](https://github.com/badges/shields/issues/6183)
- update [flathub] error handling [#6185](https://github.com/badges/shields/issues/6185)
- Add [Testspace] badges [#6162](https://github.com/badges/shields/issues/6162)
- Dependency updates
## server-2021-02-01
- Docs.rs badge (#6098)
- Fix feedz service in case the package page gets paginated (#6101)
- [Bitbucket] Server Adding Auth Tokens and Resolving Pull Request api … (#6076)
- Dependency updates
## server-2021-01-18
- Gotta start somewhere

View File

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

View File

@@ -8,7 +8,10 @@ financial contributions, issues, and pull requests!
### Financial contributions
We welcome financial contributions in full transparency on our
[open collective](https://opencollective.com/shields).
[open collective](https://opencollective.com/shields). Anyone can file an
expense. If the expense makes sense for the development of the community, it
will be "merged" into the ledger of our open collective by the core
contributors and the person who filed the expense will be reimbursed.
### Contributing code
@@ -46,9 +49,8 @@ simple changes, like badge additions. These are usually tagged with
Please review [these impeccable guidelines][code review guidelines].
You can monitor [issues][], [discussions][] and the [chat room][], and help
other people who have questions about contributing to Shields, or using it
for their projects.
You can monitor [issues][] and the [chat room][], and help other people who
have questions about contributing to Shields, or using it for their projects.
Feel free to reach out to one of the [maintainers][]
if you need help getting started.
@@ -56,7 +58,6 @@ if you need help getting started.
[service badge pr tag]: https://github.com/badges/shields/pulls?q=is%3Apr+is%3Aopen+label%3Aservice-badge
[code review guidelines]: https://kickstarter.engineering/a-guide-to-mindful-communication-in-code-reviews-48aab5282e5e
[issues]: https://github.com/badges/shields/issues
[discussions]: https://github.com/badges/shields/discussions
[chat room]: https://discordapp.com/invite/HjJCwm5
[maintainers]: https://github.com/badges/shields#project-leaders
@@ -74,29 +75,20 @@ don't see it, feel free to [open a new issue][open an issue].
[open an issue]: https://github.com/badges/shields/issues/new/choose
### Requesting new logos
We consume logos via [the SimpleIcons project][simple-icons github], and
encourage you to contribute logos there. Please review their
[guidance][simple-icons contributing] before doing so.
[simple-icons github]: https://github.com/simple-icons/simple-icons
[simple-icons contributing]: https://github.com/simple-icons/simple-icons/blob/develop/CONTRIBUTING.md
### Spreading the word
Feel free to star the repository. This will help increase the visibility of the project, therefore attracting more users and contributors to Shields!
We're also asking for [donations](https://opencollective.com/shields) from developers who use and love Shields, please spread the word!
We're also asking for [one-time \$10 donations](https://opencollective.com/shields) from developers who use and love Shields, please spread the word!
## Getting help
There are three places to get help:
1. If you're new to the project, a good place to start is the [tutorial][].
2. If you need help getting started or implementing a change, [start a discussion][discussions]
2. If you need help getting started or implementing a change, [open an issue][]
with your question. We promise it's okay to do that. If there is already an
issue open for the feature you're working on, you can post there directly.
issue open for the feature you're working on, you can post there.
3. You can also join the [chat room][] and ask your question there.
[tutorial]: doc/TUTORIAL.md
@@ -104,18 +96,11 @@ There are three places to get help:
## Badge guidelines
- Shields.io hosts integrations for services which are primarily
used by developers or which are widely used by developers.
used by developers or which are widely used by developers
- The left-hand side of a badge should not advertise. It should be a lowercase _noun_
succinctly describing the meaning of the right-hand side.
- Except for badges using the `social` style, logos and links should be _turned off by
- Except for badges using the `social` style, logos should be _turned off by
default_.
- Badges should not obtain data from undocumented or reverse-engineered API endpoints.
- Badges should not obtain data by scraping web pages - these are likely to break frequently.
Whereas API publishers are incentivised to maintain a stable platform for their users,
authors of web pages have no such incentive.
- Badges may require users to specify a token in the badge URL as long it is scoped only to
fetching information and doesn't expose any sensitive information. Generating a token with the
correct scope must be clearly documented.
## Badge URLs
@@ -137,22 +122,18 @@ Prettier before a commit by default.
### Tests
When adding or changing a service [please write tests][service-tests], and ensure the [title of your Pull Requests follows the required conventions](#running-service-tests-in-pull-requests) to ensure your tests are executed.
When adding or changing a service [please write tests][service-tests].
When opening a pull request, include your service name in brackets in the pull
request title. That way, those service tests will run in CI.
e.g. **[Travis] Fix timeout issues**
When changing other code, please add unit tests.
The integration tests are not run by default. For most contributions it is OK to skip these unless you're working directly on the code for storing the GitHub token pool in postgres.
To run the integration tests:
- You must have PostgreSQL installed. Use `brew install postgresql`, `apt-get install postgresql`, etc.
- Set a connection string either with an env var `POSTGRES_URL=postgresql://user:pass@127.0.0.1:5432/db_name` or by using
```yaml
private:
postgres_url: 'postgresql://user:pass@127.0.0.1:5432/db_name'
```
in a yaml config file.
- Run `npm run migrate up` to apply DB migrations
- Run `npm run test:integration` to run the tests
To run the integration tests, you must have redis installed and in your PATH.
Use `brew install redis`, `yum install redis`, etc. The test runner will
start the server automatically.
[service-tests]: https://github.com/badges/shields/blob/master/doc/service-tests.md
@@ -160,34 +141,6 @@ To run the integration tests:
There is a [High-level code walkthrough](doc/code-walkthrough.md) describing the layout of the project.
## Pull Requests
### Logos
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.
We have [documentation for logo usage](doc/logos.md) which includes [contribution guidance](doc/logos.md#contributing-logos)

View File

@@ -1,36 +1,24 @@
FROM node:20-alpine AS builder
FROM node:8-alpine
RUN mkdir -p /usr/src/app
RUN mkdir /usr/src/app/private
WORKDIR /usr/src/app
COPY package.json package-lock.json /usr/src/app/
# Without the badge-maker package.json and CLI script in place, `npm ci` will fail.
COPY badge-maker /usr/src/app/badge-maker/
# Without the gh-badges package.json and CLI script in place, `npm ci` will fail.
COPY gh-badges /usr/src/app/gh-badges/
RUN npm install -g "npm@^10"
# 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
COPY . /usr/src/app
RUN npm run build
RUN npm prune --omit=dev
RUN npm prune --production
RUN npm cache clean --force
# Use multi-stage build to reduce size
FROM node:20-alpine
ARG version=dev
ENV DOCKER_SHIELDS_VERSION=$version
LABEL version=$version
LABEL fly.version=$version
# Run the server using production configs.
ENV NODE_ENV=production
ENV NODE_ENV production
WORKDIR /usr/src/app
COPY --from=builder --chown=0:0 /usr/src/app /usr/src/app
CMD node server
CMD ["node", "server"]
EXPOSE 80 443
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

175
README.md
View File

@@ -1,25 +1,36 @@
<p align="center">
<img src="https://raw.githubusercontent.com/badges/shields/master/readme-logo.svg?sanitize=true"
<img src="https://raw.githubusercontent.com/badges/shields/master/frontend/images/logo.svg?sanitize=true"
height="130">
</p>
<p align="center">
<a href="https://shields.io/community#backers" alt="Backers on Open Collective">
<a href="https://github.com/badges/shields/graphs/contributors" alt="Contributors">
<img src="https://img.shields.io/github/contributors/badges/shields" /></a>
<a href="#backers" alt="Backers on Open Collective">
<img src="https://img.shields.io/opencollective/backers/shields" /></a>
<a href="https://shields.io/community#sponsors" alt="Sponsors on Open Collective">
<a href="#sponsors" alt="Sponsors on Open Collective">
<img src="https://img.shields.io/opencollective/sponsors/shields" /></a>
<a href="https://github.com/badges/shields/pulse" alt="Activity">
<img src="https://img.shields.io/github/commit-activity/m/badges/shields" /></a>
<a href="https://github.com/badges/shields/discussions" alt="Discussions">
<img src="https://img.shields.io/github/discussions/badges/shields" /></a>
<a href="https://github.com/badges/shields/actions/workflows/daily-tests.yml">
<img src="https://img.shields.io/github/actions/workflow/status/badges/shields/daily-tests.yml?label=daily%20tests"
alt="Daily Tests Status"></a>
<a href="https://circleci.com/gh/badges/shields/tree/master">
<img src="https://img.shields.io/circleci/project/github/badges/shields/master" alt="build status"></a>
<a href="https://circleci.com/gh/badges/daily-tests">
<img src="https://img.shields.io/circleci/project/github/badges/daily-tests?label=service%20tests"
alt="service-test status"></a>
<a href="https://coveralls.io/github/badges/shields">
<img src="https://img.shields.io/coveralls/github/badges/shields"
alt="Code 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">
<img src="https://img.shields.io/discord/308323056592486420?logo=discord&logoColor=white"
alt="Chat on Discord"></a>
<img src="https://img.shields.io/discord/308323056592486420?logo=discord"
alt="chat on Discord"></a>
<a href="https://twitter.com/intent/follow?screen_name=shields_io">
<img src="https://img.shields.io/twitter/follow/shields_io?style=social&logo=twitter"
alt="follow on Twitter"></a>
</p>
This is home to [Shields.io][shields.io], a service for concise, consistent,
@@ -27,27 +38,21 @@ and legible badges in SVG and raster format, which can easily be included in
GitHub readmes or any other web page. The service supports dozens of
continuous integration services, package registries, distributions, app
stores, social networks, code coverage services, and code analysis services.
Every month it serves over 1.6 billion images and is used by some of the
world's most popular open-source projects, [VS Code][vscode], [Vue.js][vue]
and [Bootstrap][bootstrap] to name a few.
[vscode]: https://github.com/Microsoft/vscode
[vue]: https://github.com/vuejs/vue
[bootstrap]: https://github.com/twbs/bootstrap
Every month it serves over 470 million images.
This repo hosts:
- The [Shields.io][shields.io] frontend and server code
- An [NPM library for generating badges][badge-maker]
- [documentation][badge-maker-docs]
- [changelog][badge-maker-changelog]
- An [NPM library for generating badges][gh-badges]
- [documentation][gh-badges-docs]
- [changelog][gh-badges-changelog]
- The [badge design specification][badge-spec]
[shields.io]: https://shields.io/
[badge-maker]: https://www.npmjs.com/package/badge-maker
[gh-badges]: https://www.npmjs.com/package/gh-badges
[badge-spec]: https://github.com/badges/shields/tree/master/spec
[badge-maker-docs]: https://github.com/badges/shields/tree/master/badge-maker/README.md
[badge-maker-changelog]: https://github.com/badges/shields/tree/master/badge-maker/CHANGELOG.md
[gh-badges-docs]: https://github.com/badges/shields/tree/master/gh-badges/README.md
[gh-badges-changelog]: https://github.com/badges/shields/tree/master/gh-badges/CHANGELOG.md
## Examples
@@ -60,18 +65,14 @@ This repo hosts:
- amount of [Liberapay](https://liberapay.com/) donations per week: ![receives](https://img.shields.io/badge/receives-2.00%20USD%2Fweek-yellow)
- Python package downloads: ![downloads](https://img.shields.io/badge/downloads-13k%2Fmonth-brightgreen)
- Chrome Web Store extension rating: ![rating](https://img.shields.io/badge/rating-★★★★☆-brightgreen)
- Uptime Robot uptime percentage: ![uptime](https://img.shields.io/badge/uptime-100%25-brightgreen)
- [Uptime Robot](https://uptimerobot.com) percentage: ![uptime](https://img.shields.io/badge/uptime-100%25-brightgreen)
[Make your own badges!][custom badges]
(Quick example: `https://img.shields.io/badge/left-right-f39f37`)
[custom badges]: https://img.shields.io/badges/static-badge
Browse a [complete list of badges][shields.io].
### Quickstart
Browse a [complete list of badges][shields.io] and locate a particular badge by using the search bar or by browsing the categories. Click on the badge to fill in required data elements for that badge type (like your username or repo) and optionally customize (label, colors etc.). And it's ready for use!
Use the button at the bottom to copy your badge url or snippet, which can then be added to places like your GitHub readme files or other web pages.
[custom badges]: http://shields.io/#your-badge
## Contributing
@@ -81,23 +82,20 @@ and pull requests! You can peruse the [contributing guidelines][contributing].
When adding or changing a service [please add tests][service-tests].
This project has quite a backlog of suggestions! If you're new to the project,
maybe you'd like to open a pull request to address one of them.
You can read a [tutorial on how to add a badge][tutorial].
maybe you'd like to open a pull request to address one of them:
[![GitHub issues by-label](https://img.shields.io/github/issues/badges/shields/good%20first%20issue)](https://github.com/badges/shields/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
If you intend on reporting or contributing a fix related to security vulnerabilities, please first refer to our [security policy][security].
You can read a [tutorial on how to add a badge][tutorial].
[service-tests]: https://github.com/badges/shields/blob/master/doc/service-tests.md
[tutorial]: https://github.com/badges/shields/blob/master/doc/TUTORIAL.md
[contributing]: https://github.com/badges/shields/blob/master/CONTRIBUTING.md
[security]: https://github.com/badges/shields/blob/master/SECURITY.md
[tutorial]: doc/TUTORIAL.md
[contributing]: CONTRIBUTING.md
## Development
1. Install Node 20 or later. You can use the [package manager][] of your choice.
Tests need to pass in Node 20 and 22.
1. Install Node 8 or later. You can use the [package manager][] of your choice.
Tests need to pass in Node 8 and 10.
2. Clone this repository.
3. Run `npm ci` to install the dependencies.
4. Run `npm start` to start the badge server and the frontend dev server.
@@ -105,9 +103,9 @@ If you intend on reporting or contributing a fix related to security vulnerabili
When server source files change, the badge server should automatically restart
itself (using [nodemon][]). When the frontend files change, the frontend dev
server (`docusaurus start`) should also automatically reload. However the badge
server (`gatsby dev`) should also automatically reload. However the badge
definitions are built only before the server first starts. To regenerate those,
either run `npm run prestart` or manually restart the server.
either run `npm run defs` or manually restart the server.
To debug a badge from the command line, run `npm run badge -- /npm/v/nock`.
It also works with full URLs like
@@ -126,46 +124,30 @@ Please report any Gitpod bugs, questions, or suggestions in issue
[Snapshot tests][] ensure we don't inadvertently make changes that affect the
SVG or JSON output. When deliberately changing the output, run
`SNAPSHOT_DRY=1 npm run test:package` to preview changes to the saved
snapshots, and `SNAPSHOT_UPDATE=1 npm run test:package` to update them.
`SNAPSHOT_DRY=1 npm run test:js:server` to preview changes to the saved
snapshots, and `SNAPSHOT_UPDATE=1 npm run test:js:server` to update them.
The server can be configured to use [Sentry][] ([configuration][sentry configuration]) and [Prometheus][] ([configuration][prometheus configuration]).
Our [full test suite][full test suite] as well as [code coverage][code coverage] are run on a daily basis.
Daily tests, including a full run of the service tests and overall code coverage, are run via [badges/daily-tests][daily-tests].
[package manager]: https://nodejs.org/en/download/package-manager/
[gitpod]: https://www.gitpod.io/
[snapshot tests]: https://glebbahmutov.com/blog/snapshot-testing/
[prometheus]: https://prometheus.io/
[prometheus configuration]: https://github.com/badges/shields/blob/master/doc/self-hosting.md#prometheus
[prometheus configuration]: doc/self-hosting.md#prometheus
[sentry]: https://sentry.io/
[sentry configuration]: https://github.com/badges/shields/blob/master/doc/self-hosting.md#sentry
[sentry configuration]: doc/self-hosting.md#sentry
[daily-tests]: https://github.com/badges/daily-tests
[nodemon]: https://nodemon.io/
[nodemon debug]: https://github.com/Microsoft/vscode-recipes/tree/master/nodemon
[vs code]: https://code.visualstudio.com/
[full test suite]: https://github.com/badges/shields/actions/workflows/daily-tests.yml
[code coverage]: https://coveralls.io/github/badges/shields
## Hosting your own server
There is documentation about [hosting your own server][self-hosting].
[self-hosting]: https://github.com/badges/shields/blob/master/doc/self-hosting.md
## Related projects
[![Awesome](https://awesome.re/badge.svg)](https://awesome.re)
Status badges are used widely across open-source and private software projects.
Academics have studied the "signal" badges provide about software project
quality. There are many existing libraries for rendering these badges, and
alternatives to the hosted Shields badge service. [awesome-badges][] is a
curated collection of such resources.
[Contributions][contributing to awesome-badges] may be considered there.
(The presence of a project in that collection should not be interpreted as an endorsement nor promotion from the Shields project)
[awesome-badges]: https://github.com/badges/awesome-badges
[contributing to awesome-badges]: https://github.com/badges/awesome-badges/blob/main/CONTRIBUTING.md
[self-hosting]: doc/self-hosting.md
## History
@@ -190,33 +172,70 @@ You can read more about [the project's inception][thread],
[olivierlacan]: https://github.com/olivierlacan
[espadrine]: https://github.com/espadrine
[old-gh-badges]: https://github.com/badges/gh-badges
[motivation]: https://github.com/badges/shields/blob/master/spec/motivation.md
[spec]: https://github.com/badges/shields/blob/master/spec/SPECIFICATION.md
[motivation]: spec/motivation.md
[spec]: spec/SPECIFICATION.md
[thread]: https://github.com/h5bp/lazyweb-requests/issues/150
## Project leaders
Maintainers:
- [calebcartwright](https://github.com/calebcartwright)
- [chris48s](https://github.com/chris48s)
- [jNullj](https://github.com/jnullj)
- [paulmelnikow](https://github.com/paulmelnikow)
- [PyvesB](https://github.com/PyvesB)
- [calebcartwright](https://github.com/calebcartwright) (core team)
- [chris48s](https://github.com/chris48s) (core team)
- [Daniel15](https://github.com/Daniel15) (core team)
- [espadrine](https://github.com/espadrine) (core team)
- [paulmelnikow](https://github.com/paulmelnikow) (core team)
- [platan](https://github.com/platan) (core team)
- [PyvesB](https://github.com/PyvesB) (core team)
- [RedSparr0w](https://github.com/RedSparr0w) (core team)
Operations:
- [espadrine](https://github.com/espadrine) (sysadmin)
- [paulmelnikow](https://github.com/paulmelnikow) (limited access)
Alumni:
- [Daniel15](https://github.com/Daniel15)
- [espadrine](https://github.com/espadrine)
- [olivierlacan](https://github.com/olivierlacan)
- [platan](https://github.com/platan)
- [RedSparr0w](https://github.com/RedSparr0w)
## Related projects
- [badgerbadgerbadger gem][gem]
- [pybadges python library][pybadges]
[gem]: https://github.com/badges/badgerbadgerbadger
[pybadges]: https://github.com/google/pybadges
## License
All assets and code are under the [CC0 LICENSE](LICENSE) and in the public
domain unless specified otherwise.
## Community
The assets in `logo/` are trademarks of their respective companies and are
under their terms and license.
Thanks to the people and companies who donate money, services or time to keep the project running. [https://shields.io/community](https://shields.io/community)
## Contributors
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
<a href="https://github.com/badges/shields/graphs/contributors"><img src="https://opencollective.com/shields/contributors.svg?width=890" /></a>
## Backers
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/shields#backer)]
<a href="https://opencollective.com/shields#backers" target="_blank"><img src="https://opencollective.com/shields/backers.svg?width=890"></a>
## Sponsors
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/shields#sponsor)]
<a href="https://opencollective.com/shields/sponsor/0/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/shields/sponsor/1/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/shields/sponsor/2/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/shields/sponsor/3/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/shields/sponsor/4/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/shields/sponsor/5/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/shields/sponsor/6/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/shields/sponsor/7/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/shields/sponsor/8/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/shields/sponsor/9/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/9/avatar.svg"></a>

View File

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

View File

@@ -0,0 +1,19 @@
exports['The badge generator SVG should always produce the same SVG (unless we have changed something!) 1'] = `
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="90" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="a"><rect width="90" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#a)"><path fill="#555" d="M0 0h45v20H0z"/><path fill="#4c1" d="M45 0h45v20H45z"/><path fill="url(#b)" d="M0 0h90v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110"> <text x="235" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">cactus</text><text x="235" y="140" transform="scale(.1)" textLength="350">cactus</text><text x="665" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="665" y="140" transform="scale(.1)" textLength="350">grown</text></g> </svg>
`
exports['The badge generator badges with logos should always produce the same badge shields GitHub logo custom color (whitesmoke) 1'] = `
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="113" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="a"><rect width="113" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#a)"><path fill="#555" d="M0 0h54v20H0z"/><path fill="#4c1" d="M54 0h59v20H54z"/><path fill="url(#b)" d="M0 0h113v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110"><image x="5" y="3" width="14" height="14" xlink:href="github"/> <text x="365" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">label</text><text x="365" y="140" transform="scale(.1)" textLength="270">label</text><text x="825" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="490">message</text><text x="825" y="140" transform="scale(.1)" textLength="490">message</text></g> </svg>
`
exports['The badge generator badges with logos should always produce the same badge shields GitHub logo default color (#333333) 1'] = `
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="113" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="a"><rect width="113" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#a)"><path fill="#555" d="M0 0h54v20H0z"/><path fill="#4c1" d="M54 0h59v20H54z"/><path fill="url(#b)" d="M0 0h113v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110"><image x="5" y="3" width="14" height="14" xlink:href="github"/> <text x="365" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">label</text><text x="365" y="140" transform="scale(.1)" textLength="270">label</text><text x="825" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="490">message</text><text x="825" y="140" transform="scale(.1)" textLength="490">message</text></g> </svg>
`
exports['The badge generator badges with logos should always produce the same badge simple-icons javascript logo custom color (rgba(46,204,113,0.8)) 1'] = `
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="113" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="a"><rect width="113" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#a)"><path fill="#555" d="M0 0h54v20H0z"/><path fill="#4c1" d="M54 0h59v20H54z"/><path fill="url(#b)" d="M0 0h113v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110"><image x="5" y="3" width="14" height="14" xlink:href="javascript"/> <text x="365" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">label</text><text x="365" y="140" transform="scale(.1)" textLength="270">label</text><text x="825" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="490">message</text><text x="825" y="140" transform="scale(.1)" textLength="490">message</text></g> </svg>
`
exports['The badge generator badges with logos should always produce the same badge simple-icons javascript logo default color (#F7DF1E) 1'] = `
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="113" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="a"><rect width="113" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#a)"><path fill="#555" d="M0 0h54v20H0z"/><path fill="#4c1" d="M54 0h59v20H54z"/><path fill="url(#b)" d="M0 0h113v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110"><image x="5" y="3" width="14" height="14" xlink:href="javascript"/> <text x="365" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">label</text><text x="365" y="140" transform="scale(.1)" textLength="270">label</text><text x="825" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="490">message</text><text x="825" y="140" transform="scale(.1)" textLength="490">message</text></g> </svg>
`

File diff suppressed because it is too large Load Diff

42
app.json Normal file
View File

@@ -0,0 +1,42 @@
{
"name": "Shields",
"description": "Concise, consistent, and legible badges in SVG and raster format.",
"keywords": ["badge", "github", "svg", "status"],
"website": "https://shields.io/",
"repository": "https://github.com/badges/shields",
"logo": "http://shields.io/favicon.png",
"env": {
"CYPRESS_INSTALL_BINARY": {
"description": "Disable the cypress binary installation",
"value": "0",
"required": false
},
"HUSKY_SKIP_INSTALL": {
"description": "Skip the husky git hook setup",
"value": "1",
"required": false
},
"WHEELMAP_TOKEN": {
"description": "Configure the token to be used for the Wheelmap service.",
"required": false
},
"GH_TOKEN": {
"description": "Configure the token to be used for the GitHub services.",
"required": false
},
"TWITCH_CLIENT_ID": {
"description": "Configure the client id to be used for the Twitch service.",
"required": false
},
"TWITCH_CLIENT_SECRET": {
"description": "Configure the client secret to be used for the Twitch service.",
"required": false
}
},
"formation": {
"web": {
"quantity": 1,
"size": "free"
}
}
}

View File

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

View File

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

View File

@@ -1,44 +0,0 @@
'use strict'
import path from 'path'
import { fileURLToPath } from 'url'
import { spawn } from 'child-process-promise'
import { expect, use } from 'chai'
import sinonChai from 'sinon-chai'
use(sinonChai)
const dirName = path.dirname(fileURLToPath(import.meta.url))
function runCli(args) {
return spawn('node', [path.join(dirName, 'badge-cli.js'), ...args], {
capture: ['stdout'],
})
}
describe('The CLI', function () {
it('should provide a help message', async function () {
const { stdout } = await runCli([])
expect(stdout.startsWith('Usage')).to.be.true
})
it('should produce default badges', async function () {
const { default: isSvg } = await import('is-svg')
const { stdout } = await runCli(['cactus', 'grown'])
expect(stdout)
.to.satisfy(isSvg)
.and.to.include('cactus')
.and.to.include('grown')
})
it('should produce colorschemed badges', async function () {
const { default: isSvg } = await import('is-svg')
const { stdout } = await runCli(['cactus', 'grown', ':green'])
expect(stdout).to.satisfy(isSvg)
})
it('should produce right-color badges', async function () {
const { default: isSvg } = await import('is-svg')
const { stdout } = await runCli(['cactus', 'grown', '#abcdef'])
expect(stdout).to.satisfy(isSvg).and.to.include('#abcdef')
})
})

View File

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

View File

@@ -1,5 +0,0 @@
const DEFAULT_LOGO_HEIGHT = 14
module.exports = {
DEFAULT_LOGO_HEIGHT,
}

View File

@@ -1,117 +0,0 @@
'use strict'
/**
* @module badge-maker
*/
const _makeBadge = require('./make-badge')
class ValidationError extends Error {}
function _validate(format) {
if (format !== Object(format)) {
throw new ValidationError('makeBadge takes an argument of type object')
}
if (!('message' in format)) {
throw new ValidationError('Field `message` is required')
}
const stringFields = ['labelColor', 'color', 'message', 'label', 'logoBase64']
stringFields.forEach(function (field) {
if (field in format && typeof format[field] !== 'string') {
throw new ValidationError(`Field \`${field}\` must be of type string`)
}
})
if ('links' in format) {
if (!Array.isArray(format.links)) {
throw new ValidationError('Field `links` must be an array of strings')
} else {
if (format.links.length > 2) {
throw new ValidationError(
'Field `links` must not have more than 2 elements',
)
}
format.links.forEach(function (field) {
if (typeof field !== 'string') {
throw new ValidationError('Field `links` must be an array of strings')
}
})
}
}
const styleValues = [
'plastic',
'flat',
'flat-square',
'for-the-badge',
'social',
]
if ('style' in format && !styleValues.includes(format.style)) {
throw new ValidationError(
`Field \`style\` must be one of (${styleValues.toString()})`,
)
}
if ('idSuffix' in format && !/^[a-zA-Z0-9\-_]*$/.test(format.idSuffix)) {
throw new ValidationError(
'Field `idSuffix` must contain only numbers, letters, -, and _',
)
}
}
function _clean(format) {
const expectedKeys = [
'label',
'message',
'labelColor',
'color',
'style',
'logoBase64',
'links',
'idSuffix',
]
const cleaned = {}
Object.keys(format).forEach(key => {
if (format[key] != null && key === 'logoBase64') {
cleaned.logo = format[key]
} else if (format[key] != null && expectedKeys.includes(key)) {
cleaned[key] = format[key]
} else {
throw new ValidationError(
`Unexpected field '${key}'. Allowed values are (${expectedKeys.toString()})`,
)
}
})
// Legacy.
cleaned.label = cleaned.label || ''
return cleaned
}
/**
* Create a badge
*
* @param {object} format Object specifying badge data
* @param {string} format.label (Optional) Badge label (e.g: 'build')
* @param {string} format.message (Required) Badge message (e.g: 'passing')
* @param {string} format.labelColor (Optional) Label color
* @param {string} format.color (Optional) Message color
* @param {string} format.style (Optional) Visual style (e.g: 'flat')
* @param {string} format.logoBase64 (Optional) Logo data URL
* @param {Array} format.links (Optional) Links array (e.g: ['https://example.com', 'https://example.com'])
* @param {string} format.idSuffix (Optional) Suffix for IDs, e.g. 1, 2, and 3 for three invocations that will be used on the same page.
* @returns {string} Badge in SVG format
* @see https://github.com/badges/shields/tree/master/badge-maker/README.md
*/
function makeBadge(format) {
_validate(format)
const cleanedFormat = _clean(format)
return _makeBadge(cleanedFormat)
}
module.exports = {
makeBadge,
ValidationError,
}

View File

@@ -1,111 +0,0 @@
'use strict'
import { expect } from 'chai'
import { makeBadge, ValidationError } from './index.js'
describe('makeBadge function', function () {
it('should produce badge with valid input', async function () {
const { default: isSvg } = await import('is-svg')
expect(
makeBadge({
label: 'build',
message: 'passed',
}),
).to.satisfy(isSvg)
expect(
makeBadge({
message: 'passed',
}),
).to.satisfy(isSvg)
expect(
makeBadge({
label: 'build',
message: 'passed',
color: 'green',
style: 'flat',
}),
).to.satisfy(isSvg)
expect(
makeBadge({
label: 'build',
message: 'passed',
color: 'green',
style: 'flat',
labelColor: 'blue',
logoBase64: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
links: ['https://example.com', 'https://example.com'],
}),
)
.to.satisfy(isSvg)
// explicitly make an assertion about logoBase64
// this param is not a straight passthrough
.and.to.include('data:image/svg+xml;base64,PHN2ZyB4bWxu')
})
it('should throw a ValidationError with invalid inputs', function () {
;[null, undefined, 7, 'foo', 4.25].forEach(x => {
console.log(x)
expect(() => makeBadge(x)).to.throw(
ValidationError,
'makeBadge takes an argument of type object',
)
})
expect(() => makeBadge({})).to.throw(
ValidationError,
'Field `message` is required',
)
expect(() => makeBadge({ label: 'build' })).to.throw(
ValidationError,
'Field `message` is required',
)
expect(() =>
makeBadge({ label: 'build', message: 'passed', labelColor: 7 }),
).to.throw(ValidationError, 'Field `labelColor` must be of type string')
expect(() =>
makeBadge({ label: 'build', message: 'passed', logoBase64: 7 }),
).to.throw(ValidationError, 'Field `logoBase64` must be of type string')
expect(() =>
makeBadge({ label: 'build', message: 'passed', links: 'test' }),
).to.throw(ValidationError, 'Field `links` must be an array of strings')
expect(() =>
makeBadge({ label: 'build', message: 'passed', links: [1] }),
).to.throw(ValidationError, 'Field `links` must be an array of strings')
expect(() =>
makeBadge({ label: 'build', message: 'passed', links: ['1', '2', '3'] }),
).to.throw(
ValidationError,
'Field `links` must not have more than 2 elements',
)
expect(() =>
makeBadge({ label: 'build', message: 'passed', format: 'png' }),
).to.throw(ValidationError, "Unexpected field 'format'")
expect(() =>
makeBadge({ label: 'build', message: 'passed', template: 'flat' }),
).to.throw(ValidationError, "Unexpected field 'template'")
expect(() =>
makeBadge({ label: 'build', message: 'passed', foo: 'bar' }),
).to.throw(ValidationError, "Unexpected field 'foo'")
expect(() =>
makeBadge({
label: 'build',
message: 'passed',
style: 'something else',
}),
).to.throw(
ValidationError,
'Field `style` must be one of (plastic,flat,flat-square,for-the-badge,social)',
)
expect(() =>
makeBadge({ label: 'build', message: 'passed', style: 'popout' }),
).to.throw(
ValidationError,
'Field `style` must be one of (plastic,flat,flat-square,for-the-badge,social)',
)
expect(() =>
makeBadge({ label: 'build', message: 'passed', idSuffix: '\\' }),
).to.throw(
ValidationError,
'Field `idSuffix` must contain only numbers, letters, -, and _',
)
})
})

View File

@@ -1,67 +0,0 @@
'use strict'
const { normalizeColor, toSvgColor } = require('./color')
const badgeRenderers = require('./badge-renderers')
const { stripXmlWhitespace } = require('./xml')
const { DEFAULT_LOGO_HEIGHT } = require('./constants')
/*
note: makeBadge() is fairly thinly wrapped so if we are making changes here
it is likely this will impact on the package's public interface in index.js
*/
module.exports = function makeBadge({
format,
style = 'flat',
label,
message,
color,
labelColor,
logo,
logoSize,
logoWidth,
links = ['', ''],
idSuffix,
}) {
// String coercion and whitespace removal.
label = `${label}`.trim()
message = `${message}`.trim()
// This ought to be the responsibility of the server, not `makeBadge`.
if (format === 'json') {
return JSON.stringify({
label,
message,
logoWidth,
// Only call normalizeColor for the JSON case: this is handled
// internally by toSvgColor in the SVG case.
color: normalizeColor(color),
labelColor: normalizeColor(labelColor),
link: links,
name: label,
value: message,
idSuffix,
})
}
const render = badgeRenderers[style]
if (!render) {
throw new Error(`Unknown badge style: '${style}'`)
}
logoWidth = +logoWidth || (logo ? DEFAULT_LOGO_HEIGHT : 0)
return stripXmlWhitespace(
render({
label,
message,
links,
logo,
logoWidth,
logoSize,
logoPadding: logo && label.length ? 3 : 0,
color: toSvgColor(color),
labelColor: toSvgColor(labelColor),
idSuffix,
}),
)
}

View File

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

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 omitted 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

@@ -6,22 +6,13 @@ public:
metrics:
prometheus:
enabled: 'METRICS_PROMETHEUS_ENABLED'
endpointEnabled: 'METRICS_PROMETHEUS_ENDPOINT_ENABLED'
influx:
enabled: 'METRICS_INFLUX_ENABLED'
url: 'METRICS_INFLUX_URL'
timeoutMilliseconds: 'METRICS_INFLUX_TIMEOUT_MILLISECONDS'
intervalSeconds: 'METRICS_INFLUX_INTERVAL_SECONDS'
instanceIdFrom: 'METRICS_INFLUX_INSTANCE_ID_FROM'
instanceIdEnvVarName: 'METRICS_INFLUX_INSTANCE_ID_ENV_VAR_NAME'
envLabel: 'METRICS_INFLUX_ENV_LABEL'
ssl:
isSecure: 'HTTPS'
key: 'HTTPS_KEY'
cert: 'HTTPS_CRT'
redirectUrl: 'REDIRECT_URI'
redirectUri: 'REDIRECT_URI'
rasterUrl: 'RASTER_URL'
@@ -30,20 +21,19 @@ public:
__name: 'ALLOWED_ORIGIN'
__format: 'json'
persistence:
dir: 'PERSISTENCE_DIR'
services:
bitbucketServer:
authorizedOrigins: 'BITBUCKET_SERVER_ORIGINS'
drone:
authorizedOrigins: 'DRONE_ORIGINS'
gitea:
authorizedOrigins: 'GITEA_ORIGINS'
github:
baseUri: 'GITHUB_URL'
debug:
enabled: 'GITHUB_DEBUG_ENABLED'
intervalSeconds: 'GITHUB_DEBUG_INTERVAL_SECONDS'
gitlab:
authorizedOrigins: 'GITLAB_ORIGINS'
jenkins:
authorizedOrigins: 'JENKINS_ORIGINS'
jira:
@@ -52,71 +42,46 @@ public:
authorizedOrigins: 'NEXUS_ORIGINS'
npm:
authorizedOrigins: 'NPM_ORIGINS'
obs:
authorizedOrigins: 'OBS_ORIGINS'
pypi:
baseUri: 'PYPI_URL'
sonar:
authorizedOrigins: 'SONAR_ORIGINS'
teamcity:
authorizedOrigins: 'TEAMCITY_ORIGINS'
weblate:
authorizedOrigins: 'WEBLATE_ORIGINS'
trace: 'TRACE_SERVICES'
cacheHeaders:
defaultCacheLengthSeconds: 'BADGE_MAX_AGE_SECONDS'
rateLimit: 'RATE_LIMIT'
fetchLimit: 'FETCH_LIMIT'
userAgentBase: 'USER_AGENT_BASE'
requestTimeoutSeconds: 'REQUEST_TIMEOUT_SECONDS'
requestTimeoutMaxAgeSeconds: 'REQUEST_TIMEOUT_MAX_AGE_SECONDS'
requireCloudflare: 'REQUIRE_CLOUDFLARE'
private:
azure_devops_token: 'AZURE_DEVOPS_TOKEN'
bintray_user: 'BINTRAY_USER'
bintray_apikey: 'BINTRAY_API_KEY'
bitbucket_username: 'BITBUCKET_USER'
bitbucket_password: 'BITBUCKET_PASS'
bitbucket_server_username: 'BITBUCKET_SERVER_USER'
bitbucket_server_password: 'BITBUCKET_SERVER_PASS'
curseforge_api_key: 'CURSEFORGE_API_KEY'
discord_bot_token: 'DISCORD_BOT_TOKEN'
dockerhub_username: 'DOCKERHUB_USER'
dockerhub_pat: 'DOCKERHUB_PAT'
drone_token: 'DRONE_TOKEN'
gitea_token: 'GITEA_TOKEN'
gh_client_id: 'GH_CLIENT_ID'
gh_client_secret: 'GH_CLIENT_SECRET'
gh_token: 'GH_TOKEN'
gitlab_token: 'GITLAB_TOKEN'
jenkins_user: 'JENKINS_USER'
jenkins_pass: 'JENKINS_PASS'
jira_user: 'JIRA_USER'
jira_pass: 'JIRA_PASS'
librariesio_tokens: 'LIBRARIESIO_TOKENS'
nexus_user: 'NEXUS_USER'
nexus_pass: 'NEXUS_PASS'
npm_token: 'NPM_TOKEN'
obs_user: 'OBS_USER'
obs_pass: 'OBS_PASS'
redis_url: 'REDIS_URL'
opencollective_token: 'OPENCOLLECTIVE_TOKEN'
pepy_key: 'PEPY_KEY'
postgres_url: 'POSTGRES_URL'
reddit_client_id: 'REDDIT_CLIENT_ID'
reddit_client_secret: 'REDDIT_CLIENT_SECRET'
sentry_dsn: 'SENTRY_DSN'
shields_secret: 'SHIELDS_SECRET'
sl_insight_userUuid: 'SL_INSIGHT_USER_UUID'
sl_insight_apiToken: 'SL_INSIGHT_API_TOKEN'
sonarqube_token: 'SONARQUBE_TOKEN'
stackapps_api_key: 'STACKAPPS_API_KEY'
teamcity_user: 'TEAMCITY_USER'
teamcity_pass: 'TEAMCITY_PASS'
twitch_client_id: 'TWITCH_CLIENT_ID'
twitch_client_secret: 'TWITCH_CLIENT_SECRET'
influx_username: 'INFLUX_USERNAME'
influx_password: 'INFLUX_PASSWORD'
weblate_api_key: 'WEBLATE_API_KEY'
youtube_api_key: 'YOUTUBE_API_KEY'
wheelmap_token: 'WHEELMAP_TOKEN'

View File

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

View File

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

View File

@@ -1,18 +1,12 @@
private:
# These are the keys which are set on the production servers.
curseforge_api_key: ...
discord_bot_token: ...
gh_client_id: ...
gh_client_secret: ...
gitlab_token: ...
reddit_client_id: ...
reddit_client_secret: ...
redis_url: ...
sentry_dsn: ...
shields_secret: ...
sl_insight_userUuid: ...
sl_insight_apiToken: ...
twitch_client_id: ...
twitch_client_secret: ...
weblate_api_key: ...
wheelmap_token: ...
youtube_api_key: ...

View File

@@ -4,15 +4,7 @@ private:
# The possible values are documented in `doc/server-secrets.md`. Note that
# you can also set these values through environment variables, which may be
# preferable for self hosting.
curseforge_api_key: '...'
gh_token: '...'
gitlab_token: '...'
obs_user: '...'
obs_pass: '...'
reddit_client_id: '...'
reddit_client_secret: '...'
twitch_client_id: '...'
twitch_client_secret: '...'
weblate_api_key: '...'
wheelmap_token: '...'
youtube_api_key: '...'

View File

@@ -2,24 +2,17 @@ public:
metrics:
prometheus:
enabled: true
influx:
enabled: true
url: https://metrics.shields.io/telegraf
instanceIdFrom: env-var
instanceIdEnvVarName: FLY_ALLOC_ID
envLabel: shields-production
ssl:
isSecure: false
isSecure: true
cors:
allowedOrigin: ['http://shields.io', 'https://shields.io']
services:
gitlab:
authorizedOrigins: 'https://gitlab.com'
redirectUrl: 'https://shields.io/'
rasterUrl: 'https://raster.shields.io'
userAgentBase: 'Shields.io'
requireCloudflare: true
requestTimeoutSeconds: 20
private:
# These are not really private; they should be moved to `public`.
shields_ips: ['192.99.59.72', '51.254.114.150', '149.56.96.133']

View File

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

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

@@ -0,0 +1,108 @@
export function badgeUrlFromPath({
baseUrl,
path,
queryParams,
style,
format,
longCache,
}: {
baseUrl?: string
path: string
queryParams: { [k: string]: string | number | boolean }
style?: string
format?: string
longCache?: boolean
}): string
export function badgeUrlFromPattern({
baseUrl,
pattern,
namedParams,
queryParams,
style,
format,
longCache,
}: {
baseUrl?: string
pattern: string
namedParams: { [k: string]: string }
queryParams: { [k: string]: string | number | boolean }
style?: string
format?: string
longCache?: boolean
}): string
export function encodeField(s: string): string
export function staticBadgeUrl({
baseUrl,
label,
message,
color,
style,
namedLogo,
format,
}: {
baseUrl?: string
label: string
message: string
color?: string
style?: string
namedLogo?: string
format?: string
}): string
export function queryStringStaticBadgeUrl({
baseUrl,
label,
message,
color,
labelColor,
style,
namedLogo,
logoColor,
logoWidth,
logoPosition,
format,
}: {
baseUrl?: string
label: string
message: string
color?: string
labelColor?: string
style?: string
namedLogo?: string
logoColor?: string
logoWidth?: number
logoPosition?: number
format?: string
}): string
export function dynamicBadgeUrl({
baseUrl,
datatype,
label,
dataUrl,
query,
prefix,
suffix,
color,
style,
format,
}: {
baseUrl?: string
datatype: string
label: string
dataUrl: string
query: string
prefix: string
suffix: string
color?: string
style?: string
format?: string
}): string
export function rasterRedirectUrl(
{ rasterUrl }: { rasterUrl: string },
badgeUrl: string
): string

View File

@@ -1,13 +1,160 @@
// Avoid "Attempted import error: 'URL' is not exported from 'url'" in frontend.
import url from 'url'
'use strict'
const { URL } = require('url')
const queryString = require('query-string')
const { compile } = require('path-to-regexp')
function badgeUrlFromPath({
baseUrl = '',
path,
queryParams,
style,
format = '',
longCache = false,
}) {
const outExt = format.length ? `.${format}` : ''
const outQueryString = queryString.stringify({
cacheSeconds: longCache ? '2592000' : undefined,
style,
...queryParams,
})
const suffix = outQueryString ? `?${outQueryString}` : ''
return `${baseUrl}${path}${outExt}${suffix}`
}
function badgeUrlFromPattern({
baseUrl = '',
pattern,
namedParams,
queryParams,
style,
format = '',
longCache = false,
}) {
const toPath = compile(pattern, {
strict: true,
sensitive: true,
encode: encodeURIComponent,
})
const path = toPath(namedParams)
return badgeUrlFromPath({
baseUrl,
path,
queryParams,
style,
format,
longCache,
})
}
function encodeField(s) {
return encodeURIComponent(s.replace(/-/g, '--').replace(/_/g, '__'))
}
function staticBadgeUrl({
baseUrl = '',
label,
message,
color = 'lightgray',
style,
namedLogo,
format = '',
}) {
const path = [label, message, color].map(encodeField).join('-')
const outQueryString = queryString.stringify({
style,
logo: namedLogo,
})
const outExt = format.length ? `.${format}` : ''
const suffix = outQueryString ? `?${outQueryString}` : ''
return `${baseUrl}/badge/${path}${outExt}${suffix}`
}
function queryStringStaticBadgeUrl({
baseUrl = '',
label,
message,
color,
labelColor,
style,
namedLogo,
logoColor,
logoWidth,
logoPosition,
format = '',
}) {
// schemaVersion could be a parameter if we iterate on it,
// for now it's hardcoded to the only supported version.
const schemaVersion = '1'
const suffix = `?${queryString.stringify({
label,
message,
color,
labelColor,
style,
logo: namedLogo,
logoColor,
logoWidth,
logoPosition,
})}`
const outExt = format.length ? `.${format}` : ''
return `${baseUrl}/static/v${schemaVersion}${outExt}${suffix}`
}
function dynamicBadgeUrl({
baseUrl,
datatype,
label,
dataUrl,
query,
prefix,
suffix,
color,
style,
format = '',
}) {
const outExt = format.length ? `.${format}` : ''
const queryParams = {
label,
url: dataUrl,
query,
style,
}
if (color) {
queryParams.color = color
}
if (prefix) {
queryParams.prefix = prefix
}
if (suffix) {
queryParams.suffix = suffix
}
const outQueryString = queryString.stringify(queryParams)
return `${baseUrl}/badge/dynamic/${datatype}${outExt}?${outQueryString}`
}
function rasterRedirectUrl({ rasterUrl }, badgeUrl) {
// Ensure we're always using the `rasterUrl` by using just the path from
// the request URL.
const { pathname, search } = new url.URL(badgeUrl, 'https://bogus.test')
const result = new url.URL(pathname, rasterUrl)
const { pathname, search } = new URL(badgeUrl, 'https://bogus.test')
const result = new URL(pathname, rasterUrl)
result.search = search
return result
}
export { rasterRedirectUrl }
module.exports = {
badgeUrlFromPath,
badgeUrlFromPattern,
encodeField,
staticBadgeUrl,
queryStringStaticBadgeUrl,
dynamicBadgeUrl,
rasterRedirectUrl,
}

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,7 @@
import { URL } from 'url'
import dayjs from 'dayjs'
import Joi from 'joi'
import checkErrorResponse from './check-error-response.js'
import { InvalidParameter, InvalidResponse } from './errors.js'
import { fetch } from './got.js'
import { parseJson } from './json.js'
import validate from './validate.js'
'use strict'
let jwtCache = Object.create(null)
const { URL } = require('url')
const { InvalidParameter } = require('./errors')
class AuthHelper {
constructor(
@@ -19,7 +13,7 @@ class AuthHelper {
isRequired = false,
defaultToEmptyStringForUser = false,
},
config,
config
) {
if (!userKey && !passKey) {
throw Error('Expected userKey or passKey to be set')
@@ -82,7 +76,7 @@ class AuthHelper {
}
static _isInsecureSslRequest({ options = {} }) {
const strictSSL = options?.https?.rejectUnauthorized ?? true
const { strictSSL = true } = options
return strictSSL !== true
}
@@ -95,7 +89,7 @@ class AuthHelper {
}
}
isAllowedOrigin(url) {
shouldAuthenticateRequest({ url, options = {} }) {
let parsed
try {
parsed = new URL(url)
@@ -105,11 +99,7 @@ class AuthHelper {
const { protocol, host } = parsed
const origin = `${protocol}//${host}`
return this._authorizedOrigins.includes(origin)
}
shouldAuthenticateRequest({ url, options = {} }) {
const originViolation = !this.isAllowedOrigin(url)
const originViolation = !this._authorizedOrigins.includes(origin)
const strictSslCheckViolation =
this._requireStrictSslToAuthenticate &&
@@ -119,10 +109,8 @@ class AuthHelper {
}
get _basicAuth() {
const { _user: username, _pass: password } = this
return this.isConfigured
? { username: username || '', password: password || '' }
: undefined
const { _user: user, _pass: pass } = this
return this.isConfigured ? { user, pass } : undefined
}
/*
@@ -145,7 +133,7 @@ class AuthHelper {
const { options, ...rest } = requestParams
return {
options: {
...auth,
auth,
...options,
},
...rest,
@@ -154,20 +142,13 @@ class AuthHelper {
withBasicAuth(requestParams) {
return this._withAnyAuth(requestParams, requestParams =>
this.constructor._mergeAuth(requestParams, this._basicAuth),
this.constructor._mergeAuth(requestParams, this._basicAuth)
)
}
_bearerAuthHeader(bearerKey) {
get _bearerAuthHeader() {
const { _pass: pass } = this
return this.isConfigured
? { Authorization: `${bearerKey} ${pass}` }
: undefined
}
_apiKeyHeader(apiKeyHeader) {
const { _pass: pass } = this
return this.isConfigured ? { [apiKeyHeader]: pass } : undefined
return this.isConfigured ? { Authorization: `Bearer ${pass}` } : undefined
}
static _mergeHeaders(requestParams, headers) {
@@ -187,32 +168,20 @@ class AuthHelper {
}
}
withApiKeyHeader(requestParams, header = 'x-api-key') {
withBearerAuthHeader(requestParams) {
return this._withAnyAuth(requestParams, requestParams =>
this.constructor._mergeHeaders(requestParams, this._apiKeyHeader(header)),
)
}
withBearerAuthHeader(
requestParams,
bearerKey = 'Bearer', // lgtm [js/hardcoded-credentials]
) {
return this._withAnyAuth(requestParams, requestParams =>
this.constructor._mergeHeaders(
requestParams,
this._bearerAuthHeader(bearerKey),
),
this.constructor._mergeHeaders(requestParams, this._bearerAuthHeader)
)
}
static _mergeQueryParams(requestParams, query) {
const {
options: { searchParams: existingQuery, ...restOptions } = {},
options: { qs: existingQuery, ...restOptions } = {},
...rest
} = requestParams
return {
options: {
searchParams: {
qs: {
...existingQuery,
...query,
},
@@ -227,106 +196,9 @@ class AuthHelper {
this.constructor._mergeQueryParams(requestParams, {
...(userKey ? { [userKey]: this._user } : undefined),
...(passKey ? { [passKey]: this._pass } : undefined),
}),
)
}
static _getJwtExpiry(token, max = dayjs().add(1, 'hours').unix()) {
// get the expiry timestamp for this JWT (capped at a max length)
const parts = token.split('.')
if (parts.length < 2) {
throw new InvalidResponse({
prettyMessage: 'invalid response data from auth endpoint',
})
}
const json = validate(
{
ErrorClass: InvalidResponse,
prettyErrorMessage: 'invalid response data from auth endpoint',
},
parseJson(Buffer.from(parts[1], 'base64').toString()),
Joi.object({ exp: Joi.number().required() }).required(),
)
return Math.min(json.exp, max)
}
static _isJwtValid(expiry) {
// we consider the token valid if the expiry
// datetime is later than (now + 1 minute)
return dayjs.unix(expiry).isAfter(dayjs().add(1, 'minutes'))
}
async _getJwt(loginEndpoint) {
const { _user: username, _pass: password } = this
// attempt to get JWT from cache
if (
jwtCache?.[loginEndpoint]?.[username]?.token &&
jwtCache?.[loginEndpoint]?.[username]?.expiry &&
this.constructor._isJwtValid(jwtCache[loginEndpoint][username].expiry)
) {
// cache hit
return jwtCache[loginEndpoint][username].token
}
// cache miss - request a new JWT
const originViolation = !this.isAllowedOrigin(loginEndpoint)
if (originViolation) {
throw new InvalidParameter({
prettyMessage: 'requested origin not authorized',
})
}
const { buffer } = await checkErrorResponse({})(
await fetch(loginEndpoint, {
method: 'POST',
form: { username, password },
}),
)
const json = validate(
{
ErrorClass: InvalidResponse,
prettyErrorMessage: 'invalid response data from auth endpoint',
},
parseJson(buffer),
Joi.object({ token: Joi.string().required() }).required(),
)
const token = json.token
const expiry = this.constructor._getJwtExpiry(token)
// store in the cache
if (!(loginEndpoint in jwtCache)) {
jwtCache[loginEndpoint] = {}
}
jwtCache[loginEndpoint][username] = { token, expiry }
return token
}
async _getJwtAuthHeader(loginEndpoint) {
if (!this.isConfigured) {
return undefined
}
const token = await this._getJwt(loginEndpoint)
return { Authorization: `Bearer ${token}` }
}
async withJwtAuth(requestParams, loginEndpoint) {
const authHeader = await this._getJwtAuthHeader(loginEndpoint)
return this._withAnyAuth(requestParams, requestParams =>
this.constructor._mergeHeaders(requestParams, authHeader),
)
}
}
function clearJwtCache() {
jwtCache = Object.create(null)
}
export { AuthHelper, clearJwtCache }
module.exports = { AuthHelper }

View File

@@ -1,48 +1,24 @@
import dayjs from 'dayjs'
import nock from 'nock'
import { expect } from 'chai'
import { test, given, forCases } from 'sazerac'
import { AuthHelper, clearJwtCache } from './auth-helper.js'
import { InvalidParameter, InvalidResponse } from './errors.js'
'use strict'
function base64UrlEncode(input) {
const base64 = btoa(JSON.stringify(input))
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
}
const { expect } = require('chai')
const { test, given, forCases } = require('sazerac')
const { AuthHelper } = require('./auth-helper')
const { InvalidParameter } = require('./errors')
function getMockJwt(extras) {
// this function returns a mock JWT that contains enough
// for a unit test but ignores important aspects e.g: signing
const header = {
alg: 'HS256',
typ: 'JWT',
}
const payload = {
iat: Math.floor(Date.now() / 1000),
...extras,
}
const encodedHeader = base64UrlEncode(header)
const encodedPayload = base64UrlEncode(payload)
return `${encodedHeader}.${encodedPayload}`
}
describe('AuthHelper', function () {
describe('constructor checks', function () {
it('throws without userKey or passKey', function () {
describe('AuthHelper', function() {
describe('constructor checks', function() {
it('throws without userKey or passKey', function() {
expect(() => new AuthHelper({}, {})).to.throw(
Error,
'Expected userKey or passKey to be set',
'Expected userKey or passKey to be set'
)
})
it('throws without serviceKey or authorizedOrigins', function () {
it('throws without serviceKey or authorizedOrigins', function() {
expect(
() =>
new AuthHelper({ userKey: 'myci_user', passKey: 'myci_pass' }, {}),
() => new AuthHelper({ userKey: 'myci_user', passKey: 'myci_pass' }, {})
).to.throw(Error, 'Expected authorizedOrigins or serviceKey to be set')
})
it('throws when authorizedOrigins is not an array', function () {
it('throws when authorizedOrigins is not an array', function() {
expect(
() =>
new AuthHelper(
@@ -51,17 +27,17 @@ describe('AuthHelper', function () {
passKey: 'myci_pass',
authorizedOrigins: true,
},
{ private: {} },
),
{ private: {} }
)
).to.throw(Error, 'Expected authorizedOrigins to be an array of origins')
})
})
describe('isValid', function () {
describe('isValid', function() {
function validate(config, privateConfig) {
return new AuthHelper(
{ authorizedOrigins: ['https://example.test'], ...config },
{ private: privateConfig },
{ private: privateConfig }
).isValid
}
test(validate, () => {
@@ -69,20 +45,20 @@ describe('AuthHelper', function () {
// Fully configured user + pass.
given(
{ userKey: 'myci_user', passKey: 'myci_pass', isRequired: true },
{ myci_user: 'admin', myci_pass: 'abc123' },
{ myci_user: 'admin', myci_pass: 'abc123' }
),
given(
{ userKey: 'myci_user', passKey: 'myci_pass' },
{ myci_user: 'admin', myci_pass: 'abc123' },
{ myci_user: 'admin', myci_pass: 'abc123' }
),
// Fully configured user or pass.
given(
{ userKey: 'myci_user', isRequired: true },
{ myci_user: 'admin' },
{ myci_user: 'admin' }
),
given(
{ passKey: 'myci_pass', isRequired: true },
{ myci_pass: 'abc123' },
{ myci_pass: 'abc123' }
),
given({ userKey: 'myci_user' }, { myci_user: 'admin' }),
given({ passKey: 'myci_pass' }, { myci_pass: 'abc123' }),
@@ -96,16 +72,16 @@ describe('AuthHelper', function () {
// Partly configured.
given(
{ userKey: 'myci_user', passKey: 'myci_pass', isRequired: true },
{ myci_user: 'admin' },
{ myci_user: 'admin' }
),
given(
{ userKey: 'myci_user', passKey: 'myci_pass' },
{ myci_user: 'admin' },
{ myci_user: 'admin' }
),
// Missing required config.
given(
{ userKey: 'myci_user', passKey: 'myci_pass', isRequired: true },
{},
{}
),
given({ userKey: 'myci_user', isRequired: true }, {}),
given({ passKey: 'myci_pass', isRequired: true }, {}),
@@ -113,93 +89,88 @@ describe('AuthHelper', function () {
})
})
describe('_basicAuth', function () {
describe('_basicAuth', function() {
function validate(config, privateConfig) {
return new AuthHelper(
{ authorizedOrigins: ['https://example.test'], ...config },
{ private: privateConfig },
{ private: privateConfig }
)._basicAuth
}
test(validate, () => {
forCases([
given(
{ userKey: 'myci_user', passKey: 'myci_pass', isRequired: true },
{ myci_user: 'admin', myci_pass: 'abc123' },
{ myci_user: 'admin', myci_pass: 'abc123' }
),
given(
{ 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({
username: 'admin',
password: '',
user: 'admin',
pass: undefined,
})
given({ passKey: 'myci_pass' }, { myci_pass: 'abc123' }).expect({
username: '',
password: 'abc123',
user: undefined,
pass: 'abc123',
})
given({ userKey: 'myci_user', passKey: 'myci_pass' }, {}).expect(
undefined,
undefined
)
given(
{ passKey: 'myci_pass', defaultToEmptyStringForUser: true },
{ myci_pass: 'abc123' },
{ myci_pass: 'abc123' }
).expect({
username: '',
password: 'abc123',
user: '',
pass: 'abc123',
})
})
})
describe('_isInsecureSslRequest', function () {
describe('_isInsecureSslRequest', function() {
test(AuthHelper._isInsecureSslRequest, () => {
forCases([
given({ url: 'http://example.test' }),
given({ url: 'http://example.test', options: {} }),
given({ url: 'http://example.test', options: { strictSSL: true } }),
given({
url: 'http://example.test',
options: { https: { rejectUnauthorized: true } },
}),
given({
url: 'http://example.test',
options: { https: { rejectUnauthorized: undefined } },
options: { strictSSL: undefined },
}),
]).expect(false)
given({
url: 'http://example.test',
options: { https: { rejectUnauthorized: false } },
options: { strictSSL: false },
}).expect(true)
})
})
describe('enforceStrictSsl', function () {
describe('enforceStrictSsl', function() {
const authConfig = {
userKey: 'myci_user',
passKey: 'myci_pass',
serviceKey: 'myci',
}
context('by default', function () {
context('by default', function() {
const authHelper = new AuthHelper(authConfig, {
public: {
services: { myci: { authorizedOrigins: ['http://myci.test'] } },
},
private: { myci_user: 'admin', myci_pass: 'abc123' },
})
it('does not throw for secure requests', function () {
it('does not throw for secure requests', function() {
expect(() => authHelper.enforceStrictSsl({})).not.to.throw()
})
it('throws for insecure requests', function () {
it('throws for insecure requests', function() {
expect(() =>
authHelper.enforceStrictSsl({
options: { https: { rejectUnauthorized: false } },
}),
authHelper.enforceStrictSsl({ options: { strictSSL: false } })
).to.throw(InvalidParameter)
})
})
context("when strict SSL isn't required", function () {
context("when strict SSL isn't required", function() {
const authHelper = new AuthHelper(authConfig, {
public: {
services: {
@@ -211,27 +182,25 @@ describe('AuthHelper', function () {
},
private: { myci_user: 'admin', myci_pass: 'abc123' },
})
it('does not throw for secure requests', function () {
it('does not throw for secure requests', function() {
expect(() => authHelper.enforceStrictSsl({})).not.to.throw()
})
it('does not throw for insecure requests', function () {
it('does not throw for insecure requests', function() {
expect(() =>
authHelper.enforceStrictSsl({
options: { https: { rejectUnauthorized: false } },
}),
authHelper.enforceStrictSsl({ options: { strictSSL: false } })
).not.to.throw()
})
})
})
describe('shouldAuthenticateRequest', function () {
describe('shouldAuthenticateRequest', function() {
const authConfig = {
userKey: 'myci_user',
passKey: 'myci_pass',
serviceKey: 'myci',
}
context('by default', function () {
context('by default', function() {
const authHelper = new AuthHelper(authConfig, {
public: {
services: {
@@ -244,20 +213,20 @@ describe('AuthHelper', function () {
})
const shouldAuthenticateRequest = requestOptions =>
authHelper.shouldAuthenticateRequest(requestOptions)
describe('a secure request to an authorized origin', function () {
describe('a secure request to an authorized origin', function() {
test(shouldAuthenticateRequest, () => {
given({ url: 'https://myci.test/api' }).expect(true)
})
})
describe('an insecure request', function () {
describe('an insecure request', function() {
test(shouldAuthenticateRequest, () => {
given({
url: 'https://myci.test/api',
options: { https: { rejectUnauthorized: false } },
options: { strictSSL: false },
}).expect(false)
})
})
describe('a request to an unauthorized origin', function () {
describe('a request to an unauthorized origin', function() {
test(shouldAuthenticateRequest, () => {
forCases([
given({ url: 'http://myci.test/api' }),
@@ -268,7 +237,7 @@ describe('AuthHelper', function () {
})
})
context('when auth over insecure SSL is allowed', function () {
context('when auth over insecure SSL is allowed', function() {
const authHelper = new AuthHelper(authConfig, {
public: {
services: {
@@ -282,20 +251,20 @@ describe('AuthHelper', function () {
})
const shouldAuthenticateRequest = requestOptions =>
authHelper.shouldAuthenticateRequest(requestOptions)
describe('a secure request to an authorized origin', function () {
describe('a secure request to an authorized origin', function() {
test(shouldAuthenticateRequest, () => {
given({ url: 'https://myci.test' }).expect(true)
})
})
describe('an insecure request', function () {
describe('an insecure request', function() {
test(shouldAuthenticateRequest, () => {
given({
url: 'https://myci.test',
options: { https: { rejectUnauthorized: false } },
options: { strictSSL: false },
}).expect(true)
})
})
describe('a request to an unauthorized origin', function () {
describe('a request to an unauthorized origin', function() {
test(shouldAuthenticateRequest, () => {
forCases([
given({ url: 'http://myci.test' }),
@@ -306,7 +275,7 @@ describe('AuthHelper', function () {
})
})
context('when the service is partly configured', function () {
context('when the service is partly configured', function() {
const authHelper = new AuthHelper(authConfig, {
public: {
services: {
@@ -320,7 +289,7 @@ describe('AuthHelper', function () {
})
const shouldAuthenticateRequest = requestOptions =>
authHelper.shouldAuthenticateRequest(requestOptions)
describe('a secure request to an authorized origin', function () {
describe('a secure request to an authorized origin', function() {
test(shouldAuthenticateRequest, () => {
given({ url: 'https://myci.test' }).expect(false)
})
@@ -328,7 +297,7 @@ describe('AuthHelper', function () {
})
})
describe('withBasicAuth', function () {
describe('withBasicAuth', function() {
const authHelper = new AuthHelper(
{
userKey: 'myci_user',
@@ -344,20 +313,19 @@ describe('AuthHelper', function () {
},
},
private: { myci_user: 'admin', myci_pass: 'abc123' },
},
}
)
const withBasicAuth = requestOptions =>
authHelper.withBasicAuth(requestOptions)
describe('authenticates a secure request to an authorized origin', function () {
describe('authenticates a secure request to an authorized origin', function() {
test(withBasicAuth, () => {
given({
url: 'https://myci.test/api',
}).expect({
url: 'https://myci.test/api',
options: {
username: 'admin',
password: 'abc123',
auth: { user: 'admin', pass: 'abc123' },
},
})
given({
@@ -369,14 +337,13 @@ describe('AuthHelper', function () {
url: 'https://myci.test/api',
options: {
headers: { Accept: 'application/json' },
username: 'admin',
password: 'abc123',
auth: { user: 'admin', pass: 'abc123' },
},
})
})
})
describe('does not authenticate a request to an unauthorized origin', function () {
describe('does not authenticate a request to an unauthorized origin', function() {
test(withBasicAuth, () => {
given({
url: 'https://other.test/api',
@@ -397,162 +364,13 @@ describe('AuthHelper', function () {
})
})
describe('throws on an insecure SSL request', function () {
describe('throws on an insecure SSL request', function() {
expect(() =>
withBasicAuth({
url: 'https://myci.test/api',
options: { https: { rejectUnauthorized: false } },
}),
options: { strictSSL: false },
})
).to.throw(InvalidParameter)
})
})
context('JTW Auth', function () {
describe('_isJwtValid', function () {
test(AuthHelper._isJwtValid, () => {
given(dayjs().add(1, 'month').unix()).expect(true)
given(dayjs().add(2, 'minutes').unix()).expect(true)
given(dayjs().add(30, 'seconds').unix()).expect(false)
given(dayjs().unix()).expect(false)
given(dayjs().subtract(1, 'seconds').unix()).expect(false)
})
})
describe('_getJwtExpiry', function () {
it('extracts expiry from valid JWT', function () {
const nowPlus30Mins = dayjs().add(30, 'minutes').unix()
expect(
AuthHelper._getJwtExpiry(getMockJwt({ exp: nowPlus30Mins })),
).to.equal(nowPlus30Mins)
})
it('caps expiry at max', function () {
const nowPlus1Hour = dayjs().add(1, 'hours').unix()
const nowPlus2Hours = dayjs().add(2, 'hours').unix()
expect(
AuthHelper._getJwtExpiry(getMockJwt({ exp: nowPlus2Hours })),
).to.equal(nowPlus1Hour)
})
it('throws if JWT does not contain exp', function () {
expect(() => {
AuthHelper._getJwtExpiry(getMockJwt({}))
}).to.throw(InvalidResponse)
})
it('throws if JWT is invalid', function () {
expect(() => {
AuthHelper._getJwtExpiry('abc')
}).to.throw(InvalidResponse)
})
})
describe('withJwtAuth', function () {
const authHelper = new AuthHelper(
{
userKey: 'jwt_user',
passKey: 'jwt_pass',
authorizedOrigins: ['https://example.com'],
isRequired: false,
},
{ private: { jwt_user: 'fred', jwt_pass: 'abc123' } },
)
beforeEach(function () {
clearJwtCache()
})
it('should use cached response if valid', async function () {
// the expiry is far enough in the future that the token
// will still be valid on the second hit
const mockToken = getMockJwt({ exp: dayjs().add(1, 'hours').unix() })
// .times(1) ensures if we try to make a second call to this endpoint,
// we will throw `Nock: No match for request`
nock('https://example.com')
.post('/login')
.times(1)
.reply(200, { token: mockToken })
const params1 = await authHelper.withJwtAuth(
{ url: 'https://example.com/some-endpoint' },
'https://example.com/login',
)
expect(nock.isDone()).to.equal(true)
expect(params1).to.deep.equal({
options: {
headers: {
Authorization: `Bearer ${mockToken}`,
},
},
url: 'https://example.com/some-endpoint',
})
// second time round, we'll get the same response again
// but this time served from cache
const params2 = await authHelper.withJwtAuth(
{ url: 'https://example.com/some-endpoint' },
'https://example.com/login',
)
expect(params2).to.deep.equal({
options: {
headers: {
Authorization: `Bearer ${mockToken}`,
},
},
url: 'https://example.com/some-endpoint',
})
nock.cleanAll()
})
it('should not use cached response if expired', async function () {
// this time we define a token expiry is close enough
// that the token will not be valid on the second call
const mockToken1 = getMockJwt({
exp: dayjs().add(20, 'seconds').unix(),
})
nock('https://example.com')
.post('/login')
.times(1)
.reply(200, { token: mockToken1 })
const params1 = await authHelper.withJwtAuth(
{ url: 'https://example.com/some-endpoint' },
'https://example.com/login',
)
expect(nock.isDone()).to.equal(true)
expect(params1).to.deep.equal({
options: {
headers: {
Authorization: `Bearer ${mockToken1}`,
},
},
url: 'https://example.com/some-endpoint',
})
// second time round we make another network request
const mockToken2 = getMockJwt({
exp: dayjs().add(20, 'seconds').unix(),
})
nock('https://example.com')
.post('/login')
.times(1)
.reply(200, { token: mockToken2 })
const params2 = await authHelper.withJwtAuth(
{ url: 'https://example.com/some-endpoint' },
'https://example.com/login',
)
expect(nock.isDone()).to.equal(true)
expect(params2).to.deep.equal({
options: {
headers: {
Authorization: `Bearer ${mockToken2}`,
},
},
url: 'https://example.com/some-endpoint',
})
nock.cleanAll()
})
})
})
})

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