# Technical Documentation: badges/shields

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/badges/shields)

### Overview (`README.md`)

<p align="center">
    <img src="https://raw.githubusercontent.com/badges/shields/master/readme-logo.svg?sanitize=true"
        height="130">
</p>
<p align="center">
    <a href="https://shields.io/community#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">
        <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://coveralls.io/github/badges/shields">
        <img src="https://img.shields.io/coveralls/github/badges/shields"
            alt="Code Coverage"></a>
    <a href="https://discord.gg/HjJCwm5">
        <img src="https://img.shields.io/discord/308323056592486420?logo=discord&logoColor=white"
            alt="Chat on Discord"></a>
</p>

This is home to [Shields.io][shields.io], a service for concise, consistent, 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

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]
- The [badge design specification][badge-spec]

[shields.io]: https://shields.io/
[badge-maker]: https://www.npmjs.com/package/badge-maker
[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

## Examples

- code coverage percentage: 
- stable release version: 
- package manager release: 
- status of third-party dependencies: 
- static code analysis grade: 
- [SemVer](https://semver.org/) version observance: 
- amount of [Liberapay](https://liberapay.com/) donations per week: 
- Python package downloads: 
- Chrome Web Store extension rating: 
- Uptime Robot uptime percentage: 

[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

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

## Contributing

Shields is a community project. We invite your participation through issues 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].

[](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].

[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

## Development

[](https://codespaces.new/badges/shields?quickstart=1)

1. Install Node 24. You can use the [package manager][] of your choice.
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.
5. Open `http://localhost:3000/` to view the frontend.

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 definitions are built only before the server first starts. To regenerate those, either run `npm run prestart` 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 `npm run badge -- https://img.shields.io/npm/v/nock`.

Use `npm run debug:server` to start server in debug mode. [This recipe][nodemon debug] shows how to debug Node.js application in [VS Code][].

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

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.

[package manager]: https://nodejs.org/en/download/package-manager/
[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
[sentry]: https://sentry.io/
[sentry configuration]: https://github.com/badges/shields/blob/master/doc/self-hosting.md#sentry
[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

[](https://awesome.re)

Status badges are used widely across open-source and private software projects. Academics have studied the "signal" badges provide about software project quality. There are many existing libraries for rendering these badges, and alternatives to the hosted Shields badge service. [awesome-badges][] is a curated collection of such resources. [Contributions][contributing to awesome-badges] may be considered there. (The presence of a project in that collection should not be interpreted as an endorsement nor promotion from the Shields project)

[awesome-badges]: https://github.com/badges/awesome-badges
[contributing to awesome-badges]: https://github.com/badges/awesome-badges/blob/main/CONTRIBUTING.md

## History

b.adge.me was the original website for this service. Heroku back then had a thing which made it hard to use a toplevel domain with it, hence the odd domain. It used code developed in 2013 from a library called [gh-badges][old-gh-badges], both developed by [Thaddée Tyl][espadrine]. The project merged with shields.io by making it use the b.adge.me code and closed b.adge.me.

The original badge specification was developed in 2013 by [Olivier Lacan][olivierlacan]. It was inspired by the Travis CI and similar badges (there were a lot fewer, back then). In 2014 Thaddée Tyl redesigned it with help from a Travis CI employee and convinced everyone to switch to it. The old design is what today is called the plastic style; the new one is the flat style.

You can read more about [the project's inception][thread], [the motivation of the SVG badge specification][motivation], and [the specification itself][spec].

[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
[thread]: https://github.com/h5bp/lazyweb-requests/issues/150

## Project leaders

Maintainers:

- [jNullj](https://github.com/jnullj)
- [LitoMore](https://github.com/LitoMore)
- [paulmelnikow](https://github.com/paulmelnikow)
- [PyvesB](https://github.com/PyvesB)

Alumni:

- [calebcartwright](https://github.com/calebcartwright)
- [chris48s](https://github.com/chris48s)
- [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)

## License

Shields.io is dual licensed and distributed under the terms of both the [MIT license](LICENSE-MIT) and the [Apache license (Version 2.0)](LICENSE-APACHE).

## Community

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)

---

### Overview (`badge-maker/README.md`)

# badge-maker

[](https://npmjs.org/package/badge-maker) [](https://npmjs.org/package/badge-maker) [](https://npmjs.org/package/badge-maker)

## Installation

```sh
npm install badge-maker
```

## Usage

### On the console

```sh
npm install badge-maker
badge build passed :brightgreen > mybadge.svg
```

### As a library

```js
import { makeBadge, ValidationError } from 'badge-maker'
```

```js
const format = {
  label: 'build',
  message: 'passed',
  color: 'brightgreen',
}

const svg = makeBadge(format)
console.log(svg) // <svg...

try {
  makeBadge({})
} catch (e) {
  console.log(e) // ValidationError: Field `message` is required
}
```

### Node version support

The latest version of badge-maker supports all currently maintained Node versions. See the [Node Release Schedule][].

[node release schedule]: https://github.com/nodejs/Release#release-schedule

## Format

The format is the following:

```js
{
  label: 'build',  // (Optional) Badge label
  message: 'passed',  // (Required) Badge message
  labelColor: '#555',  // (Optional) Label color
  color: '#4c0',  // (Optional) Message color
  logoBase64: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCI+PHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiByeD0iOCIgZmlsbD0iI2IxY2U1NiIvPjxwYXRoIGQ9Ik04IDBoMjR2NjRIOGMtNC40MzIgMC04LTMuNTY4LTgtOFY4YzAtNC40MzIgMy41NjgtOCA4LTh6IiBmaWxsPSIjNWQ1ZDVkIi8+PC9zdmc+', // (Optional) Any custom logo can be passed in a URL parameter by base64 encoding
  links: ['https://example.com', 'https://example.com'], // (Optional) Links array of maximum two links

  // (Optional) One of: 'plastic', 'flat', 'flat-square', 'for-the-badge' or 'social'
  // Each offers a different visual design.
  style: 'flat',

  // (Optional) A string with only letters, numbers, -, and _. This can be used
  // to ensure every element id within the SVG is unique and prevent CSS
  // cross-contamination when the SVG badge is rendered inline in HTML pages.
  idSuffix: 'dd'
}
```

## Colors

There are three ways to specify `color` and `labelColor`:

1. One of the [Shields named colors](./lib/color.js):

- ![][brightgreen]
- ![][green]
- ![][yellow]
- ![][yellowgreen]
- ![][orange]
- ![][red]
- ![][blue]
- ![][grey] ![][gray] – the default `labelColor`
- ![][lightgrey] ![][lightgray] – the default `color`

- ![][success]
- ![][important]
- ![][critical]
- ![][informational]
- ![][inactive] – the default `color`

2. A three- or six-character hex color, optionally prefixed with `#`:

- ![][9cf]
- ![][#007fff]
- etc.

3. [Any valid CSS color][css color], e.g.

- `rgb(...)`, `rgba(...)`
- `hsl(...)`, `hsla(...)`
- ![][aqua] ![][fuchsia] ![][lightslategray] etc.

[brightgreen]: https://img.shields.io/badge/brightgreen-brightgreen.svg
[success]: https://img.shields.io/badge/success-success.svg
[green]: https://img.shields.io/badge/green-green.svg
[yellow]: https://img.shields.io/badge/yellow-yellow.svg
[yellowgreen]: https://img.shields.io/badge/yellowgreen-yellowgreen.svg
[orange]: https://img.shields.io/badge/orange-orange.svg
[important]: https://img.shields.io/badge/important-important.svg
[red]: https://img.shields.io/badge/red-red.svg
[critical]: https://img.shields.io/badge/critical-critical.svg
[blue]: https://img.shields.io/badge/blue-blue.svg
[informational]: https://img.shields.io/badge/informational-informational.svg
[grey]: https://img.shields.io/badge/grey-grey.svg
[gray]: https://img.shields.io/badge/gray-gray.svg
[lightgrey]: https://img.shields.io/badge/lightgrey-lightgrey.svg
[lightgray]: https://img.shields.io/badge/lightgray-lightgray.svg
[inactive]: https://img.shields.io/badge/inactive-inactive.svg
[9cf]: https://img.shields.io/badge/9cf-9cf.svg
[#007fff]: https://img.shields.io/badge/%23007fff-007fff.svg
[aqua]: https://img.shields.io/badge/aqua-aqua.svg
[fuchsia]: https://img.shields.io/badge/fuchsia-fuchsia.svg
[lightslategray]: https://img.shields.io/badge/lightslategray-lightslategray.svg
[css color]: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
[css/svg color]: http://www.w3.org/TR/SVG/types.html#DataTypeColor

## Raster Formats

Conversion to raster formats is no longer directly supported. In javascript code, SVG badges can be converted to raster formats using a library like [gm](https://www.npmjs.com/package/gm). On the console, the output of `badge` can be piped to a utility like [imagemagick](https://imagemagick.org/script/command-line-processing.php) e.g: `badge build passed :green | magick svg:- gif:-`.

---

### Overview (`vendor/http-deceiver/README.md`)

# vendor/http-deceiver

This is a workaround to bypass the blocker for https://github.com/badges/shields/issues/11071.

Related PR: https://github.com/badges/shields/pull/11279.

## LICENSE

This software is licensed under the MIT License.

Copyright Fedor Indutny, 2015.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

---

### Frontend Docs Index (`frontend/docs/index.md`)

---
sidebar_position: 1
---

# Intro

Shields.io is a service for concise, consistent, and legible badges, 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. It is used by some of the world's most popular open-source projects.

Browse a [complete list of badges](/badges) and locate a particular badge by using the search bar or by browsing the categories.

Use the builder to fill in required path parameters for that badge type (like your username or repo) and optionally customize (label, colors etc.). And it's ready for use! Copy your badge url or code snippet, which can then be added to places like your GitHub readme files or other web pages.

---

### Doc TUTORIAL (`doc/TUTORIAL.md`)

# Tutorial on how to add a badge for a service

This tutorial should help you add a service to shields.io in form of a badge. You will need to learn to use JavaScript, Git and GitHub, however, this document will guide you through that journey if you are a beginner. Please [improve the tutorial](https://github.com/badges/shields/edit/master/doc/TUTORIAL.md) while you read it.

## (1) Reading

- [Contributing Guidance](../CONTRIBUTING.md)
- [Documentation](https://contributing.shields.io/index.html) for the Shields Core API
- You can also read previous [merged pull-requests with the 'service-badge' label](https://github.com/badges/shields/pulls?utf8=%E2%9C%93&q=is%3Apr+label%3Aservice-badge+is%3Amerged) to see how other people implemented their badges.

## (2) Setup

### Pre-requisites

#### Git

You should have [git](https://git-scm.com/) installed. If you do not, [install git](https://www.linode.com/docs/development/version-control/how-to-install-git-on-linux-mac-and-windows/) and learn about the [GitHub workflow](http://try.github.io/).

#### Node, NPM

Node 24 and NPM 11.x or 12.x is required. If you don't already have them, install node and npm: https://nodejs.org/en/download/

### Setup a dev install

1. [Fork](https://github.com/badges/shields/fork) this repository.
2. Clone the fork `git clone git@github.com:YOURGITHUBUSERNAME/shields.git`
3. `cd shields`
4. Install project dependencies `npm ci`
5. Run the badge server and the frontend dev server `npm start`
6. Visit the website to check the front-end is loaded: [http://localhost:3000/](http://localhost:3000/).

In case you get the _"getaddrinfo ENOTFOUND localhost"_ error, visit [http://127.0.0.1:3000/](http://127.0.0.1:3000) instead or take a look at [this issue](https://github.com/angular/angular-cli/issues/2227#issuecomment-358036526).

## (3) Open an Issue

Before you want to implement your service, you may want to [open an issue](https://github.com/badges/shields/issues/new?template=3_Badge_request.yml) and describe what you have in mind:

- What is the badge for?
- Which API do you want to use?

You may additionally proceed to say what you want to work on. This information allows other humans to help and build on your work.

## (4) Implementing

### (4.1) Structure and Layout

Service badge code is stored in the [/services](https://github.com/badges/shields/tree/master/services/) directory. Each service has a directory for its files:

- In files ending with `.service.js`, you can find the code which handles incoming requests and generates the badges. Sometimes, code for a service can be re-used. This might be the case when you add a badge for an API which is already used by other badges.

  Imagine a service that lives at https://img.shields.io/example/some-param-here.
  - For services with a single badge, the badge code will generally be stored in `/services/example/example.service.js`. If you add a badge for a new API, create a new directory.

    Example: [Docs.rs](https://github.com/badges/shields/tree/master/services/docsrs)

  - For service families with multiple badges we usually store the code for each badge in its own file like this:
    - `/services/example/example-downloads.service.js`
    - `/services/example/example-version.service.js` etc.

    Example: [ruby gems](https://github.com/badges/shields/tree/master/services/gem)

- In files ending with `.tester.js`, you can find the code which uses the shields server to test if the badges are generated correctly. There is a [chapter on Tests][write tests].

### (4.2) Our First Badge

All service badge classes inherit from [BaseService] or another class which extends it. Other classes implement useful behavior on top of [BaseService].

- [BaseJsonService](https://contributing.shields.io/module-core_base-service_base-json-BaseJsonService.html) implements methods for performing requests to a JSON API and schema validation.
- [BaseJsonlService](https://contributing.shields.io/module-core_base-service_base-jsonl-BaseJsonlService.html) implements methods for performing requests to a JSONL API and schema validation.
- [BaseXmlService](https://contributing.shields.io/module-core_base-service_base-xml-BaseXmlService.html) implements methods for performing requests to an XML API and schema validation.
- [BaseYamlService](https://contributing.shields.io/module-core_base-service_base-yaml-BaseYamlService.html) implements methods for performing requests to a YAML API and schema validation.
- [BaseTomlService](https://contributing.shields.io/module-core_base-service_base-toml-BaseTomlService.html) implements methods for performing requests to a TOML API and schema validation.
- [BaseSvgScrapingService](https://contributing.shields.io/module-core_base-service_base-svg-scraping-BaseSvgScrapingService.html) implements methods for retrieving information from existing third-party badges.
- [BaseGraphqlService](https://contributing.shields.io/module-core_base-service_base-graphql-BaseGraphqlService.html) implements methods for performing requests to a GraphQL API and schema validation.
- If you are contributing to a _service family_, you may define a common super class for the badges or one may already exist.

[baseservice]: https://contributing.shields.io/module-core_base-service_base.html

As a first step we will look at the code for an example which generates a badge without contacting an API. Note that lower camelCase should be used for variables, functions, and path/query parameters.

```js
// (1)
import { BaseService } from '../index.js'

// (2)
export default class Example extends BaseService {
  // (3)
  static category = 'build'

  // (4)
  static route = { base: 'example', pattern: ':text' }

  // (5)
  async handle({ text }) {
    return {
      label: 'example',
      message: text,
      color: 'blue',
    }
  }
}
```

Description of the code:

1. Our service badge class will extend `BaseService` so we need to import it.
2. Our module must export a class which extends `BaseService`.
3. Returns the name of the category to sort this badge into (eg. "build"). Used to sort the examples on the main [shields.io](https://shields.io) website. [Here](https://github.com/badges/shields/blob/master/services/categories.js) is the list of the valid categories. See [section 4.4](#44-adding-documentation-to-the-frontend) for more details.
4. `route` declares the URL path at which the service operates. It also maps components of the URL path to handler parameters.
   - `base` defines the first part of the URL that doesn't change, e.g. `/example/`.
   - `pattern` defines the variable part of the route, everything that comes after `/example/`. It can include any number of named parameters. These are converted into regular expressions by [`path-to-regexp`][path-to-regexp]. Because a service instance won't be created until it's time to handle a request, the route and other metadata must be obtained by examining the classes themselves. [That's why they're marked `static`.][static]
   - See [designing badge URLs](./badge-urls.md) for additional naming and URL conventions.
   - `routeEnum` (optional): an array of strings that defines the allowed values for the first named parameter in `pattern`.
     - Only the first named parameter is validated by `routeEnum`.
     - Example: for `pattern: ':type/:user/:repo'`, set `static routeEnum = ['dt', 'dm', 'dd']`; the incoming `type` value is validated against that array.
     - If that first parameter value is not present in `routeEnum`, the request is rejected before `handle()` is invoked.

```js
// Example service that uses routeEnum
export default class Downloads extends BaseService {
  static route = { base: 'downloads', pattern: ':type/:user/:repo' }
  static routeEnum = ['dt', 'dm', 'dd']

  async handle({ type, user, repo }) {
    // `type` is guaranteed to be one of 'dt', 'dm', or 'dd'
  }
}
```

5. All badges must implement the `async handle()` function that receives parameters to render the badge. Parameters of `handle()` will match the name defined in `route` Because we're capturing a single variable called `text` our function signature is `async handle({ text })`. `async` is needed to let JavaScript do other things while we are waiting for result from external API. Although in this simple case, we don't make any external calls. Our `handle()` function should return an object with 3 properties:
   - `label`: the text on the left side of the badge
   - `message`: the text on the right side of the badge - here we are passing through the parameter we captured in the route regex
   - `color`: the background color of the right side of the badge

The process of turning this object into an image is handled automatically by the `BaseService` class.

To try out this example badge:

1. Copy and paste this code into a new file in `/services/example/example.service.js`
2. The server should restart on its own. (If it doesn't for some reason, quit the running server with `Control+C`, then start it again with `npm start`.)
3. Visit the badge at <http://localhost:8080/example/foo>. It should look like this: 

[path-to-regexp]: https://github.com/pillarjs/path-to-regexp#parameters
[static]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/static

### (4.3) Querying an API

The example above was completely static. In order to make a useful service badge we will need to get some data from somewhere. The most common case is that we will query an API which serves up some JSON data, but other formats (e.g: XML) may be used.

This example is based on the [Ruby Gems version](https://github.com/badges/shields/blob/master/services/gem/gem-version.service.js) badge:

```js
// (1)
import { renderVersionBadge } from '../version.js'
// (2)
import { BaseJsonService } from '../index.js'

// (3)
import Joi from 'joi'
const schema = Joi.object({
  version: Joi.string().required(),
}).required()

// (4)
export default class GemVersion extends BaseJsonService {
  // (5)
  static category = 'version'

  // (6)
  static route = { base: 'gem/v', pattern: ':gem' }

  // (7)
  static defaultBadgeData = { label: 'gem' }

  // (10)
  static render({ version }) {
    return renderVersionBadge({ version })
  }

  // (9)
  async fetch({ gem }) {
    return this._requestJson({
      schema,
      url: `https://rubygems.org/api/v1/gems/${gem}.json`,
    })
  }

  // (8)
  async handle({ gem }) {
    const { version } = await this.fetch({ gem })
    return this.constructor.render({ version })
  }
}
```

Description of the code:

1. In this case we are making a version badge, which is a common pattern. Instead of directly returning an object in this badge we will use a helper function to format our data consistently. There are a variety of helper functions to help with common tasks in `/services`. Some useful generic helpers can be found in:
   - [build-status.js](https://github.com/badges/shields/blob/master/services/build-status.js)
   - [color-formatters.js](https://github.com/badges/shields/blob/master/services/color-formatters.js)
   - [licenses.js](https://github.com/badges/shields/blob/master/services/licenses.js)
   - [text-formatters.js](https://github.com/badges/shields/blob/master/services/text-formatters.js)
   - [version.js](https://github.com/badges/shields/blob/master/services/version.js)
2. Our badge will query a JSON API so we will extend `BaseJsonService` instead of `BaseService`. This contains some helpers to reduce the need for boilerplate when calling a JSON API.
3. We perform input validation by defining a schema which we expect the JSON we receive to conform to. This is done using [Joi](https://github.com/hapijs/joi). Defining a schema means we can ensure the JSON we receive meets our expectations and throw an error if we receive unexpected input without having to explicitly code validation checks. The schema also acts as a filter on the JSON object. Any properties we're going to reference need to be validated, otherwise they will be filtered out. In this case our schema declares that we expect to receive an object which must have a property called 'version', which is a string. There is further documentation on [input validation](input-validation.md).
4. Our module exports a class which extends `BaseJsonService`
5. Returns the name of the category to sort this badge into (eg. "build"). Used to sort the examples on the main [shields.io](https://shields.io) website. [Here](https://github.com/badges/shields/blob/master/services/categories.js) is the list of the valid categories. See [section 4.4](#44-adding-documentation-to-the-frontend) for more details.
6. As with our previous badge, we need to declare a route. This time we will capture a variable called `gem`.
7. We can use `defaultBadgeData` to set a default `color`, `logo` and/or `label`. If `handle()` doesn't return any of these keys, we'll use the default. Instead of explicitly setting the label text when we return a badge object, we'll use `defaultBadgeData` here to define it declaratively.
8. We now jump to the bottom of the example code to the function all badges must implement: `async handle()`. This is the function the server will invoke to handle an incoming request. Because our URL pattern captures a variable called `gem`, our function signature is `async handle({ gem })`. We usually separate the process of generating a badge into 2 stages or concerns: fetch and render. The `fetch()` function is responsible for calling an API endpoint to get data. The `render()` function formats the data for display. In a case where there is a lot of calculation or intermediate steps, this pattern may be thought of as fetch, transform, render and it might be necessary to define some helper functions to assist with the 'transform' step.
9. Working our way upward, the `async fetch()` method is responsible for calling an API endpoint to get data. Extending `BaseJsonService` gives us the helper function `_requestJson()`. Note here that we pass the schema we defined in step 3 as an argument. `_requestJson()` will deal with validating the response against the schema and throwing an error if necessary.
   - `_requestJson()` automatically adds an Accept header, checks the status code, parses the response as JSON, and returns the parsed response.
   - `_requestJson()` uses [got](https://github.com/sindresorhus/got) to perform the HTTP request. Options can be passed to got, including method, query string, and headers. If headers are provided they will override the ones automatically set by `_requestJson()`. There is no need to specify json, as the JSON parsing is handled by `_requestJson()`. See the `got` docs for [supported options](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md).
   - Error messages corresponding to each status code can be returned by passing a dictionary of status codes -> messages in `httpErrors`.
   - A more complex call to `_requestJson()` might look like this:
     ```js
     return this._requestJson({
       schema: mySchema,
       url,
       options: { searchParams: { branch: 'master' } },
       httpErrors: {
         401: 'private application not supported',
         404: 'application not found',
       },
     })
     ```

10. Upward still, the `static render()` method is responsible for formatting the data for display. `render()` is a pure function so we can make it a `static` method. By convention we declare functions which don't reference `this` as `static`. We could explicitly return an object here, as we did in the previous example. In this case, we will hand the version string off to `renderVersionBadge()` which will format it consistently and set an appropriate color. Because `renderVersionBadge()` doesn't return a `label` key, the default label we defined in `defaultBadgeData` will be used when we generate the badge.

This code allows us to call this URL <https://img.shields.io/gem/v/formatador> to generate this badge: 

It is also worth considering the code we _haven't_ written here. Note that our example doesn't contain any explicit error handling code, but our badge handles errors gracefully. For example, if we call https://img.shields.io/gem/v/does-not-exist we render a 'not found' badge  because https://rubygems.org/api/v1/gems/this-package-does-not-exist.json returns a `404 Not Found` status code. When dealing with well-behaved APIs, some of our error handling will be handled implicitly in `BaseJsonService`.

Specifically `BaseJsonService` will handle the following errors for us:

- API does not respond
- API responds with a non-`200 OK` status code
- API returns a response which can't be parsed as JSON
- API returns a response which doesn't validate against our schema

Sometimes it may be necessary to manually throw an exception to deal with a non-standard error condition. If so, there are several standard exceptions that can be used. The errors are documented at [errors](https://contributing.shields.io/module-core_base-service_errors.html) and can be imported via the import shortcut and then thrown:

```js
import { NotFound } from '../index.js'

throw new NotFound({ prettyMessage: 'package not found' })
```

### (4.4) Adding Documentation to the Frontend

To render the shields.io website, we produce an [OpenAPI 3 specification](https://swagger.io/specification/) which describes the available badge endpoints. Then we use that specification to render the frontend.

Once we have implemented our badge, we want to add it to the index so that users can discover it. We will do this by adding an additional object `openApi` to our class. This object contains an [OpenAPI Paths Object](https://swagger.io/specification/#paths-object) describing the endpoint or endpoints exposed by this service class.

```js
// (1)
import { pathParams } from '../index.js'

export default class GemVersion extends BaseJsonService {
  // ...

  // (2)
  static category = 'version'

  static openApi = {
    // (3)
    '/gem/v/{gem}': {
      // (4)
      get: {
        // (5)
        summary: 'Gem Version',
        description:
          '[Ruby Gems](https://rubygems.org/) is a registry for ruby libraries',
        // (6)
        parameters: pathParams({
          name: 'gem',
          description: 'Name of the Ruby Gem',
          example: 'formatador',
        }),
      },
    },
  }
}
```

1. There are four helper functions we can use to assemble [Open API Parameter objects](https://swagger.io/specification/#parameter-object). These are:
   - `pathParam` - returns a single Parameter object describing a single path parameter
   - `pathParams` - returns an array of path parameter objects
   - `queryParam` - returns a single Parameter object describing a single query parameter
   - `queryParams` - returns an array of query parameter objects

   These four helper functions are documented in more detail at http://contributing.shields.io/module-core_base-service_openapi.html

2. We defined category earlier in the tutorial. The `category` property defines which heading in the index our example will appear under.
3. The keys of the `openApi` object are routes. In this case we only need to describe one route. In some cases, a service class can define more than one badge route. Open API doesn't have the concept of optional path parameters (more specifically, `in: 'path'` implies `required: true`) so if there are any optional path parameters in our route, our `openApi` object needs to describe two URLs: one without the optional parameter, and another with it.
4. The HTTP method. Shields only allows GET requests, so this is always `get`.
5. `summary` (required) is a short title or description of the badge. `description` is an optional longer description or additional documentation. We can use markdown or HTML syntax inside the `description` field.
6. `parameters` is an array of [Open API Parameter objects](https://swagger.io/specification/#parameter-object) describing any parameters we can pass to this route. This array should include all path parameters included in the key that this value object describes and all relevant query parameters. As a minimum, we need to supply `name` and `example`. The example must be a valid example of a value we can provide for this parameter. In this case we need a valid ruby gem, so we've picked [formatador](https://rubygems.org/gems/formatador). There are also optional keys we can pass. The code

   ```js
   parameters: pathParams({
     name: 'gem',
     description: 'Name of the Ruby Gem',
     example: 'formatador',
   })
   ```

   is equivalent to

   ```js
   parameters: [
     {
       name: 'gem',
       in: 'path',
       required: true,
       schema: { type: 'string' },
       example: 'formatador',
       description: 'Name of the Ruby Gem',
     },
   ]
   ```

   but we have used the helper function `pathParams` to imply some defaults and reduce the amount of code we need to write by hand.

7. Path and query parameters should be lower camelCase, for example use `gemName` instead of `gem_name`.

Save, run `npm start`, and you can see it [locally](http://127.0.0.1:3000/).

If you update `openApi`, you don't have to restart the server. Run `npm run prestart` in another terminal window and the frontend will update.

### (4.5) Write Tests

[write tests]: #45-write-tests

When creating a badge for a new service or changing a badge's behavior, tests should be included. They serve several purposes:

1. They speed up future contributors when they are debugging or improving a badge.
2. If the contributors would like to change your badge, chances are, they forget edge cases and break your code. Tests may give hints in such cases.
3. The contributor and reviewer can easily verify the code works as intended.
4. When a badge stops working on the live server, maintainers can find out right away.

There is a dedicated [tutorial for tests in the service-tests folder](service-tests.md). Please follow it to include tests on your pull-request.

### (4.6) Update the Docs

If your submission requires an API token or authentication credentials, please update [server-secrets.md](./server-secrets.md). You should explain what the token or credentials are for and how to obtain them.

## (5) Create a Pull Request

Once you have implemented a new badge:

- Before submitting your changes, please review the [coding guidelines](https://github.com/badges/shields/blob/master/CONTRIBUTING.md#coding-guidelines).
- [Create a pull-request](https://help.github.com/articles/creating-a-pull-request/) to propose your changes.
- CI will check the tests pass and that your code conforms to our coding standards.
- We also use [Danger](https://danger.systems/) to check for some common problems. The first comment on your pull request will be posted by a bot. If there are any errors or warnings raised, please review them.
- One of the [maintainers](https://github.com/badges/shields/blob/master/README.md#project-leaders) will review your contribution.
- We'll work with you to progress your contribution suggesting improvements if necessary. Although there are some occasions where a contribution is not appropriate, if your contribution conforms to our [guidelines](https://github.com/badges/shields/blob/master/CONTRIBUTING.md#badge-guidelines) we'll aim to work towards merging it. The majority of pull requests adding a service badge are merged.
- If your contribution is merged, the final comment on the pull request will be an automated post which you can monitor to tell when your contribution has been deployed to production.

---

### Doc Adding New Config Values (`doc/adding-new-config-values.md`)

# Adding New Config Values

The Badge Server supports a [variety of methods for defining configuration settings and secrets](./server-secrets.md), and provides a framework for loading those values during bootstrapping.

Any new configuration setting or secret must be correctly registered so that it will be loaded at startup along with the others.

This generally includes adding the corresponding information for your new setting(s)/secret(s) to the following locations:

- [core/server/server.js](https://github.com/badges/shields/blob/master/core/server/server.js) - Add the new values to the [schemas](https://github.com/badges/shields/blob/e1443eb7146466ad987830c1f286e2b0b186500c/core/server/server.js#L67-L215). Secrets/tokens/etc. should go in the `privateConfigSchema` while non-secret configuration settings should go in the `publicConfigSchema`.
- [config/custom-environment-variables.yml](https://github.com/badges/shields/blob/master/config/custom-environment-variables.yml)
- [docs/server-secrets.md](https://github.com/badges/shields/blob/master/doc/server-secrets.md) (only applicable for secrets)
- [config/default.yml](https://github.com/badges/shields/blob/master/config/default.yml) (optional)
- Any other template config files (e.g. `config/local.template.yml`) (optional)

The exact values needed will depend on what type of secret/setting you are adding, but for reference a few commits are included below which added secrets and or settings:

- (secret) [4a75cf09a786c5e93232ae699af985e5bc89440e](https://github.com/badges/shields/commit/4a75cf09a786c5e93232ae699af985e5bc89440e)
- (secret) [bd6f4ee1465d14a8f188c37823748a21b6a46762](https://github.com/badges/shields/commit/bd6f4ee1465d14a8f188c37823748a21b6a46762)
- (secret) [0fd557d7bb623e3852c92cebac586d5f6d6d89d8](https://github.com/badges/shields/commit/0fd557d7bb623e3852c92cebac586d5f6d6d89d8)
- (configuration setting) [b1fc4925928c061234e9492f3794c0797467e123](https://github.com/badges/shields/commit/b1fc4925928c061234e9492f3794c0797467e123)

Don't hesitate to reach out if you're unsure of the exact values needed for your new secret/setting, or have any other questions. Feel free to post questions on your corresponding Issue/Pull Request, and/or ping us on the `contributing` channel on our Discord server.

---

### Doc Authentication (`doc/authentication.md`)

# Badges Requiring Authentication

There are two patterns for how shields.io can interact with APIs that require auth:

1. We can store one token at the service level which allows us to read public data for everyone's projects, or lift a rate limit. If you are looking for information on configuring credentials for a self-hosted instance see https://github.com/badges/shields/blob/master/doc/server-secrets.md
2. If every user needs to provide their own token, that has to be a token which can be passed to us as a query param in the badge URL. This means it must be possible to generate a key or token that can be exposed in a public github README public with no negative consequences. (i.e: that key or token only exposes public metrics).

If every user would need to supply their own token for some particular service and it is only possible to generate a key or token which allows access to sensitive data or allows write access to resources, we can't provide an integration for this service.

---

### Doc Badge Redirectors (`doc/badge-redirectors.md`)

# Badge Redirectors

When a badge URL pattern needs to change, we should ensure that existing badges continue to work. This is achieved through redirectors, which automatically redirect old URLs to their new equivalents.

Redirectors issue HTTP 301 (permanent redirect) responses, allowing browsers and clients to follow the redirect to the new URL while maintaining backward compatibility for all existing badge usages.

## Creating a Redirector

Redirectors are created using the `redirector()` function from `core/base-service/redirector.js`.

### Required Properties

Every redirector must specify:

- **`category`** - The badge category (e.g. `'build'`, `'version'`, `'downloads'`). [Here](https://github.com/badges/shields/blob/master/services/categories.js) is the list of valid categories.
- **`route`** - An object defining the old URL pattern to match:
  - `base` - The first part of the old URL path
  - `pattern` - The variable part of the route (using [path-to-regexp](https://github.com/pillarjs/path-to-regexp) syntax)
- **`transformPath`** - A function that transforms the old path parameters into the new badge path
- **`dateAdded`** - The date when this redirector was added (e.g., `new Date('2025-11-23')`)

### Optional Properties

- **`name`** - Custom name for the service class (auto-generated from route if not specified)
- **`transformQueryParams`** - A function to transform path parameters into query parameters
- **`overrideTransformedQueryParams`** - Boolean (default: `false`). When `true`, query params from the URL take precedence over transformed params in case of conflicts
- **`isRetired`** - Boolean (default: `true`). Set to `false` for non-retired redirectors that should appear in the API documentation
- **`openApi`** - OpenAPI documentation object. Only needed for non-retired redirectors (`isRetired: false`) that should appear in the user-facing documentation

## Examples

### Example 1: Simple Path Redirect

When simply redirecting from an old URL pattern to a new one:

```js
import { redirector } from '../index.js'

export default redirector({
  category: 'other',
  route: {
    base: 'badge/endpoint',
    pattern: '',
  },
  transformPath: () => '/endpoint',
  dateAdded: new Date('2025-01-01'),
})
```

This redirects `/badge/endpoint` to `/endpoint`.

### Example 2: Redirecting with Path Parameters

When the old URL has parameters that need to be mapped to a new structure:

```js
import { redirector } from '../index.js'

export default redirector({
  category: 'analysis',
  route: {
    base: 'scrutinizer',
    pattern: ':vcs/:user/:repo/:branch*',
  },
  routeEnum: ['g', 'b'],
  transformPath: ({ vcs, user, repo, branch }) =>
    `/scrutinizer/quality/${vcs}/${user}/${repo}${branch ? `/${branch}` : ''}`,
  dateAdded: new Date('2025-02-02'),
})
```

This redirects patterns like `/scrutinizer/g/user/repo` to `/scrutinizer/quality/g/user/repo`.

### Example 3: Converting Path Parameters to Query Parameters

When migrating from path-based to query-based parameters:

```js
import { redirector } from '../index.js'

export default redirector({
  category: 'monitoring',
  route: {
    base: 'website',
    pattern: ':protocol/:hostAndPath+',
  },
  routeEnum: ['https', 'http'],
  transformPath: () => '/website',
  transformQueryParams: ({ protocol, hostAndPath }) => ({
    url: `${protocol}://${hostAndPath}`,
  }),
  dateAdded: new Date('2025-03-03'),
})
```

This redirects `/website/https/example.com` to `/website?url=https://example.com`.

### Example 4: Handling Query Parameter Conflicts

When both path-based and query-string parameters exist, conflicts can occur. By default, transformed query parameters take precedence over user-provided ones. Use `overrideTransformedQueryParams: true` to reverse this behavior and let user-provided query parameters win:

```js
import { redirector } from '../index.js'

export default redirector({
  category: 'build',
  route: {
    base: 'old/service',
    pattern: 'token/:token/:param',
  },
  transformPath: ({ param }) => `/new/service/${param}`,
  transformQueryParams: ({ token }) => ({ token }),
  overrideTransformedQueryParams: true,
  dateAdded: new Date('2025-04-04'),
})
```

If a user specifies `/old/service/token/abc123/foo?token=xyz789`, `xyz789` takes precedence due to `overrideTransformedQueryParams: true`.

### Example 5: Non-Deprecated Redirector with Documentation

In some cases, a badged based on a redirector may not be retired and should appear on the website. This requires setting `isRetired: false` and providing an `openApi` specification:

```js
import { redirector, pathParam } from '../index.js'
import { commonParams } from '../maven-metadata/maven-metadata.js'

export default redirector({
  category: 'version',
  isRetired: false,
  route: {
    base: 'gradle-plugin-portal/v',
    pattern: ':pluginId',
  },
  openApi: {
    '/gradle-plugin-portal/v/{pluginId}': {
      get: {
        summary: 'Gradle Plugin Portal Version',
        parameters: [
          pathParam({ name: 'pluginId', example: 'com.gradle.plugin-publish' }),
          ...commonParams,
        ],
      },
    },
  },
  transformPath: () => '/maven-metadata/v',
  transformQueryParams: ({ pluginId }) => ({
    metadataUrl: `https://plugins.gradle.org/m2/...`,
    label: 'plugin portal',
  }),
  overrideTransformedQueryParams: true,
  dateAdded: new Date('2025-05-05'),
})
```

This pattern is uncommon and should only be used when the redirect is part of the public API rather than a legacy compatibility layer.

## File Naming Convention

Redirectors should preferably be specified in a separate file named using the `*-redirect.service.js` pattern, and placed in the appropriate service directory. For example:

```
services/
  npm/
    my-service.service.js
    my-service.tester.js
    my-service-redirect.service.js
    my-service-redirect.tester.js
```

## Testing Redirectors

### Basic Test Structure

Tests for redirectors should verify that the old URL correctly redirects to the new URL.

Example redirect tests from `npm-downloads.tester.js`:

```js
import { createServiceTester } from '../tester.js'
export const t = await createServiceTester()

t.create('Total downloads redirect: unscoped package')
  .get('/dt/left-pad.svg')
  .expectRedirect('/npm/d18m/left-pad.svg')

t.create('Total downloads redirect: scoped package')
  .get('/dt/@cycle/core.svg')
  .expectRedirect('/npm/d18m/@cycle/core.svg')
```

The `expectRedirect()` helper verifies that:

- The response is an HTTP 301 (permanent redirect)
- The `Location` header points to the expected new URL

### Running Redirector Tests

Run tests the same way as other service tests:

```bash
npm run test:services -- --only=npm
```

Or for more specific tests:

```bash
npm run test:services -- --only=npm --fgrep="redirect"
```

## What Happens Next?

We'll keep the redirector for a minimum of one year. It may stay in place for significantly longer, and can only be sunset if one or both of the following conditions are met:

- The redirector led to less than 100 badge renders on a weekday.
- The redirector serves less than 1% of the traffic of the new badge URL it points to.

It can then be removed and replaced with a retired badge linking to an issue that explains the migration path, for example:

```js
import { retiredService } from '../index.js'

export default retiredService({
  category: 'build',
  route: {
    base: 'github/workflow/status',
    pattern: ':various+',
  },
  label: 'githubworkflowstatus',
  issueUrl: 'https://github.com/badges/shields/issues/8671',
  dateAdded: new Date('2025-11-29'),
})
```

Doing so will render badges similar to the following: 

Removal of the resulting retired badge follows the process documented [here](./retiring-badges.md).

---

### Doc Badge Urls (`doc/badge-urls.md`)

# Badge URL Conventions

- The format of new badges should be of the form `/SERVICE/NOUN/PARAMETERS?QUERYSTRING` e.g: `/github/issues/:user/:repo`. The service is github, the badge is for issues, and the parameters are `:user/:repo`.
- The `NOUN` part of the route is:
  - singular if the badge message represents a single entity, such as the current status of a build (e.g: `/build`), or a more abstract or aggregate representation of the thing (e.g.: `/coverage`, `/quality`)
  - plural if there are (or may) be many of the thing (e.g: `/dependencies`, `/stars`)
- Parameters should always be part of the route if they are required to display a badge e.g: `:packageName` and should be lower camelCase.
- `routeEnum` support: when a service defines a finite set of allowed values for a parameter, the service can declare `static routeEnum = [...]`. In that case the first named parameter in the route `pattern` is treated as the enum to validate against `routeEnum`.
- Common optional params like, `:branch` or `:tag` should also be passed as part of the route.
- Query string parameters should be used when:
  - The parameter is related to formatting. e.g: `/appveyor/tests/:user/:repo?compact_message`.
  - The parameter is for an uncommon optional attribute, like an alternate registry URL.
  - The parameter triggers application of alternative logic, like version semantics. e.g: `/github/v/tag/:user/:repo?sort=semver`.
  - Services which require a url/hostname parameter always should use a query string parameter to accept that value. e.g: `/discourse/topics?server=https://meta.discourse.org`.
  - Similar to parameters, query string parameters should be lower camelCase e.g: 'labelColor' and not 'label_color'.
- Query parameters for an alternative host of upstream data (like registry, repository, selfhost instance, etc) should be called `baseUrl` and include a full URL with protocol and path (if required).

It is convention to use the following standard routes and abbreviations across services:

- Coverage: `/coverage`
- Downloads or Installs:
  - Total: `/dt` - Use this even for services that only provide the total download/install data
  - Per month: `/dm`
  - Per week: `/dw`
  - Per day: `/dd`
- Rating:
  - Numeric: `/rating`
  - Stars: `/stars`
- License: `/l`
- Version or Release: `/v`

---

### Doc Code Walkthrough (`doc/code-walkthrough.md`)

# High-level code walkthrough

## Code inventory and testing strategy

The Shields codebase is divided into several parts:

1.  The frontend
    1. [`frontend`][frontend]
2.  The badge renderer (which is available as an npm package)
    1.  [`badge-maker`][badge-maker]
3.  The base service classes (about 8% of the code, and probably the most important code in the codebase)
    1.  [`core/base-service`][base-service]
4.  The server code and a few related odds and ends
    1.  [`core/server`][server]
5.  Helper code for token pooling and persistence (used to avoid GitHub rate limiting)
    1.  [`core/token-pooling`][token-pooling]
6.  Service common helper functions (about 7% of the code, and fairly important since it’s shared across much of the service code)
    1.  `*.js` in the root of [`services`][services]
7.  The services themselves (about 80% of the code)
    1.  `*.js` in the folders of [`services`][services]

[frontend]: https://github.com/badges/shields/tree/master/frontend
[badge-maker]: https://github.com/badges/shields/tree/master/badge-maker
[base-service]: https://github.com/badges/shields/tree/master/core/base-service
[server]: https://github.com/badges/shields/tree/master/core/server
[token-pooling]: https://github.com/badges/shields/tree/master/core/token-pooling
[services]: https://github.com/badges/shields/tree/master/services

The tests are also divided into several parts:

1.  Unit and functional tests of the badge renderer
    1.  `badge-maker/**/*.spec.js`
2.  Unit and functional tests of the core code
    1.  `core/**/*.spec.js`
3.  Unit and functional tests of the service helper functions
    1.  `services/*.spec.js`
4.  Unit and functional tests of the service code (we have only a few of these)
    1.  `services/*/**/*.spec.js`
5.  End-to-end tests for the frontend
    1.  `cypress/e2e/*.cy.js`
6.  The service tester and service test runner
    1.  [`core/service-test-runner`][service-test-runner]
7.  [The service tests themselves][service tests] live integration tests of the services, and some mocked tests
    1.  `*.tester.js` in subfolders of [`services`][services]
8.  Integration tests of PostgreSQL-backed persistence code
    1.  [`core/token-pooling/sql-token-persistence.integration.js`][sql-token-persistence.integration]
9.  Integration tests of the GitHub authorization code
    1.  [`services/github/github-api-provider.integration.js`][github-api-provider.integration]

[service-test-runner]: https://github.com/badges/shields/tree/master/core/service-test-runner
[service tests]: https://github.com/badges/shields/blob/master/doc/service-tests.md
[sql-token-persistence.integration]: https://github.com/badges/shields/blob/master/core/token-pooling/sql-token-persistence.integration.js
[github-api-provider.integration]: https://github.com/badges/shields/blob/master/services/github/github-api-provider.integration.js

Our goal is to reach 100% coverage of the code in the frontend, core, and service helper functions when the unit and functional tests are run.

Our test strategy for the service code is a bit different. It’s primarily based on live integration tests. That’s because service response formats can change, and when they do the badges break. We want our tests to fail when this happens. That way we can fix the problems proactively instead of waiting for users to report them. There’s a good discussion about this decision in [#927][issue 927]. It’s acceptable to write mocked tests of logic that is difficult to reach using live tests, however where possible, it’s preferred to test this kind of logic through unit tests (e.g. of `render()` and `transform()` functions).

[issue 927]: https://github.com/badges/shields/issues/927

## Server initialization

1.  The server entrypoint is [`server.js`][entrypoint] which sets up error reporting, loads config, and creates an instance of the server.

2.  The Server, which is defined in [`core/server/server.js`][core/server/server], is based on the web framework [Scoutcamp][]. It creates an http server, sets up helpers for token persistence and monitoring. Then it loads all the services, injecting dependencies as it asks each one to register its route with Scoutcamp.

3.  The service registration continues in `BaseService.register`. From its `route` property, it derives a regular expression to match the route path, and invokes `camp.route` with this value.

4.  At this point the situation gets gnarly and hard to follow. For the purpose of initialization, suffice it to say that `camp.route` invokes a callback with the four parameters `( queryParams, match, end, ask )` which is created in a legacy helper function in [`legacy-request-handler.js`][legacy-request-handler]. This callback delegates to a callback in `BaseService.register` with three different parameters `( queryParams, match, sendBadge )`, which then runs `BaseService.invoke`. `BaseService.invoke` instantiates the service and runs `BaseService#handle`.

[entrypoint]: https://github.com/badges/shields/blob/master/server.js
[core/server/server]: https://github.com/badges/shields/blob/master/core/server/server.js
[scoutcamp]: https://github.com/badges/sc
[legacy-request-handler]: https://github.com/badges/shields/blob/master/core/base-service/legacy-request-handler.js

## Downstream caching

1.  In production, the majority of requests are served from caches, including the browser cache, GitHub’s camo proxy server, and other downstream caches.
2.  The Shields servers sit behind the Cloudflare CDN. The CDN itself handles about 40% of the HTTPS requests that come in.
3.  The remaining requests are proxied to one of the servers.
4.  See the [production hosting documentation][production hosting] for a fuller discussion of the production architecture.

[production hosting]: https://github.com/badges/shields/blob/master/doc/production-hosting.md

## How the server makes a badge

1.  An HTTPS request arrives. Scoutcamp inspects the URL path and matches it against the regexes for all the registered routes until it finds one that matches. (See _Initialization_ above for an explanation of how routes are registered.)
2.  Scoutcamp invokes a callback with the four parameters: `( queryParams, match, end, ask )`. This callback is defined in [`legacy-request-handler`][legacy-request-handler]. A timeout is set to handle unresponsive service code and the next callback is invoked: the legacy handler function.
3.  The legacy handler function receives `( queryParams, match, sendBadge )`. Its job is to extract data from the regex `match` and `queryParams`, and then invoke `sendBadge` with the result.
4.  The implementation of this function is in `BaseService.register`. It works by running `BaseService.invoke`, which instantiates the service, injects more dependencies, and invokes `BaseService.handle` which is implemented by the service subclass.
5.  The job of `handle()`, which should be implemented by each service subclass, is to return an object which partially describes a badge or throw one of the handled error classes. "Partially rendered" most commonly means a non-empty message and an optional color. In the case of the Endpoint badge, it could include many other parameters. At the time of writing the handled error classes were NotFound, InvalidResponse, Inaccessible, and InvalidParameter. Throwing any other error is a programmer error which will be [reported][error reporting] and described to the user as a **shields internal error**.
6.  A typical `handle()` function delegates to one or more helpers to handle stages of the request:
    1.  **fetch**: load the needed data from the upstream service and validate it
    2.  **transform**: pluck, convert, or summarize the response format into a few properties which will be displayed on the badge
    3.  **render**: given a few properties, return a message, optional color, and optional label.
7.  When an error is thrown, BaseService steps in and converts the error object to renderable properties: `{ isError, message, color }`.
8.  The service invokes [`coalesceBadge`][coalescebadge] whose job is to coalesce query string overrides with values from the service and the service’s defaults to produce an object that fully describes the badge to be rendered.
9.  `sendBadge` is invoked with that object. It does some housekeeping on the timeout. Then it renders the badge to svg or raster and pushes out the result over the HTTPS connection.

[error reporting]: https://github.com/badges/shields/blob/master/doc/production-hosting.md#error-reporting
[coalescebadge]: https://github.com/badges/shields/blob/master/core/base-service/coalesce-badge.js

---

### Doc Input Validation (`doc/input-validation.md`)

# Input Data Validation

When we receive input data from an upstream API, we perform input validation to:

- Ensure we won't throw a runtime error trying to render a badge
- Ensure we won't render badges with spurious or unexpected output e.g:     etc
- Express and document our understanding of the input data

## Writing schemas and validation

- The default validation mechanism should be to use [Joi](https://github.com/hapijs/joi) to define a schema for the input data. Validation against Joi schemas is implemented in the base classes and inherited by every service class that extends them. Sometimes additional manual validation is needed which can't be covered by Joi and plugins in which case we implement it by hand.

- If validation is implemented manually (because we need to enforce a constraint that can't be expressed with Joi), invalid data should throw an [InvalidResponse](https://contributing.shields.io/module-core_base-service_errors-InvalidResponse.html) exception.

- Our definition of "valid" should not be stricter than the upstream API's definition of "valid".

- The schema/validation we choose is informed by the assumptions we're making about the data. e.g:
  - If we're going to use a value, make sure it exists.
  - If we need to multiply it by something, we check it's a number.
  - If we're going to call `.split()` on it, we make sure it's a string.
  - If we're going to address `foo[0]`, `foo` must be an array.
  - If we're going to sort a version on the assumption it is a semver, check it's a semver

- We don't need to validate characteristics we don't rely on. For example, if we're just going to render a version on a badge with the same exact value from the API response and do not need to sort or transform the value, then it doesn't matter what format the version number is in. We can use a very relaxed schema to validate in this case, e.g. `Joi.string().required()`

- https://joi.dev/tester/ is a tool that can be used to reverse engineer a schema from an API response. This can be a great starting point to tweak from. If using this as a starting point, remember to remove fields we don't rely on to render a badge.

- If theory (docs) and practice (real-world API responses) conflict, real-world outputs take precedence over documented behaviour. e.g: if the docs say version is a semver but we learn that there are real-world packages where the version number is `0.3b` or `1.2.1.27` then we should accept those values in preference to enforcing the documented API behaviour.

- Shields is descriptive rather than prescriptive. We reflect the established norms of the communities we serve.

- It is fine to define a single schema which is applied to multiple badges. For example, we could define a schema that says:

  ```js
  const schema = Joi.object({
    license: Joi.string().required(),
    version: Joi.string().required(),
  }).required()
  ```

  and have both the license and version badges validate the response against that schema.

- For build status badges there is a shared [isBuildStatus](https://github.com/badges/shields/blob/master/services/build-status.js) validator. In most cases build status badges should use `isBuildStatus` or input validation and `renderBuildStatusBadge` should be used for rendering. Any additional status values can be added to the relevant color arrays.

## Identifying problems

- If we know of a real-world example of a package/repo/etc that causes us to render an invalid value on a badge (e.g:    ) our input validation is broken and we should fix it.

- If we know of a real-world example of a package/repo/etc that causes us to throw an unhandled runtime exception, our input validation is broken and we should fix it.

- We should not fail to render a badge because of a validation failure on a field that isn't necessary to render the badge. In the above example of a shared license/version schema: If we become aware of a real-world example of a package/repo/etc that has a `version` key but not a `license` key then we should split the schema (or make `version` optional and handle the error in code).

---

### Doc Json Format (`doc/json-format.md`)

# JSON Format

Even though Shields is probably best known for its SVG badges, you can also retrieve a JSON payload by replacing the `.svg` extension with `.json`.

For instance, hitting [this endpoint](https://img.shields.io/badge/hello-world-brightgreen.json) will generate the following payload:

```
{
  "name": "hello",
  "label": "hello",
  "value": "world",
  "message": "world",
  "color": "brightgreen"
}
```

Note that the values of the `name` and `value` fields are duplicates of the `label` and `message` ones, respectively. As of April 2019, `name` and `value` are deprecated and will be removed in a future release, please consider migrating your application to use `label` and `message` instead.

Feel free to [open an issue](https://github.com/badges/shields/issues/new/choose) if you have any queries regarding the JSON format.

---

### Doc Performance Testing (`doc/performance-testing.md`)

# Performance testing

Shields has some basic tooling available to help you get started with performance testing.

## Benchmarking the badge generation

Want to micro-benchmark a section of the code responsible for generating the static badges? Follow these two simple steps:

1. Surround the code you want to time with `console.time` and `console.timeEnd` statements. For example:

```
console.time('makeBadge')
const svg = makeBadge(badgeData)
console.timeEnd('makeBadge')
```

2. Run `npm run benchmark:badge` in your terminal. An average timing will be displayed!

If you want to change the number of iterations in the benchmark, you can modify the values specified by the `benchmark:badge` script in _package.json_. If you want to benchmark a specific code path not covered by the static badge, you can modify the badge URL in _scripts/benchmark-performance.js_.

## Profiling the full code

Want to have an overview of how the entire application is performing? Simply run `npm run profile:server` in your terminal. This will start the backend server (i.e. without the frontend) in profiling mode and any requests you make on `localhost:8080` will generate data in a file with a name similar to _isolate-00000244AB6ED3B0-11920-v8.log_.

You can then make use of this profiling data in various tools, for example [flamebearer](https://github.com/mapbox/flamebearer):

```
npm install flamebearer
node --prof-process --preprocess -j isolate-00000244AB6ED3B0-11920-v8.log | flamebearer
```

An example output is the following:

---

### Doc Production Hosting (`doc/production-hosting.md`)

# Production hosting

Production hosting is managed by the Shields ops team:

- [jNullj](https://github.com/jnullj)
- [LitoMore](https://github.com/LitoMore)
- [paulmelnikow](https://github.com/paulmelnikow)
- [PyvesB](https://github.com/PyvesB)

[][operations issues]

[#ops chat room][ops discord]

[operations issues]: https://github.com/badges/shields/issues?q=is%3Aissue+is%3Aopen+label%3Aoperations
[ops discord]: https://discordapp.com/channels/308323056592486420/480747695879749633

| Component | Subcomponent | People with access |
| --- | --- | --- |
| Fly.io | Admin access | @calebcartwright, @chris48s, @paulmelnikow, @PyvesB |
| Fly.io | Account access | @calebcartwright, @chris48s, @jNullj, @paulmelnikow, @PyvesB |
| Cloudflare (CDN) | Account owner | @espadrine |
| Cloudflare (CDN) | Admin access | @calebcartwright, @chris48s, @espadrine, @paulmelnikow, @PyvesB |
| DNS | Account owner | @olivierlacan |
| DNS | Read-only account access | @chris48s, @espadrine, @paulmelnikow |
| Sentry | Owner | @paulmelnikow, @jNullj |
| Sentry | Account access | @calebcartwright, @jNullj, @paulmelnikow, @PyvesB |
| Metrics server | Owner | @platan |
| More metrics | Owner | @RedSparr0w |
| Twitch | OAuth app | @PyvesB |
| Reddit | OAuth app | @chris48s, @PyvesB |
| Discord | OAuth app | @PyvesB |
| CurseForge | OAuth app | @PyvesB |
| GitLab | Account owner | @calebcartwright |
| GitLab | Account access | @calebcartwright, @chris48s, @paulmelnikow, @PyvesB |

## Attached state

Shields has mercifully little persistent state:

1. The GitHub tokens we collect are stored in a fly.io postgres database
2. The server keeps the [resource cache][] in memory. It is neither persisted nor inspectable.

[resource cache]: https://github.com/badges/shields/blob/master/core/base-service/resource-cache.js

## Configuration

To bootstrap the configuration of non-secret settings, we set a single environment variable:

```
NODE_CONFIG_ENV=shields-io-production
```

With that variable set, the server ([using `config`][config]) reads these files:

- [`local-shields-io-production.yml`][local-shields-io-production.yml]. This file contains secrets which are checked in with a deploy commit.
- [`shields-io-production.yml`][shields-io-production.yml]. This file contains non-secrets which are checked in to the main repo.
- [`default.yml`][default.yml]. This file contains defaults.

Secrets are supplied directly as environment vars.

[config]: https://github.com/lorenwest/node-config/wiki/Configuration-Files
[local-shields-io-production.yml]: ../config/local-shields-io-production.template.yml
[shields-io-production.yml]: ../config/shields-io-production.yml
[default.yml]: ../config/default.yml

## Badge CDN

Sitting in front of our servers is a Cloudflare Free account which provides several services:

- Global CDN, caching, and SSL gateway for `img.shields.io` and `shields.io`
- Analytics through the Cloudflare dashboard
- DNS resolution for `shields.io` (and subdomains)

Cloudflare is configured to respect the servers' cache headers.

### Fly.io Deployment

Both the badge server and frontend are served from Fly.io. Deployments are triggered using GitHub actions in a private repo.

The raster server `raster.shields.io` (a.k.a. the rasterizing proxy) is also hosted on Fly.io. It's managed in the [squint](https://github.com/badges/squint/) repo.

## DNS

DNS is registered with [DNSimple][].

[dnsimple]: https://dnsimple.com/

## Error reporting

[Error reporting][sentry] is one of the most useful tools we have for monitoring the server. It's generously donated by [Sentry][sentry home]. We bundle [`@sentry/node`][sentry-node] into the application, and the Sentry DSN is configured via `local-shields-io-production.yml` (see [documentation][sentry configuration]).

[sentry]: https://sentry.io/shields/
[sentry-node]: https://www.npmjs.com/package/@sentry/node
[sentry home]: https://sentry.io/shields/
[sentry configuration]: https://github.com/badges/shields/blob/master/doc/self-hosting.md#sentry

## URLs

The canonical and only recommended domain for badge URLs is `img.shields.io`. Currently it is possible to request badges on both `img.shields.io` and `shields.io` i.e: https://img.shields.io/badge/build-passing-brightgreen and https://shields.io/badge/build-passing-brightgreen will both work. However:

- We never show or generate the `img.`-less URL format on https://shields.io/
- We make no guarantees about the `img.`-less URL format. At some future point we may remove the ability to serve badges on `shields.io` (without `img.`) without any warning. `img.shields.io` should always be used for badge urls.

## Monitoring

Overall server performance and requests by service are monitored using [Prometheus and Grafana][server metrics].

Request performance is monitored in two places:

- [Status][] (using NodePing)
- [Server metrics][] using Prometheus and Grafana [#monitor chat room][monitor discord]

[metrics]: https://metrics.shields.io/
[status]: https://nodeping.com/reports/status/YBISBQB254
[server metrics]: https://metrics.shields.io/
[monitor discord]: https://discordapp.com/channels/308323056592486420/470700909182320646

---

### Doc Releases (`doc/releases.md`)

# Releases

Shields is a community project that is stewarded by a handful of core maintainers who contribute on a volunteer basis. We do our best to maintain the availability and reliability of the service, and enhance and improve the project overall. However, if you've spotted something wrong or would like to see a specific feature implemented, please consider helping us resolve it by submitting a pull request. All community contributions, even documentation improvements, are welcome!

https://github.com/badges/shields is a monorepo and hosts the Shields frontend and server code as well as the [badge-maker][npm package] NPM library (and the [badge design specification](https://github.com/badges/shields/tree/master/spec)). The packaging and release processes for these items are described in the respective sections below.

## badge-maker package

We follow [Semantic Versioning](https://semver.org/) for the badge-maker package, and publish git Tags using the form `X.Y.Z`, and all such tags of this form are badge-maker releases (e.g. [3.3.0](https://github.com/badges/shields/releases/tag/3.3.0))

The [badge-maker][npm package] is also published to the npm.js registry.

Releases of badge-maker are done as and when needed, and not on any predetermined interval.

## Shields.io service

The [Shields.io Service][shields.io] consists of the frontend [https://shields.io][shields.io] website which allows users to browse and discover available badges, as well as the badge server backend that serves up requested badges (e.g. https://img.shields.io/badge/badges-rock-blue).

This is the core, free, anonymous service available for anyone to use and which serves up hundreds of millions of badges every month!

We do not have a fixed schedule for deploying updates to the Shields.io production environment, but we typically deploy the latest version at least once per week.

We do not have any guaranteed SLA for the Shields.io service, but we do have monitoring and observability capabilities in place and our [Maintainer team](https://github.com/badges/shields#project-leaders) will review and address any availability, performance, etc. issues on a best-effort basis.

More information about the production environment can be found [here][production hosting]

## Shields server

Some users may wish to host their own instance of Shields so we also make it possible for users to do so. This is particularly useful if you want to serve badges for resources that require authentication or are not exposed to the internet (e.g: inside a corporate network). A variety of options are available either by installing from source or using a docker image.

### Important information

This Shields server is the exact same codebase that powers the main Shields.io service; we do not have a separate self-hosted version of Shields. This means that the server codebase is geared towards the development, maintenance, and operation of the Shields.io service so there are a few things to note:

- We often have to reject or de-prioritize feature requests that are only applicable for self-hosting and/or which may be problematic for the core Shields.io offering (e.g. requiring authentication on the badge server)
- We do not accept new badges that cannot be utilized with Shields.io (e.g. those that would always require user-specific authorization)
- We do not do any additional testing nor validation of self-hosted instances (e.g. we test Shields.io with Jira Cloud badges, but we don't test a self-hosted Shields server against a self-hosted Jira server instance with auth)
- We're happy to try to offer some tips and guidance to self-hosters when and where we can, but we don't have the ability to troubleshoot/debug/support/etc. self-hosted Shields servers
  - Note that Paul, a core team member, offers some paid support options for those interested in self-hosting support. Contact him at  for more information.

We are happy to document and collate any self-hosting patterns/approaches that others have found to be helpful to be able to share with the broader community. If you have any self-hosting material you'd like to share, please feel free to get in contact with us (or even open a PR) and we'll work with you to get that incorporated for the benefit of other self-hosters!

### Server Releases

We try to make it as easy as possible for users to self-host a Shields server so we publish a few releases of the server. Please be sure to refer to the [self hosting guide][self hosting] for a detailed walk through on how to spin up a server.

- The server uses [Calendar Versioning](https://calver.org/). Tags of the form `server-YYYY-MM-DD` are server releases (these are the tags that are relevant to self-hosting users, e.g. [server-2021-02-01](https://github.com/badges/shields/releases/tag/server-2021-02-01)).
- As well as [tags on GitHub](https://github.com/badges/shields/tags), server releases are also pushed to [DockerHub](https://registry.hub.docker.com/r/shieldsio/shields/tags) and [GitHub Container Registry](https://github.com/badges/shields/pkgs/container/shields/versions?filters%5Bversion_type%5D=tagged). See the self-hosting section on [Docker](https://github.com/badges/shields/blob/master/doc/self-hosting.md#Docker) for more details.
- We publish release notes for server releases in the [CHANGELOG](https://github.com/badges/shields/blob/master/CHANGELOG.md). There may occasionally be non-backwards compatible changes to be aware of.
- We will normally put out one release per month. If there is a security patch or major bugfix affecting self-hosting users, we may put out an out-of-sequence release.
- Releases are just a snapshot in time. We advise always tracking the latest release to ensure you are up-to-date with the latest bug fixes and security updates. There are no 'patch' releases - we don't backport fixes to old releases. Tagged versions just provide a convenient way to apply upgrades in a controlled way or roll back to an older version if necessary and communicate about versions.
- You can stay on the bleeding edge by tracking the `master` branch for source installs or the `next` tag on DockerHub/GHCR.

[shields.io]: https://shields.io
[npm package]: https://www.npmjs.com/package/badge-maker
[production hosting]: https://github.com/badges/shields/blob/master/doc/production-hosting.md
[self hosting]: https://github.com/badges/shields/blob/master/doc/self-hosting.md

---

### Doc Retiring Badges (`doc/retiring-badges.md`)

# Retiring Badges

When a service that Shields integrates with shuts down, those badges will no longer work and need to be retired (deprecated) within Shields.

Retiring a badge involves two steps:

1.  Updating the service code to use the `RetiredService` class
2.  Updating the service tests to reflect the new behavior of the retired service

## Update Service Implementation

Locate the source file(s) for the service, which can be found in `*.service.js` files located within the directory for the service (`./services/:service-name/`) such as `./services/imagelayers/imagelayers.service.js`.

Replace the existing service class implementation with the `RetiredService` class from `./core/base-service/retired-service.js` using the respective `category`, `route`, and `label` values for that service.

Set the badge label to the service name. This ensures users can immediately identify which service is no longer available at a glance.

For example:

```js
import { retiredService } from '../index.js'

export default retiredService({
  category: 'size',
  route: {
    base: 'imagelayers',
    pattern: ':various+',
  },
  label: 'imagelayers',
  dateAdded: new Date('2019-xx-xx'), // Be sure to update this with today's date!
})
```

## Update Service Tests

Locate the test file(s) for the service, which can be found in `*.tester.js` files located in the service directory (`./services/:service-name/`), such as `./services/imagelayers/imagelayers.tester.js`.

With `RetiredService` classes we cannot use `createServiceTester()` so you will need to create the `ServiceTester` class directly. For example:

```js
import { ServiceTester } from '../tester.js'

export const t = new ServiceTester({
  id: 'imagelayers',
  title: 'ImageLayers',
})
```

Next you will need to replace/refactor the existing tests to validate the new retired badge behavior for this service. Retired badges always return a message of `retired badge` (such as `imagelayers | retired badge`) so the tests need to be updated to reflect that message value. For example:

```js
t.create('retired badge (previously image size)')
  .get('/image-size/_/ubuntu/latest.json')
  .expectBadge({
    label: 'imagelayers',
    message: 'retired badge',
  })
```

Make sure to have a live (non-mocked) test for each badge the service provides that validates the each badge returns the `retired badge` message.

Here is an example of what the final result would look like for a test file:

```js
import { ServiceTester } from '../tester.js'

export const t = new ServiceTester({
  id: 'imagelayers',
  title: 'ImageLayers',
})

t.create('retired badge (previously image size)')
  .get('/image-size/_/ubuntu/latest.json')
  .expectBadge({
    label: 'imagelayers',
    message: 'retired badge',
  })

t.create('retired badge (previously number of layers)')
  .get('/layers/_/ubuntu/latest.json')
  .expectBadge({
    label: 'imagelayers',
    message: 'retired badge',
  })
```

## What Happens Next?

Once a service is retired, we'll keep the retirement notice for a minimum of one year. During that time, the badge will render as follows: 

Past that point, all related code will be deleted, and a not found error will be rendered instead: 

Here is a listing of all deleted badges that were once part of the Shields.io service:

- Ansible Collection, Role, Quality
- APM
- Beerpay
- Bintray
- bitHound
- Bountysource
- Cauditor
- Chrome Web Store Price
- CocoaPods Apps
- CocoaPods Downloads
- Codetally
- Coincap
- continuousphp
- Coverity
- Criterion
- David
- dependabot
- Dockbit
- Dotnet Status
- Gemnasium
- Gratipay/Gittip
- Hackage (dependencies)
- ImageLayers
- Issue Stats
- JitPack Downloads
- Leanpub
- LGTM
- Libscore
- Magnum CI
- MicroBadger
- NSP
- pkgreview
- PHP Eye
- Pub (popularity)
- Redmine
- requires.io
- Shippable
- Snap CI
- Snyk
- Tas
- Tokei
- Travis.org
- VersionEye
- Waffle
- Wercker
- Wheelmap

## Additional Information

Some other information that may be useful:

- [Contributing Docs](../CONTRIBUTING.md)
- [Badge Tutorial](./TUTORIAL.md)
- [Service Tests Tutorial](./service-tests.md)
- Previous Pull Requests that retired badges like [#10371](https://github.com/badges/shields/pull/10371) and [#11075](https://github.com/badges/shields/pull/11075)

---

### Doc Self Hosting (`doc/self-hosting.md`)

# Hosting your own Shields server

This document describes how to host your own shields server either from source or using a docker image. See the docs on [releases](https://github.com/badges/shields/blob/master/doc/releases.md#shields-server) for info on how we version the server and how to choose a release.

## Installing from Source

You will need Node 24, which you can install using a [package manager][].

On Ubuntu / Debian:

```sh
curl -sL https://deb.nodesource.com/setup_24.x | sudo -E bash -; sudo apt-get install -y nodejs
```

```sh
git clone https://github.com/badges/shields.git
cd shields
git checkout $(git tag | grep server | tail -n 1)  # checkout the latest tag
npm ci  # You may need sudo for this.
```

[package manager]: https://nodejs.org/en/download/package-manager/

### Build the frontend

```sh
npm run build
```

### Start the server

```sh
sudo node server
```

The server uses port 80 by default, which requires `sudo` permissions.

There are two ways to provide an alternate port:

```sh
PORT=8080 node server
node server 8080
```

The root gets redirected to https://shields.io.

For testing purposes, you can go to `http://localhost/`.

### Deploying to Heroku

Once you have installed the [Heroku CLI][]

```bash
heroku login
heroku create your-app-name
git push heroku master
heroku open
```

[heroku cli]: https://devcenter.heroku.com/articles/heroku-cli

### Deploying to Zeit Vercel

To deploy using Zeit Vercel:

```console
npm run build  # Not sure why, but this needs to be run before deploying.
vercel
```

## Docker

### Public Images

We publish images to:

- DockerHub at https://registry.hub.docker.com/r/shieldsio/shields and
- GitHub Container Registry at https://github.com/badges/shields/pkgs/container/shields

The `next` tag is the latest build from `master`. These are only available for linux/amd64

```sh
# DockerHub
$ docker pull shieldsio/shields:next
$ docker run shieldsio/shields:next
```

```sh
# GHCR
$ docker pull ghcr.io/badges/shields:next
$ docker pull ghcr.io/badges/shields:next
```

Tagged snapshot releases are also available:

- https://registry.hub.docker.com/r/shieldsio/shields/tags
- https://github.com/badges/shields/pkgs/container/shields/versions?filters%5Bversion_type%5D=tagged

We push both linux/amd64 and linux/arm64 snapshot images. We use the linux/amd64 image ourselves to host shields.io. We push a linux/arm64 image, but we don't consume it ourselves and it receives no testing beyond ensuring the docker image builds without error.

### Building Docker Image Locally

Alternatively, you can build and run the server locally using Docker. First build an image:

```console
$ docker build -t shields .
Sending build context to Docker daemon 3.923 MB
…
Successfully built 4471b442c220
```

Optionally, alter the default values for configuration by setting them via [environment variables](https://docs.docker.com/engine/reference/commandline/run/#set-environment-variables--e---env---env-file). See [server-secrets.md](server-secrets.md) and [config/custom-environment-variables.yml](/config/custom-environment-variables.yml) for possible values. In [config/custom-environment-variables.yml](/config/custom-environment-variables.yml), environment variable names are specified as the quoted, uppercase key values (e.g. `GH_TOKEN`).

Then run the container, and be sure to specify the same mapped port as the one Shields is listening on :

```console
$ docker run --rm -p 8080:8080 --env PORT=8080 --name shields shieldsio/shields:next

Configuration:
...
0916211515 Server is starting up: http://0.0.0.0:8080/
```

Assuming Docker is running locally, you should be able to get to the application at http://localhost:8080/.

If you run Docker in a virtual machine (such as boot2docker or Docker Machine) then you will need to replace `localhost` with the IP address of that virtual machine.

### Disabling Dynamic and Endpoint badges

Dynamic and Endpoint badges are enabled by default. These badge families fetch URLs supplied by the requester, which may be unsuitable for an internet-facing self-hosted server that can reach private network resources.

To disable both badge families, set the following public configuration:

```yml
public:
  dynamicAndEndpointBadgesEnabled: false
```

Alternatively, set the environment variable `DYNAMIC_AND_ENDPOINT_BADGES_ENABLED=false`.

When disabled, Dynamic and Endpoint routes are not registered. Requests to those routes receive the standard `404 | badge not found` badge response. Other badge services remain available.

## Raster server

If you want to host PNG badges, you can also self-host a [raster server][] which points to your badge server. It's a docker container. We host it on Fly.io but should be possible to host on a wide variety of platforms.

- In your raster instance, set `BASE_URL` to your Shields instance, e.g. `https://shields.example.co`.
- Optionally, in your Shields, instance, configure `RASTER_URL` to the base URL, e.g. `https://raster.example.co`. This will send 301 redirects for the legacy raster URLs instead of 404's.

If anyone has set this up, more documentation on how to do this would be welcome!

[raster server]: https://github.com/badges/squint

## Server secrets

You can add your own server secrets in environment variables or `config/local.yml`.

These are documented in [server-secrets.md](./server-secrets.md)

## Separate frontend hosting

If you want to host the frontend on a separate server, such as cloud storage or a CDN, you can do that.

First, build the frontend, pointing `BASE_URL` to your server.

```sh
BASE_URL=https://your-server.example.com npm run build
```

Then copy the contents of the `public/` folder to your static hosting / CDN.

There are also a couple settings you should configure on the server.

To help out users, you can make the Shields server redirect the server root. Set the `REDIRECT_URI` environment variable:

```sh
REDIRECT_URI=http://my-custom-shields.s3.amazonaws.com/
```

## Sentry

In order to enable integration with [Sentry](https://sentry.io), you need your own [Sentry DSN](https://docs.sentry.io/quickstart/#configure-the-dsn). It’s an URL in format `https://{PUBLIC_KEY}:{SECRET_KEY}@sentry.io/{PROJECT_ID}`.

### How to obtain the Sentry DSN

1.  [Sign up](https://sentry.io/pricing/) for Sentry
2.  Log in to Sentry
3.  Create a new project for Node.js
4.  You should see [Sentry DSN](https://docs.sentry.io/quickstart/#configure-the-dsn) for your project. Sentry DSN can be found by navigating to \[Project Name] -> Project Settings -> Client Keys (DSN) as well.

Start the server using the Sentry DSN. You can set it:

- by `SENTRY_DSN` environment variable

```
sudo SENTRY_DSN=https://xxx:yyy@sentry.io/zzz node server
```

Or via config as you would do with [server secrets](server-secrets.md):

```yml
private:
  sentry_dsn: ...
```

```sh
sudo node server
```

## Prometheus

Shields uses [prom-client](https://github.com/siimon/prom-client) to provide [default metrics](https://prometheus.io/docs/instrumenting/writing_clientlibs/#standard-and-runtime-collectors). These metrics are disabled by default. You can enable them by `METRICS_PROMETHEUS_ENABLED` and `METRICS_PROMETHEUS_ENDPOINT_ENABLED` environment variables.

```bash
METRICS_PROMETHEUS_ENABLED=true METRICS_PROMETHEUS_ENDPOINT_ENABLED=true npm start
```

Metrics are available at `/metrics` resource.

## Cloudflare

Shields.io uses Cloudflare as a downstream CDN. If your installation does the same, you can configure your server to only accept requests coming from Cloudflare's IPs. Set `public.requireCloudflare: true`.

---

### Doc Server Secrets (`doc/server-secrets.md`)

# Server Secrets

It is possible to provide a token or credentials for a number of external services. These may be used to lift a rate limit or provide access to private resources from a self-hosted instance.

There are two ways of setting secrets:

1. Via environment variables. This is a good way to set them in a PaaS environment.

```sh
DRONE_TOKEN=...
DRONE_ORIGINS="https://drone.example.com"
```

2. Via checked-in `config/local.yml`:

```yml
public:
  services:
    drone:
      authorizedOrigins: ['https://drone.example.com']
private:
  drone_token: '...'
```

For more complex scenarios, configuration files can cascade. See the [node-config documentation][] for details.

[node-config documentation]: https://github.com/lorenwest/node-config/wiki/Configuration-Files

## Authorized origins

Several of the badges provided by Shields allow users to specify the target URL/server of the upstream instance to use via a query parameter in the badge URL (e.g. https://img.shields.io/nexus/s/com.google.guava/guava?server=https%3A%2F%2Foss.sonatype.org). This supports scenarios where your users may need badges from multiple upstream targets, for example if you have more than one Nexus server.

Accordingly, if you configure credentials for one of these services with your self-hosted Shields instance, you must also specifically authorize the hosts to which the credentials are allowed to be sent. If your self-hosted Shields instance then receives a badge request for a target that does not match any of the authorized origins, one of two things will happen:

- if credentials are required for the targeted service, Shields will render an error badge.
- if credentials are optional for the targeted service, Shields will attempt the request, but without sending any credentials.

When setting authorized origins through an environment variable, use a space to separate multiple origins. Note that failing to define authorized origins for a service will default to an empty list, i.e. no authorized origins.

It is highly recommended to use `https` origins with valid SSL, to avoid the possibility of exposing your credentials, for example through DNS-based attacks.

It is also recommended to use tokens for a service account having [the fewest privileges needed][polp] for fetching the relevant status information.

[polp]: https://en.wikipedia.org/wiki/Principle_of_least_privilege

## Services

### Azure DevOps

- `AZURE_DEVOPS_TOKEN` (yml: `private.azure_devops_token`)

An Azure DevOps Token (PAT) is required for accessing [private Azure DevOps projects][ado project visibility].

[Create a PAT][ado personal access tokens] using an account that has access to your target Azure DevOps projects. Your PAT only needs the following [scopes:][ado token scopes]

- `Build (read)`
- `Release (read)`
- `Test Management (read)`

[ado project visibility]: https://docs.microsoft.com/en-us/azure/devops/organizations/public/about-public-projects?view=vsts
[ado personal access tokens]: https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=vsts#create-personal-access-tokens-to-authenticate-access
[ado token scopes]: https://docs.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/oauth?view=vsts#scopes

### Bitbucket (Cloud)

- `BITBUCKET_USER` (yml: `private.bitbucket_username`)
- `BITBUCKET_PASS` (yml: `private.bitbucket_password`)

Bitbucket badges use basic auth. Provide a username and password to give your self-hosted Shields installation access to private repositories hosted on bitbucket.org.

### Bitbucket Server

- `BITBUCKET_SERVER_ORIGINS` (yml: `public.services.bitbucketServer.authorizedOrigins`)
- `BITBUCKET_SERVER_USER` (yml: `private.bitbucket_server_username`)
- `BITBUCKET_SERVER_PASS` (yml: `private.bitbucket_server_password`)

Bitbucket badges use basic auth. Provide a username and password to give your self-hosted Shields installation access to a private Bitbucket Server instance.

### CurseForge

- `CURSEFORGE_API_KEY` (yml: `private.curseforge_api_key`)

A CurseForge API key is required to use the [CurseForge API][cf api]. To obtain an API key, [signup to CurseForge Console][cf signup] with a Google account and create an organization, then go to the [API keys page][cf api key] and copy the generated API key.

[cf api]: https://docs.curseforge.com
[cf signup]: https://console.curseforge.com/#/signup
[cf api key]: https://console.curseforge.com/#/api-keys

### Discord

Using a token for Discord is optional but will allow higher API rates.

- `DISCORD_BOT_TOKEN` (yml: `private.discord_bot_token`)

Register an application in the [Discord developer console](https://discord.com/developers). To obtain a token, simply create a bot for your application.

### DockerHub

Using authentication for DockerHub is optional but can be used to allow higher API rates or access to private repos.

- `DOCKERHUB_USER` (yml: `private.dockerhub_username`)
- `DOCKERHUB_PAT` (yml: `private.dockerhub_pat`)

`DOCKERHUB_PAT` is a Personal Access Token. Generate a token in your [account security settings](https://hub.docker.com/settings/security) with "Read-Only" or "Public Repo Read-Only", depending on your needs.

### Drone

- `DRONE_ORIGINS` (yml: `public.services.drone.authorizedOrigins`)
- `DRONE_TOKEN` (yml: `private.drone_token`)

The self-hosted Drone API [requires authentication][drone auth]. Log in to your Drone instance and obtain a token from the user profile page.

[drone auth]: https://0-8-0.docs.drone.io/api-authentication/

### GitHub

- `GITHUB_URL` (yml: `public.services.github.baseUri`)
- `GH_TOKEN` (yml: `private.gh_token`)

Because of GitHub rate limits, you will need to provide a token, or else badges will stop working once you hit 60 requests per hour, the [unauthenticated rate limit][github rate limit].

You can [create a personal access token][personal access tokens] (PATs) through the GitHub website. When you create the token, you can choose to give read access to your repositories. If you do that, your self-hosted Shields installation will have access to your private repositories.

For most users we recommend using a classic PAT as opposed to a [fine-grained PAT][fine-grained pat]. It is possible to request a fairly large subset of the GitHub badge suite using a fine-grained PAT for authentication but there are also some badges that won't work. This is because some of our badges make use of GitHub's v4 GraphQL API and the GraphQL API only supports authentication with a classic PAT.

When a `gh_token` is specified, it is used in place of the Shields token rotation logic.

`GITHUB_URL` can be used to optionally send all the GitHub requests to a GitHub Enterprise server. This can be done in conjunction with setting a token, though it's not required.

[github rate limit]: https://developer.github.com/v3/#rate-limiting
[personal access tokens]: https://github.com/settings/tokens
[fine-grained pat]: https://github.blog/2022-10-18-introducing-fine-grained-personal-access-tokens-for-github/

- `GH_CLIENT_ID` (yml: `private.gh_client_id`)
- `GH_CLIENT_SECRET` (yml: `private.gh_client_secret`)

These settings are used by shields.io for GitHub OAuth app authorization but will not be necessary for most self-hosted installations. See [production-hosting.md](./production-hosting.md).

### Gitea

- `GITEA_ORIGINS` (yml: `public.services.gitea.authorizedOrigins`)
- `GITEA_TOKEN` (yml: `private.gitea_token`)

A Gitea [Personal Access Token][gitea-pat] is required for accessing private content. If you need a Gitea token for your self-hosted Shields server then we recommend limiting the scopes to the minimal set necessary for the badges you are using.

[gitea-pat]: https://docs.gitea.com/development/api-usage#generating-and-listing-api-tokens

### GitLab

- `GITLAB_ORIGINS` (yml: `public.services.gitlab.authorizedOrigins`)
- `GITLAB_TOKEN` (yml: `private.gitlab_token`)

A GitLab [Personal Access Token][gitlab-pat] is required for accessing private content. If you need a GitLab token for your self-hosted Shields server then we recommend limiting the scopes to the minimal set necessary for the badges you are using.

[gitlab-pat]: https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html

### Jenkins CI

- `JENKINS_ORIGINS` (yml: `public.services.jenkins.authorizedOrigins`)
- `JENKINS_USER` (yml: `private.jenkins_user`)
- `JENKINS_PASS` (yml: `private.jenkins_pass`)

Provide a username and password to give your self-hosted Shields installation access to a private Jenkins CI instance.

### Jira

- `JIRA_ORIGINS` (yml: `public.services.jira.authorizedOrigins`)
- `JIRA_USER` (yml: `private.jira_user`)
- `JIRA_PASS` (yml: `private.jira_pass`)

Provide a username and password to give your self-hosted Shields installation access to a private JIRA instance.

### Libraries.io/Bower

- `LIBRARIESIO_TOKENS` (yml: `private.librariesio_tokens`)

Note that the Bower badges utilize the Libraries.io API, so use this secret for both Libraries.io badges and/or Bower badges.

Just like the `*_ORIGINS` type secrets, this value can accept a single token as a string, or a group of tokens provided as an array of strings. For example:

```yaml
private:
  librariesio_tokens: my-token
## Or
private:
  librariesio_tokens: [my-token some-other-token]
```

When using the environment variable with multiple tokens, be sure to use a space to separate the tokens, e.g. `LIBRARIESIO_TOKENS="my-token some-other-token"`

### Nexus

- `NEXUS_ORIGINS` (yml: `public.services.nexus.authorizedOrigins`)
- `NEXUS_USER` (yml: `private.nexus_user`)
- `NEXUS_PASS` (yml: `private.nexus_pass`)

Provide a username and password to give your self-hosted Shields installation access to your private nexus repositories.

### npm

- `NPM_ORIGINS` (yml: `public.services.npm.authorizedOrigins`)
- `NPM_TOKEN` (yml: `private.npm_token`)

[Generate an npm token][npm token] to give your self-hosted Shields installation access to private npm packages

[npm token]: https://docs.npmjs.com/getting-started/working_with_tokens

### Open Build Service

- `OBS_USER` (yml: `private.obs_user`)
- `OBS_PASS` (yml: `private.obs_user`)

Only authenticated users are allowed to access the Open Build Service API. Authentication is done by sending a Basic HTTP Authorisation header. A user account for the [reference instance](https://build.opensuse.org) is a SUSE IdP account, which can be created [here](https://idp-portal.suse.com/univention/self-service/#page=createaccount).

While OBS supports [API tokens](https://openbuildservice.org/help/manuals/obs-user-guide/cha.obs.authorization.token.html#id-1.5.10.16.4), they can only be scoped to execute specific actions on a POST request. This means however, that an actual account is required to read the build status of a package.

### OpenCollective

- `OPENCOLLECTIVE_TOKEN` (yml: `private.opencollective_token`)

OpenCollective's GraphQL API only allows 10 reqs/minute for anonymous users. An [API token](https://graphql-docs-v2.opencollective.com/access) can be provided to access a higher rate limit of 100 reqs/minute.

### OutageDeck

- `OUTAGEDECK_API_KEY` (yml: `private.outagedeck_api_key`)

An optional Bearer key raises the OutageDeck API rate limit. The hosted Shields.io service can use its complimentary dedicated key; self-hosted instances can leave this unset and use anonymous access, which is limited to 120 requests per IP address per hour.

### Pepy

- `PEPY_KEY` (yml: `private.pepy_key`)

The Pepy API requires authentication. To obtain a key, Create an account, sign in and obtain generate a key on your [account page](https://www.pepy.tech/user).

### PyPI

- `PYPI_URL` (yml: `public.pypi.baseUri`)

`PYPI_URL` can be used to optionally send all the PyPI requests to a Self-hosted Pypi registry, users can also override this by query parameter `pypiBaseUrl`.

### Read the Docs

- `READTHEDOCS_TOKEN` (yml: `private.readthedocs_token`)

Authentication is optional, but a [Read the Docs API token][readthedocs api authentication] increases the API rate limit for Read the Docs badges.

Use a token from a dedicated account that has access only to public projects. Badge endpoints are public, so a token whose account can access private projects could allow badge requests to reveal those projects' build status.

[readthedocs api authentication]: https://docs.readthedocs.com/platform/stable/api/v3.html#authentication-and-authorization

### Reddit

Using a token for Reddit is optional but will allow higher API rates.

- `REDDIT_CLIENT_ID` (yml: `private.reddit_client_id`)
- `REDDIT_CLIENT_SECRET` (yml: `private.reddit_client_secret`)

Register to use the API using [this form](https://support.reddithelp.com/hc/en-us/requests/new?ticket_form_id=14868593862164) and create an app in the [Reddit preferences page](https://www.reddit.com/prefs/apps) in order to obtain a client id and a client secret for making Reddit API calls.

### SymfonyInsight (formerly Sensiolabs)

- `SL_INSIGHT_USER_UUID` (yml: `private.sl_insight_userUuid`)
- `SL_INSIGHT_API_TOKEN` (yml: `private.sl_insight_apiToken`)

The SymfonyInsight API requires authentication. To obtain a token, Create an account, sign in and obtain a uuid and token from your [account page](https://insight.sensiolabs.com/account).

### SonarQube

- `SONAR_ORIGINS` (yml: `public.services.sonar.authorizedOrigins`)
- `SONARQUBE_TOKEN` (yml: `private.sonarqube_token`)

[Generate a token](https://docs.sonarqube.org/latest/user-guide/user-token/) to give your self-hosted Shields installation access to a private SonarQube instance or private project on a public instance.

### StackApps (for StackExchange and StackOverflow)

- `STACKAPPS_API_KEY`: (yml: `private.stackapps_api_key`)

Anonymous requests to the stackexchange API are limited to 300 calls per day. To increase your quota to 10,000 calls per day, create an account at [StackApps](https://stackapps.com/) and [register an OAuth app](https://stackapps.com/apps/oauth/register). Having registered an OAuth app, you'll be granted a key which can be used to increase your request quota. It is not necessary to perform a full OAuth Flow to gain an access token.

### TeamCity

- `TEAMCITY_ORIGINS` (yml: `public.services.teamcity.authorizedOrigins`)
- `TEAMCITY_USER` (yml: `private.teamcity_user`)
- `TEAMCITY_PASS` (yml: `private.teamcity_pass`)

Provide a username and password to give your self-hosted Shields installation access to your private nexus repositories.

### Twitch

- `TWITCH_CLIENT_ID` (yml: `private.twitch_client_id`)
- `TWITCH_CLIENT_SECRET` (yml: `private.twitch_client_secret`)

Register an application in the [Twitch developer console](https://dev.twitch.tv/console) in order to obtain a client id and a client secret for making Twitch API calls.

### Weblate

- `WEBLATE_ORIGINS` (yml: `public.services.weblate.authorizedOrigins`)
- `WEBLATE_API_KEY` (yml: `private.weblate_api_key`)

Weblate heavily throttles [unauthenticated request][weblate authentication], it is recommended to use an API key for requests.

You can find your Weblate API key in your profile under ["API access"][weblate api key location].

[weblate authentication]: https://docs.weblate.org/en/latest/api.html#authentication-and-generic-parameters
[weblate api key location]: https://hosted.weblate.org/accounts/profile/#api

## Error reporting

- `SENTRY_DSN` (yml: `private.sentry_dsn`)

A [Sentry DSN][] may be used to send error reports from your installation to [Sentry.io][]. For more info, see the [self hosting docs][].

[sentry dsn]: https://docs.sentry.io/error-reporting/quickstart/?platform=javascript#configure-the-dsn
[sentry.io]: http://sentry.io/
[self hosting docs]: https://github.com/badges/shields/blob/master/doc/self-hosting.md#sentry

---

### Doc Service Tests (`doc/service-tests.md`)

# Service tests

When creating a badge for a new service or changing a badge's behavior, automated tests should be included. They serve three purposes:

1. The contributor and reviewer can easily verify the code works as intended.

2. When a badge stops working due to an upstream API, maintainers can find out right away.

3. They speed up future contributors when they are debugging or improving a badge.

Tests should cover:

1. Valid behavior
2. Optional parameters like tags or branches
3. Any customized error handling
4. If a non-trivial validator is defined, include tests for malformed responses

## Tutorial

Before getting started, set up a development environment by following the [setup instructions](https://github.com/badges/shields/blob/master/doc/TUTORIAL.md#2-setup)

We will write some tests for [Docs.rs](https://github.com/badges/shields/blob/master/services/docsrs/docsrs.service.js), a service that builds documentation of crates, which are packages in the Rust programming language.

### (1) Boilerplate

The code for our badge is in `services/docsrs/docsrs.service.js`. Tests for this badge should be stored in `services/docsrs/docsrs.tester.js`.

We'll start by adding some boilerplate to our file:

```js
import { createServiceTester } from '../tester.js'

export const t = await createServiceTester()
```

If our `.service.js` module exports a single class, we can `createServiceTester`, which uses convention to create a `ServiceTester` object. Calling this inside `services/docsrs/docsrs.tester.js` will create a `ServiceTester` object configured for the service exported in `services/docsrs/docsrs.service.js`. We will add our tests to this `ServiceTester` object `t`, which is exported from the module.

### (2) Our First Test Case

First we'll add a test for the typical case:

```js
import Joi from 'joi'

t.create('Docs with no version specified')
  .get('/tokio.json')
  .expectBadge({
    label: 'docs',
    message: Joi.equal('passing', 'failing'),
  })
```

1. The `create()` method adds a new test to the tester object. The chained-on calls come from the API testing framework [IcedFrisby][]. Here's a [longer example][] and the complete [API guide][icedfrisby api].
2. We use the `get()` method to request a badge. There are several points to consider here:
   - We need a real crate to test against. In this case we have used [Tokio](https://docs.rs/tokio) but we could have chosen any one.
   - Note that when we call our badge, we are allowing it to communicate with an external service without mocking the response. We write tests which interact with external services, which is unusual practice in unit testing. We do this because one of the purposes of service tests is to notify us if a badge has broken due to an upstream API change. For this reason it is important for at least one test to call the live API without mocking the interaction.
   - All badges on shields can be requested in a number of formats. As well as calling https://img.shields.io/docsrs/tokio.svg to generate  we can also call https://img.shields.io/docsrs/tokio.json to request the same content as JSON. When writing service tests, we request the badge in JSON format so it is easier to make assertions about the content.
   - We don't need to explicitly call `/docsrs/tokio.json` here, only `/tokio.json`. When we create a tester object with `createServiceTester()` the URL base defined in our service class (in this case `/docsrs`) is used as the base URL for any requests made by the tester object.
3. `expectBadge()` is a helper function which accepts either a string literal, a [RegExp][] or a [Joi][] schema for the different fields. Joi is a validation library that is built into IcedFrisby which you can use to match based on a set of allowed strings, regexes, or specific values. You can refer to their [API reference][joi api].
4. We expect `label` to be a string literal `"docs"`.
5. Because this test depends on a live service, we don't want our test to depend on our API call returning a particular build status. Instead we should perform a "picture check" to assert that the badge data conforms to an expected pattern. Our test should not depend on the status of the example crates's documentation build, but should fail if trying to generate the badge throws an error, or if there is a breaking change to the upstream API. In this case, we specify a list with all possible response values, `Joi.equal('passing', 'failing')`. For more complex cases, [services/test-validators.js](https://github.com/badges/shields/blob/master/services/test-validators.js) defines a number of useful validators we can use with regular expressions. Many of the common badge types (version, downloads, rank, etc.) already have validators defined there.

When defining an IcedFrisby test, typically you would invoke the `toss()` method, to register the test. This is not necessary, because the Shields test harness will call it for you.

[icedfrisby]: https://github.com/MarkHerhold/IcedFrisby
[longer example]: https://github.com/MarkHerhold/IcedFrisby/#show-me-some-code
[icedfrisby api]: https://github.com/MarkHerhold/IcedFrisby/blob/master/API.md
[joi]: https://github.com/hapijs/joi
[joi api]: https://github.com/hapijs/joi/blob/master/API.md
[regexp]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp

### (3) Running the Tests

Lets run the test we have written:

```
npm run test:services -- --only=docsrs
```

The `--only=` option indicates which service or services you want to test. You can provide a comma-separated list here.

The `--` tells the NPM CLI to pass the remaining arguments through to the test runner.

Here's the output:

```
Server is starting up: http://localhost:1111/
  DocsRs
    [live] Docs with no version specified
      √
        [ GET /tokio.json ] (441ms)


  1 passing (1s)
```

That's looking good!

Sometimes if we have a failing test, it is useful to be able to see some logging output to help work out why the test is failing. We can do that by calling `npm run test:services:trace`. Try running

```
npm run test:services:trace -- --only=docsrs
```

to run the test with some additional debug output.

### (4) Writing More Tests

We should write tests cases for valid paths through our code. The Docs.rs badge supports an optional version parameter so we'll add a second test for a branch build. In this case, we know for sure that the documentation for this older version was successfully built, we specify a string literal instead of a Joi schema for `message`. This narrows down the expectation and gives us a more helpful error message if the test fails.

```js
t.create('Passing docs for version').get('/tokio/1.37.0.json').expectBadge({
  label: 'docs@1.37.0',
  message: 'passing',
  color: 'brightgreen',
})
```

```
Server is starting up: http://localhost:1111/
  DocsRs
    [live] Docs with no version specified
      √
        [ GET /tokio.json ] (408ms)
    [live] Passing docs for version
      √
        [ GET /tokio/1.37.0.json ] (171ms)


  2 passing (2s)
```

Once we have multiple tests, sometimes it is useful to run only one test. We can do this using the `--fgrep` argument. For example:

```
npm run test:services -- --only="docsrs" --fgrep="Passing docs for version"
```

Documentation for tokio version 1.32.1 failed to build, we can also add a corresponding test:

```js
t.create('Failing docs for version').get('/tokio/1.32.1.json').expectBadge({
  label: 'docs@1.32.1',
  message: 'failing',
  color: 'red',
})
```

Note that in these tests, we have specified a `color` parameter in `expectBadge`. This is helpful in a case like this when we want to test custom color logic, but it is only necessary to explicitly test color values if our badge implements custom logic for setting the badge colors.

Having covered the typical and custom cases, we'll move on to errors. We should include a test for the 'not found' response and also tests for any other custom error handling. When a version is specified, the Docs.rs integration defines a custom error condition for 400 status codes:

```js
httpErrors: version ? { 400: 'malformed version' } : {},
```

First we'll add a test for a crate and a test for a version which will return 404 errors:

```js
t.create('Crate not found')
  .get('/not-a-crate/latest.json')
  .expectBadge({ label: 'docs', message: 'not found' })

t.create('Version not found')
  .get('/tokio/0.8.json')
  .expectBadge({ label: 'docs', message: 'not found' })
```

We also want to include a test for a case where a malformed version was specified. For example:

```js
t.create('Malformed version')
  .get('/tokio/not-a-version.json')
  .expectBadge({ label: 'docs', message: 'malformed version' })
```

## (5) Mocking Responses

If we didn't have a stable example of crate version with a failing documentation build, another approach would be to mock the response. An alternative test for the 'Failing docs for version' case might look like:

```js
t.create('Failing docs for version')
  .get('/tokio/1.32.1.json')
  .intercept(nock =>
    nock('https://docs.rs/crate')
      .get('/tokio/1.32.1/status.json')
      .reply(200, { doc_status: false }),
  )
  .expectBadge({
    label: 'docs@1.32.1',
    message: 'failing',
    color: 'red',
  })
```

This will intercept the request and provide our own mock response. We use the `intercept()` method provided by the [icedfrisby-nock plugin][icedfrisby-nock]. It takes a setup function, which returns an interceptor, and exposes the full API of the HTTP mocking library [Nock][].

Nock is fussy. All parts of a request must match perfectly for the mock to take effect, including the HTTP method (in this case GET), scheme (https), host, and path.

[icedfrisby-nock]: https://github.com/paulmelnikow/icedfrisby-nock#usage
[nock]: https://github.com/nock/nock

## Code coverage

By checking code coverage, we can make sure we've covered all our bases.

We can generate a coverage report and open it:

```
npm run coverage:test:services -- -- --only=docsrs
npm run coverage:report:open
```

## Pull requests

Pull requests must follow the [documented conventions][pr-conventions] in order to execute the correct set of service tests.

[pr-conventions]: https://github.com/badges/shields/blob/master/CONTRIBUTING.md#running-service-tests-in-pull-requests

## Getting help

If you have questions about how to write your tests, please open an issue. If there's already an issue open for the badge you're working on, you can post a comment there instead.

## Further reading

- [IcedFrisby API][]
- [Joi API][]
- [icedfrisby-nock][]
- [Nock API](https://github.com/nock/nock#usage)

---

### Frontend Docs Logos (`frontend/docs/logos.md`)

---
sidebar_position: 2
---

# Logos

## SimpleIcons

We support a wide range of logos via [SimpleIcons](https://simpleicons.org/). All simple-icons are referenced using icon slugs. e.g:

 - https://img.shields.io/npm/v/npm.svg?logo=nodedotjs

You can click the icon title on <a href="https://simpleicons.org/" rel="noopener noreferrer" target="_blank">simple-icons</a> to copy the slug or they can be found in the <a href="https://github.com/simple-icons/simple-icons/blob/master/slugs.md">slugs.md file</a> in the simple-icons repository. NB - the Simple Icons site and slugs.md page may at times contain new icons that haven't yet been pulled into Shields.io yet. More information on how and when we incorporate icon updates can be found [here](https://github.com/badges/shields/discussions/5369).

## Custom Logos

Any custom logo can be passed in a URL parameter by base64 encoding it. e.g:

 - https://img.shields.io/badge/play-station-blue.svg?logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEiIHdpZHRoPSI2MDAiIGhlaWdodD0iNjAwIj48cGF0aCBkPSJNMTI5IDExMWMtNTUgNC05MyA2Ni05MyA3OEwwIDM5OGMtMiA3MCAzNiA5MiA2OSA5MWgxYzc5IDAgODctNTcgMTMwLTEyOGgyMDFjNDMgNzEgNTAgMTI4IDEyOSAxMjhoMWMzMyAxIDcxLTIxIDY5LTkxbC0zNi0yMDljMC0xMi00MC03OC05OC03OGgtMTBjLTYzIDAtOTIgMzUtOTIgNDJIMjM2YzAtNy0yOS00Mi05Mi00MmgtMTV6IiBmaWxsPSIjZmZmIi8+PC9zdmc+

## logoColor parameter

The `logoColor` param can be used to set the color of the SimpleIcons named logo. Hex, rgb, rgba, hsl, hsla and css named colors can all be used.

-  - https://img.shields.io/badge/logo-javascript-blue?logo=javascript
-  - https://img.shields.io/badge/logo-javascript-blue?logo=javascript&logoColor=f5f5f5

---

### Frontend Docs Static Badges (`frontend/docs/static-badges.md`)

# Static Badges

It is possible to use shields.io to make a wide variety of badges displaying static text and/or logos. For example:

-  - https://img.shields.io/badge/any%20text-you%20like-blue
-  - https://img.shields.io/badge/just%20the%20message-8A2BE2
-  - https://img.shields.io/badge/%27for%20the%20badge%27%20style-20B2AA?style=for-the-badge
-  - https://img.shields.io/badge/with%20a%20logo-grey?style=for-the-badge&logo=javascript

For more info, see:

- [Static badge builder](/badges/static-badge), including full documentation of styles and parameters
- [Logos](/docs/logos)

---

--- METRICS ---
- Files Extracted: 21
- Estimated Token Budget: ~30580 tokens
- Canonical Reference: https://codewiki.google/github.com/badges/shields
