Compare commits
82 Commits
2.0.0-beta
...
2.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f9dd95a18 | ||
|
|
dfa1f408d9 | ||
|
|
282520041d | ||
|
|
a7efd88ceb | ||
|
|
3ad742e79a | ||
|
|
73fcc1ccac | ||
|
|
da388b7079 | ||
|
|
d3c454e0dd | ||
|
|
765dfacf72 | ||
|
|
9d77c8afe2 | ||
|
|
84a5be3946 | ||
|
|
00d5f87a77 | ||
|
|
ff9cd20821 | ||
|
|
547380f794 | ||
|
|
065dd570ad | ||
|
|
921adc9939 | ||
|
|
4c2494f20a | ||
|
|
0bf8ebea3a | ||
|
|
6aa45e756b | ||
|
|
3f0ac63ca7 | ||
|
|
51897b3c7e | ||
|
|
fe05d00747 | ||
|
|
5d63effabc | ||
|
|
b68ac16092 | ||
|
|
aed39bfde9 | ||
|
|
83e44b7e7d | ||
|
|
17716953f2 | ||
|
|
81691c5bb3 | ||
|
|
e46a6fbde1 | ||
|
|
5e99aad2de | ||
|
|
510491f376 | ||
|
|
29fedc3448 | ||
|
|
652d2e5611 | ||
|
|
a039fffe79 | ||
|
|
d0fe97d136 | ||
|
|
4b88590619 | ||
|
|
5dd4ee078b | ||
|
|
2bc2450d19 | ||
|
|
3eac8ebbfb | ||
|
|
02ec19fd22 | ||
|
|
c0f9a88719 | ||
|
|
e4e5628207 | ||
|
|
c4af2cac53 | ||
|
|
ec65291a11 | ||
|
|
804c4e4a6f | ||
|
|
291f35d4ad | ||
|
|
611e58e43e | ||
|
|
e240409033 | ||
|
|
57e4d82a90 | ||
|
|
c600bf4800 | ||
|
|
9c658a1345 | ||
|
|
6199b1a878 | ||
|
|
33d5f8f772 | ||
|
|
5019d81642 | ||
|
|
b19d6d0072 | ||
|
|
88402dd7a8 | ||
|
|
c8ce4fabb4 | ||
|
|
3bb392dfae | ||
|
|
e983f7bf3b | ||
|
|
600c369823 | ||
|
|
ba94610840 | ||
|
|
bc4bd79e90 | ||
|
|
1460855d6b | ||
|
|
d55e1c15a6 | ||
|
|
72768d32d9 | ||
|
|
83ac6ff1b3 | ||
|
|
4a298cbcb0 | ||
|
|
8feb75d97d | ||
|
|
cdb4cb36a4 | ||
|
|
52d642cf91 | ||
|
|
a5894c5350 | ||
|
|
f6b6b66fc2 | ||
|
|
275805e90c | ||
|
|
730dc67cdf | ||
|
|
07b282fa1f | ||
|
|
b7ecbd0a0d | ||
|
|
973eeb0ea7 | ||
|
|
07c5f47a73 | ||
|
|
cc843946d0 | ||
|
|
94611fb0e4 | ||
|
|
d22fa6671e | ||
|
|
f9384d769b |
@@ -3,16 +3,15 @@ version: 2
|
||||
jobs:
|
||||
npm-install:
|
||||
docker:
|
||||
- image: shieldsio/shields-ci-node-8:0.0.3
|
||||
working_directory: ~/repo
|
||||
- image: circleci/node:8
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v1-dependencies-{{ checksum "package.json" }}
|
||||
- v2-dependencies-{{ checksum "package.json" }}
|
||||
# fallback to using the latest cache if no exact match is found
|
||||
- v1-dependencies-
|
||||
- v2-dependencies-
|
||||
|
||||
- run:
|
||||
name: Install dependencies
|
||||
@@ -21,20 +20,19 @@ jobs:
|
||||
- save_cache:
|
||||
paths:
|
||||
- node_modules
|
||||
key: v1-dependencies-{{ checksum "package.json" }}
|
||||
key: v2-dependencies-{{ checksum "package.json" }}
|
||||
|
||||
main:
|
||||
docker:
|
||||
- image: shieldsio/shields-ci-node-8:0.0.3
|
||||
- image: circleci/node:8
|
||||
- image: redis
|
||||
working_directory: ~/repo
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- restore_cache:
|
||||
key: v1-dependencies-{{ checksum "package.json" }}
|
||||
key: v2-dependencies-{{ checksum "package.json" }}
|
||||
# https://github.com/badges/shields/issues/1937
|
||||
key: v1-dependencies-
|
||||
key: v2-dependencies-
|
||||
|
||||
- run:
|
||||
name: Install dependencies
|
||||
@@ -55,6 +53,11 @@ jobs:
|
||||
when: always
|
||||
command: npm run test:integration
|
||||
|
||||
- run:
|
||||
name: Tests for gh-badges package
|
||||
when: always
|
||||
command: npm run test:js:package
|
||||
|
||||
- run:
|
||||
name: 'Prettier check (quick fix: `npm run prettier`)'
|
||||
when: always
|
||||
@@ -62,16 +65,15 @@ jobs:
|
||||
|
||||
main@node-latest:
|
||||
docker:
|
||||
- image: shieldsio/shields-ci-node-latest:0.0.3
|
||||
- image: circleci/node:latest
|
||||
- image: redis
|
||||
working_directory: ~/repo
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- restore_cache:
|
||||
key: v1-dependencies-{{ checksum "package.json" }}
|
||||
key: v2-dependencies-{{ checksum "package.json" }}
|
||||
# https://github.com/badges/shields/issues/1937
|
||||
key: v1-dependencies-
|
||||
key: v2-dependencies-
|
||||
|
||||
- run:
|
||||
name: Install dependencies
|
||||
@@ -92,6 +94,11 @@ jobs:
|
||||
when: always
|
||||
command: npm run test:integration
|
||||
|
||||
- run:
|
||||
name: Tests for gh-badges package
|
||||
when: always
|
||||
command: npm run test:js:package
|
||||
|
||||
- run:
|
||||
name: 'Prettier check (quick fix: `npm run prettier`)'
|
||||
when: always
|
||||
@@ -99,15 +106,14 @@ jobs:
|
||||
|
||||
danger:
|
||||
docker:
|
||||
- image: shieldsio/shields-ci-node-8:0.0.3
|
||||
working_directory: ~/repo
|
||||
- image: circleci/node:8
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- restore_cache:
|
||||
key: v1-dependencies-{{ checksum "package.json" }}
|
||||
key: v2-dependencies-{{ checksum "package.json" }}
|
||||
# https://github.com/badges/shields/issues/1937
|
||||
key: v1-dependencies-
|
||||
key: v2-dependencies-
|
||||
|
||||
- run:
|
||||
name: Install dependencies
|
||||
@@ -120,15 +126,14 @@ jobs:
|
||||
|
||||
frontend:
|
||||
docker:
|
||||
- image: shieldsio/shields-ci-node-8:0.0.3
|
||||
working_directory: ~/repo
|
||||
- image: circleci/node:8
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- restore_cache:
|
||||
key: v1-dependencies-{{ checksum "package.json" }}
|
||||
key: v2-dependencies-{{ checksum "package.json" }}
|
||||
# https://github.com/badges/shields/issues/1937
|
||||
key: v1-dependencies-
|
||||
key: v2-dependencies-
|
||||
|
||||
- run:
|
||||
name: Install dependencies
|
||||
@@ -144,10 +149,9 @@ jobs:
|
||||
when: always
|
||||
command: npm run build
|
||||
|
||||
services-pr:
|
||||
services:
|
||||
docker:
|
||||
- image: shieldsio/shields-ci-node-8:0.0.3
|
||||
working_directory: ~/repo
|
||||
- image: circleci/node:8
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
@@ -158,9 +162,9 @@ jobs:
|
||||
echo "{\"gh_token\":\"$GITHUB_TOKEN\"}" > private/secret.json
|
||||
|
||||
- restore_cache:
|
||||
key: v1-dependencies-{{ checksum "package.json" }}
|
||||
key: v2-dependencies-{{ checksum "package.json" }}
|
||||
# https://github.com/badges/shields/issues/1937
|
||||
key: v1-dependencies-
|
||||
key: v2-dependencies-
|
||||
|
||||
- run:
|
||||
name: Install dependencies
|
||||
@@ -184,10 +188,9 @@ jobs:
|
||||
echo 'This is not a pull request. Skipping.'
|
||||
fi
|
||||
|
||||
services-pr@node-latest:
|
||||
services@node-latest:
|
||||
docker:
|
||||
- image: shieldsio/shields-ci-node-latest:0.0.3
|
||||
working_directory: ~/repo
|
||||
- image: circleci/node:latest
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
@@ -198,9 +201,9 @@ jobs:
|
||||
echo "{\"gh_token\":\"$GITHUB_TOKEN\"}" > private/secret.json
|
||||
|
||||
- restore_cache:
|
||||
key: v1-dependencies-{{ checksum "package.json" }}
|
||||
key: v2-dependencies-{{ checksum "package.json" }}
|
||||
# https://github.com/badges/shields/issues/1937
|
||||
key: v1-dependencies-
|
||||
key: v2-dependencies-
|
||||
|
||||
- run:
|
||||
name: Install dependencies
|
||||
@@ -224,20 +227,6 @@ jobs:
|
||||
echo 'This is not a pull request. Skipping.'
|
||||
fi
|
||||
|
||||
services-daily:
|
||||
docker:
|
||||
- image: shieldsio/shields-ci-node-8:0.0.3
|
||||
working_directory: ~/repo
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- restore_cache:
|
||||
key: v1-dependencies-{{ checksum "package.json" }}
|
||||
|
||||
- run:
|
||||
name: Run all service tests
|
||||
command: npm run test:services
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
|
||||
@@ -256,10 +245,10 @@ workflows:
|
||||
- frontend:
|
||||
requires:
|
||||
- npm-install
|
||||
- services-pr:
|
||||
- services:
|
||||
requires:
|
||||
- npm-install
|
||||
- services-pr@node-latest:
|
||||
- services@node-latest:
|
||||
requires:
|
||||
- npm-install
|
||||
- danger:
|
||||
@@ -268,13 +257,3 @@ workflows:
|
||||
filters:
|
||||
branches:
|
||||
ignore: /dependabot\/.*/
|
||||
|
||||
daily:
|
||||
triggers:
|
||||
- schedule:
|
||||
cron: "0 17 * * *"
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
jobs:
|
||||
- services-daily
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
Updating CircleCI Docker images
|
||||
===============================
|
||||
|
||||
Prerequisites
|
||||
-------------
|
||||
|
||||
1. Ask @paulmelnikow to be added to the shieldsio organization on DockerHub.
|
||||
2. Install Docker. I tested [these instructions on OS X][Install Docker on OS X].
|
||||
3. Run `eval $(docker-machine env default)`
|
||||
(In fish: `eval (docker-machine env default)`)
|
||||
|
||||
[Install Docker on OS X]: https://pilsniak.com/how-to-install-docker-on-mac-os-using-brew/
|
||||
|
||||
Updating the images
|
||||
-------------------
|
||||
|
||||
Note: Increment the patch version on the tag in each change. Check
|
||||
[Docker Hub][] to see the current versions.
|
||||
|
||||
```console
|
||||
IMAGE_TAG=<version> npm run circle-images:build
|
||||
docker login
|
||||
IMAGE_TAG=<version> npm run circle-images:push
|
||||
```
|
||||
|
||||
After pushing the images, bump the tag in `.circleci/config.yml`.
|
||||
|
||||
[Docker Hub]: https://hub.docker.com/u/shieldsio/
|
||||
|
||||
Reference
|
||||
---------
|
||||
|
||||
For more details see the [CircleCI custom image docs][].
|
||||
|
||||
[CircleCI custom image docs]: https://circleci.com/docs/2.0/custom-images/
|
||||
@@ -1,4 +0,0 @@
|
||||
FROM node:8
|
||||
ADD .circleci/images/prepare-container.sh /root/prepare-container.sh
|
||||
RUN /root/prepare-container.sh
|
||||
RUN rm /root/prepare-container.sh
|
||||
@@ -1,4 +0,0 @@
|
||||
FROM node:latest
|
||||
ADD .circleci/images/prepare-container.sh /root/prepare-container.sh
|
||||
RUN /root/prepare-container.sh
|
||||
RUN rm /root/prepare-container.sh
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
apt-get -y update
|
||||
apt-get install -y --no-install-recommends fonts-dejavu-core
|
||||
apt-get clean
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
npm install -g greenkeeper-lockfile@1
|
||||
@@ -35,6 +35,9 @@ rules:
|
||||
strict: "error"
|
||||
arrow-body-style: ["error", "as-needed"]
|
||||
no-extension-in-require/main: "error"
|
||||
object-shorthand: ["error", "properties"]
|
||||
prefer-template: "error"
|
||||
promise/prefer-await-to-then: "error"
|
||||
|
||||
# Mocha-related.
|
||||
mocha/no-exclusive-tests: "error"
|
||||
|
||||
1
.gitignore
vendored
@@ -7,6 +7,7 @@
|
||||
/private
|
||||
/index.html
|
||||
/shields.env
|
||||
gh-badges/package-lock.json
|
||||
|
||||
# Folder view configuration files
|
||||
.DS_Store
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
"exclude": [
|
||||
"**/*.spec.js",
|
||||
"**/*.integration.js",
|
||||
"dangerfile.js",
|
||||
"services/**/*.tester.js",
|
||||
"test-fixtures",
|
||||
"scripts",
|
||||
"coverage",
|
||||
"build"
|
||||
],
|
||||
|
||||
@@ -5,3 +5,4 @@ package-lock.json
|
||||
/build
|
||||
/coverage
|
||||
**/*.md
|
||||
private/*.json
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
FROM node:8.9.4-alpine
|
||||
|
||||
RUN apk add --no-cache gettext imagemagick librsvg ttf-dejavu git
|
||||
ENV FONT_PATH /usr/share/fonts/ttf-dejavu/DejaVuSans.ttf
|
||||
RUN apk add --no-cache gettext imagemagick librsvg git
|
||||
|
||||
RUN mkdir -p /usr/src/app
|
||||
RUN mkdir /usr/src/app/private
|
||||
|
||||
87
Makefile
@@ -1,67 +1,76 @@
|
||||
SHELL:=/bin/bash
|
||||
|
||||
DEPLOY_TEMP=${TMPDIR}shields-deploy
|
||||
SERVER_TMP=${TMPDIR}shields-server-deploy
|
||||
FRONTEND_TMP=${TMPDIR}shields-frontend-deploy
|
||||
|
||||
all: website favicon test
|
||||
# This branch is reserved for the deploy process and should not be used for
|
||||
# development. The deploy script will clobber it. To avoid accidentally
|
||||
# pushing secrets to GitHub, this branch is configured to reject pushes.
|
||||
WORKING_BRANCH=server-deploy-working-branch
|
||||
|
||||
favicon:
|
||||
# This isn't working right now. See https://github.com/badges/shields/issues/1788
|
||||
node lib/badge-cli.js '' '' '#bada55' .png > favicon.png
|
||||
all: website test
|
||||
|
||||
website:
|
||||
LONG_CACHE=false npm run build
|
||||
|
||||
# `website` is needed for the server deploys.
|
||||
deploy: website deploy-s0 deploy-s1 deploy-s2 deploy-gh-pages deploy-gh-pages-clean
|
||||
deploy: deploy-s0 deploy-s1 deploy-s2 clean-server-deploy deploy-gh-pages deploy-gh-pages-clean
|
||||
|
||||
deploy-s0:
|
||||
deploy-s0: prepare-server-deploy push-s0
|
||||
deploy-s1: prepare-server-deploy push-s1
|
||||
deploy-s2: prepare-server-deploy push-s2
|
||||
|
||||
prepare-server-deploy: website
|
||||
# Ship a copy of the front end to each server for debugging.
|
||||
# https://github.com/badges/shields/issues/1220
|
||||
git add -f Verdana.ttf private/secret.json build/
|
||||
git commit -m'MUST NOT BE ON GITHUB'
|
||||
git push -f s0 HEAD:master
|
||||
git reset HEAD~1
|
||||
git checkout master
|
||||
rm -rf ${SERVER_TMP}
|
||||
git worktree prune
|
||||
git worktree add -B ${WORKING_BRANCH} ${SERVER_TMP}
|
||||
cp -r build ${SERVER_TMP}
|
||||
git -C ${SERVER_TMP} add -f build/
|
||||
git -C ${SERVER_TMP} commit --no-verify -m '[DEPLOY] Add frontend for debugging'
|
||||
mkdir -p ${SERVER_TMP}/private
|
||||
cp private/secret-production.json ${SERVER_TMP}/private/secret.json
|
||||
git -C ${SERVER_TMP} add -f private/secret.json
|
||||
git -C ${SERVER_TMP} commit --no-verify -m '[DEPLOY] MUST NOT BE ON GITHUB'
|
||||
|
||||
deploy-s1:
|
||||
git add -f Verdana.ttf private/secret.json build/
|
||||
git commit -m'MUST NOT BE ON GITHUB'
|
||||
git push -f s1 HEAD:master
|
||||
git reset HEAD~1
|
||||
git checkout master
|
||||
clean-server-deploy:
|
||||
rm -rf ${SERVER_TMP}
|
||||
git worktree prune
|
||||
|
||||
deploy-s2:
|
||||
git add -f Verdana.ttf private/secret.json build/
|
||||
git commit -m'MUST NOT BE ON GITHUB'
|
||||
git push -f s2 HEAD:master
|
||||
git reset HEAD~1
|
||||
git checkout master
|
||||
push-s0:
|
||||
git push -f s0 ${WORKING_BRANCH}:master
|
||||
|
||||
push-s1:
|
||||
git push -f s1 ${WORKING_BRANCH}:master
|
||||
|
||||
push-s2:
|
||||
git push -f s2 ${WORKING_BRANCH}:master
|
||||
|
||||
deploy-gh-pages:
|
||||
rm -rf ${DEPLOY_TEMP}
|
||||
rm -rf ${FRONTEND_TMP}
|
||||
git worktree prune
|
||||
LONG_CACHE=true \
|
||||
BASE_URL=https://img.shields.io \
|
||||
NEXT_ASSET_PREFIX=https://shields.io \
|
||||
npm run build
|
||||
git worktree add -B gh-pages ${DEPLOY_TEMP}
|
||||
git -C ${DEPLOY_TEMP} ls-files | xargs git -C ${DEPLOY_TEMP} rm
|
||||
git -C ${DEPLOY_TEMP} commit -m '[DEPLOY] Completely clean the index'
|
||||
cp -r build/* ${DEPLOY_TEMP}
|
||||
cp favicon.png ${DEPLOY_TEMP}
|
||||
echo shields.io > ${DEPLOY_TEMP}/CNAME
|
||||
touch ${DEPLOY_TEMP}/.nojekyll
|
||||
git -C ${DEPLOY_TEMP} add .
|
||||
git -C ${DEPLOY_TEMP} commit -m '[DEPLOY] Add built site'
|
||||
git worktree add -B gh-pages ${FRONTEND_TMP}
|
||||
git -C ${FRONTEND_TMP} ls-files | xargs git -C ${FRONTEND_TMP} rm
|
||||
git -C ${FRONTEND_TMP} commit --no-verify -m '[DEPLOY] Completely clean the index'
|
||||
cp -r build/* ${FRONTEND_TMP}
|
||||
cp favicon.png ${FRONTEND_TMP}
|
||||
echo shields.io > ${FRONTEND_TMP}/CNAME
|
||||
touch ${FRONTEND_TMP}/.nojekyll
|
||||
git -C ${FRONTEND_TMP} add .
|
||||
git -C ${FRONTEND_TMP} commit --no-verify -m '[DEPLOY] Add built site'
|
||||
git push -f origin gh-pages
|
||||
|
||||
deploy-gh-pages-clean:
|
||||
rm -rf $DEPLOY_TEMP
|
||||
rm -rf ${FRONTEND_TMP}
|
||||
git worktree prune
|
||||
|
||||
deploy-heroku:
|
||||
git add -f Verdana.ttf private/secret.json build/
|
||||
git commit -m'MUST NOT BE ON GITHUB'
|
||||
git add -f private/secret.json build/
|
||||
git commit --no-verify -m'MUST NOT BE ON GITHUB'
|
||||
git push -f heroku HEAD:master
|
||||
git reset HEAD~1
|
||||
(git checkout -B gh-pages && \
|
||||
@@ -72,4 +81,4 @@ deploy-heroku:
|
||||
test:
|
||||
npm test
|
||||
|
||||
.PHONY: all favicon website deploy deploy-s0 deploy-s1 deploy-s2 deploy-gh-pages deploy-heroku setup redis test
|
||||
.PHONY: all website deploy prepare-server-deploy clean-server-deploy deploy-s0 deploy-s1 deploy-s2 push-s0 push-s1 push-s2 deploy-gh-pages deploy-gh-pages-clean deploy-heroku setup redis test
|
||||
|
||||
76
README.md
@@ -10,6 +10,12 @@
|
||||
<a href="https://circleci.com/gh/badges/shields/tree/master">
|
||||
<img src="https://img.shields.io/circleci/project/github/badges/shields/master.svg"
|
||||
alt="build status"></a>
|
||||
<a href="https://circleci.com/gh/badges/daily-tests">
|
||||
<img src="https://img.shields.io/circleci/project/github/badges/daily-tests.svg?label=daily%20tests"
|
||||
alt="daily build status"></a>
|
||||
<a href="https://coveralls.io/github/badges/shields">
|
||||
<img src="https://img.shields.io/coveralls/github/badges/shields.svg"
|
||||
alt="coverage"></a>
|
||||
<a href="https://github.com/badges/shields/compare/gh-pages...master">
|
||||
<img src="https://img.shields.io/github/commits-since/badges/shields/gh-pages.svg?label=commits%20to%20be%20deployed"
|
||||
alt="commits to be deployed"></a>
|
||||
@@ -31,25 +37,35 @@ continuous integration services, package registries, distributions, app
|
||||
stores, social networks, code coverage services, and code analysis services.
|
||||
Every month it serves over 470 million images.
|
||||
|
||||
In addition to hosting the shields.io frontend and server code, this monorepo
|
||||
hosts an [NPM library for generating badges][gh-badges], and the badge design
|
||||
specification.
|
||||
This repo hosts:
|
||||
|
||||
* The [Shields.io][shields.io] frontend and server code
|
||||
* An [NPM library for generating badges][gh-badges]
|
||||
* [documentation][gh-badges-docs]
|
||||
* [changelog][gh-badges-changelog]
|
||||
* The [badge design specification][badge-spec]
|
||||
|
||||
|
||||
[shields.io]: https://shields.io/
|
||||
[gh-badges]: https://www.npmjs.com/package/gh-badges
|
||||
[badge-spec]: https://github.com/badges/shields/tree/master/spec
|
||||
[gh-badges-docs]: https://github.com/badges/shields/tree/master/gh-badges/README.md
|
||||
[gh-badges-changelog]: https://github.com/badges/shields/tree/master/gh-badges/CHANGELOG.md
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
* build status: `build | failing`
|
||||
* code coverage percentage: `coverage | 80%`
|
||||
* stable release version: `version | 1.2.3`
|
||||
* package manager release: `gem | 1.2.3`
|
||||
* status of third-party dependencies: `dependencies | out-of-date`
|
||||
* static code analysis GPA: `code climate | 3.8`
|
||||
* [SemVer](https://semver.org/) version observance: `semver | 2.0.0`
|
||||
* amount of [Gratipay](http://gratipay.com) donations per week: `tips | $2/week`
|
||||
* 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](https://uptimerobot.com) percentage: 
|
||||
|
||||
[Make your own badges!][custom badges]
|
||||
(Quick example: `https://img.shields.io/badge/left-right-f39f37.svg`)
|
||||
@@ -79,35 +95,6 @@ You can read a [tutorial on how to add a badge][tutorial].
|
||||
[contributing]: CONTRIBUTING.md
|
||||
|
||||
|
||||
Using the badge library
|
||||
-----------------------
|
||||
|
||||
```sh
|
||||
npm install -g gh-badges
|
||||
badge build passed :green .png > mybadge.png
|
||||
```
|
||||
|
||||
```js
|
||||
const { BadgeFactory } = require('gh-badges')
|
||||
|
||||
const bf = new BadgeFactory({ fontPath: '/path/to/Verdana.ttf' })
|
||||
|
||||
const format = {
|
||||
text: ['build', 'passed'],
|
||||
colorscheme: 'green',
|
||||
template: 'flat',
|
||||
}
|
||||
|
||||
const svg = bf.create(format)
|
||||
```
|
||||
|
||||
View the [documentation for gh-badges][gh-badges doc].
|
||||
|
||||
[](https://npmjs.org/package/gh-badges)
|
||||
|
||||
[gh-badges doc]: https://github.com/badges/shields/blob/master/doc/gh-badges.md
|
||||
|
||||
|
||||
Development
|
||||
-----------
|
||||
|
||||
@@ -130,12 +117,17 @@ SVG or JSON output. When deliberately changing the output, run
|
||||
`SNAPSHOT_DRY=1 npm run test:js:server` to preview changes to the saved
|
||||
snapshots, and `SNAPSHOT_UPDATE=1 npm run test:js:server` to update them.
|
||||
|
||||
The server can be [configured][sentry configuration] to use [Sentry][sentry].
|
||||
The server can be configured to use [Sentry][] ([configuration][sentry configuration]) and [Prometheus][] ([configuration][prometheus configuration]).
|
||||
|
||||
Daily tests, including a full run of the service tests and overall code coverage, are run via [badges/daily-tests][daily-tests].
|
||||
|
||||
[package manager]: https://nodejs.org/en/download/package-manager/
|
||||
[snapshot tests]: https://glebbahmutov.com/blog/snapshot-testing/
|
||||
[sentry configuration]: doc/self-hosting.md#sentry
|
||||
[Prometheus]: https://prometheus.io/
|
||||
[prometheus configuration]: doc/self-hosting.md#prometheus
|
||||
[Sentry]: https://sentry.io/
|
||||
[sentry configuration]: doc/self-hosting.md#sentry
|
||||
[daily-tests]: https://github.com/badges/daily-tests
|
||||
|
||||
Hosting your own server
|
||||
-----------------------
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
exports['The badge generator SVG should always produce the same SVG (unless we have changed something!) 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="88" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="a"><rect width="88" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#a)"><path fill="#555" d="M0 0h45v20H0z"/><path fill="#4c1" d="M45 0h43v20H45z"/><path fill="url(#b)" d="M0 0h88v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110"> <text x="235" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">cactus</text><text x="235" y="140" transform="scale(.1)" textLength="350">cactus</text><text x="655" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="330">grown</text><text x="655" y="140" transform="scale(.1)" textLength="330">grown</text></g> </svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="90" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="a"><rect width="90" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#a)"><path fill="#555" d="M0 0h45v20H0z"/><path fill="#4c1" d="M45 0h45v20H45z"/><path fill="url(#b)" d="M0 0h90v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110"> <text x="235" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">cactus</text><text x="235" y="140" transform="scale(.1)" textLength="350">cactus</text><text x="665" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="665" y="140" transform="scale(.1)" textLength="350">grown</text></g> </svg>
|
||||
`
|
||||
|
||||
exports['The badge generator JSON should always produce the same JSON (unless we have changed something!) 1'] = `
|
||||
|
||||
@@ -116,6 +116,7 @@ if (capitals.created || underscores.created) {
|
||||
const allFiles = danger.git.created_files.concat(danger.git.modified_files)
|
||||
|
||||
allFiles.forEach(file => {
|
||||
// eslint-disable-next-line promise/prefer-await-to-then
|
||||
danger.git.diffForFile(file).then(diff => {
|
||||
if (/\+.*assert[(.]/.test(diff.diff)) {
|
||||
warn(
|
||||
|
||||
@@ -99,11 +99,10 @@ const BaseService = require('../base') // (2)
|
||||
|
||||
module.exports = class Example extends BaseService { // (3)
|
||||
|
||||
static get url() { // (4)
|
||||
static get route() { // (4)
|
||||
return {
|
||||
base: 'example',
|
||||
format: '([^/]+)',
|
||||
capture: ['text'],
|
||||
pattern: ':text',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,14 +121,14 @@ Description of the code:
|
||||
1. We declare strict mode at the start of each file. This prevents certain classes of error such as undeclared variables.
|
||||
2. Our service badge class will extend `BaseService` so we need to require it. We declare variables with `const` and `let` in preference to `var`.
|
||||
3. Our module must export a class which extends `BaseService`
|
||||
4. `url()` declares a route. We declare getters as `static`.
|
||||
4. `route()` declares a route. We declare getters as `static`.
|
||||
* `base` defines the static part of the route.
|
||||
* `format` is a [regular expression](https://www.w3schools.com/jsref/jsref_obj_regexp.asp) defining the variable part of the route.
|
||||
* We can use `capture` to extract matched regex clauses into one or more named variables. Here we are declaring that we want to store the string matched by `([^/]+)` in a variable called `text`.
|
||||
This declaration adds the route `/^\/test\/([^\/]+)\.(svg|png|gif|jpg|json)$/` to our application.
|
||||
5. All badges must implement the `async handle()` function. This is called to invoke our code. Note that the signature of `handle()` will match the capturing group defined in `url()` Because we're capturing a single variable called `text` our function signature is `async handle({ text })`. Although in this simple case, we aren't performing any asynchronous calls, `handle()` would usually spend some time blocked on I/O. We use the `async`/`await` pattern for asynchronous code. Our `handle()` function returns an object with 3 properties:
|
||||
* `pattern` defines the variable part of the route. It can include any
|
||||
number of named parameters. These are converted into
|
||||
regular expressions by [`path-to-regexp`][path-to-regexp].
|
||||
5. All badges must implement the `async handle()` function. This is called to invoke our code. Note that the signature of `handle()` will match the capturing group defined in `route()` Because we're capturing a single variable called `text` our function signature is `async handle({ text })`. Although in this simple case, we aren't performing any asynchronous calls, `handle()` would usually spend some time blocked on I/O. We use the `async`/`await` pattern for asynchronous code. Our `handle()` function returns 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 URL regex
|
||||
* `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.
|
||||
@@ -143,6 +142,8 @@ To try out this example badge:
|
||||
4. Visit the badge at <http://[::]:8080/example/foo.svg>.
|
||||
It should look like this: 
|
||||
|
||||
[path-to-regexp]: https://github.com/pillarjs/path-to-regexp#parameters
|
||||
|
||||
### (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.
|
||||
@@ -156,17 +157,16 @@ const BaseJsonService = require('../base-json') // (2)
|
||||
const { renderVersionBadge } = require('../../lib/version') // (3)
|
||||
|
||||
const Joi = require('joi') // (4)
|
||||
const versionSchema = Joi.object({ // (4)
|
||||
const schema = Joi.object({ // (4)
|
||||
version: Joi.string().required(), // (4)
|
||||
}).required() // (4)
|
||||
|
||||
module.exports = class GemVersion extends BaseJsonService { // (5)
|
||||
|
||||
static get url() { // (6)
|
||||
static get route() { // (6)
|
||||
return {
|
||||
base: 'gem/v',
|
||||
format: '(.+)',
|
||||
capture: ['gem'],
|
||||
pattern: ':gem',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,16 +174,15 @@ module.exports = class GemVersion extends BaseJsonService { // (5)
|
||||
return { label: 'gem' }
|
||||
}
|
||||
|
||||
async handle({ gem }) { // (8)
|
||||
async handle({ gem }) { // (8)
|
||||
const { version } = await this.fetch({ gem })
|
||||
return this.constructor.render({ version })
|
||||
}
|
||||
|
||||
async fetch({ gem }) { // (9)
|
||||
const url = `https://rubygems.org/api/v1/gems/${gem}.json`
|
||||
async fetch({ gem }) { // (9)
|
||||
return this._requestJson({
|
||||
url,
|
||||
schema: versionSchema,
|
||||
schema,
|
||||
url: `https://rubygems.org/api/v1/gems/${gem}.json`,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -202,11 +201,11 @@ Description of the code:
|
||||
* [licenses.js](https://github.com/badges/shields/blob/master/lib/licenses.js)
|
||||
* [text-formatters.js](https://github.com/badges/shields/blob/master/lib/text-formatters.js)
|
||||
* [version.js](https://github.com/badges/shields/blob/master/lib/version.js)
|
||||
4. 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 reveive an object which must have a property called 'status', which is a string.
|
||||
4. 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 recieve an object which must have a property called 'status', which is a string.
|
||||
5. Our module exports a class which extends `BaseJsonService`
|
||||
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. Our bage must implement the `async handle()` function. Because our URL pattern captures a variable called `gem`, our function signature is `async handle({ gem })`. We usually seperate 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.
|
||||
8. Our bage must implement the `async handle()` function. 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. 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 4 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 [request](https://github.com/request/request) to perform the HTTP request. Options can be passed to request, 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 `request` docs for [supported options](https://github.com/request/request#requestoptions-callback).
|
||||
@@ -256,9 +255,8 @@ module.exports = class GemVersion extends BaseJsonService {
|
||||
return [
|
||||
{ // (3)
|
||||
title: 'Gem',
|
||||
urlPattern: ':package',
|
||||
namedParams: { gem: 'formatador' },
|
||||
staticExample: this.render({ version: '2.1.0' }),
|
||||
exampleUrl: 'formatador',
|
||||
keywords: ['ruby'],
|
||||
},
|
||||
]
|
||||
@@ -271,9 +269,9 @@ module.exports = class GemVersion extends BaseJsonService {
|
||||
2. The examples property defines an array of examples. In this case the array will contain a single object, but in some cases it is helpful to provide multiple usage examples.
|
||||
3. Our example object should contain the following properties:
|
||||
* `title`: Descriptive text that will be shown next to the badge
|
||||
* `urlPattern`: Describe the variable part of the URL using `:param` syntax.
|
||||
* `namedParams`: Provide a valid example of params we can substitute into
|
||||
the pattern. In this case we need a valid ruby gem, so we've picked [formatador](https://rubygems.org/gems/formatador).
|
||||
* `staticExample`: On the index page we want to show an example badge, but for performance reasons we want that example to be generated without making an API call. `staticExample` should be populated by calling our `render()` method with some valid data.
|
||||
* `exampleUrl`: Provide a valid example of params we can call the badge with. In this case we need a valid ruby gem, so we've picked [formatador](https://rubygems.org/gems/formatador)
|
||||
* `keywords`: If we want to provide additional keywords other than the title, we can add them here. This helps users to search for relevant badges.
|
||||
|
||||
Save, run `npm start`, and you can see it [locally](http://127.0.0.1:3000/).
|
||||
|
||||
@@ -60,7 +60,6 @@ Once you have installed the [Heroku Toolbelt][]:
|
||||
heroku login
|
||||
heroku create your-app-name
|
||||
heroku config:set BUILDPACK_URL=https://github.com/mojodna/heroku-buildpack-multi.git#build-env
|
||||
cp /path/to/Verdana.ttf .
|
||||
make deploy
|
||||
heroku open
|
||||
```
|
||||
@@ -205,3 +204,11 @@ sudo SENTRY_DSN=https://xxx:yyy@sentry.io/zzz node server
|
||||
```
|
||||
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` environment variable. Moreover access to metrics resource is blocked for requests from any IP address by default. You can provide a regular expression with allowed IP addresses by `METRICS_PROMETHEUS_ALLOWED_IPS` environment variable.
|
||||
```bash
|
||||
METRICS_PROMETHEUS_ENABLED=true METRICS_PROMETHEUS_ALLOWED_IPS="^127\.0\.0\.1$" npm start
|
||||
```
|
||||
Metrics are available at `/metrics` resource.
|
||||
|
||||
@@ -163,7 +163,7 @@ Once we have multiple tests, sometimes it is useful to run only one test. We can
|
||||
npm run test:services -- --only="wercker" --fgrep="Build status (with branch)"
|
||||
```
|
||||
|
||||
Having covered the typical and custom cases, we'll move on to errors. We should include tests for any cusom error handling. The Wercker integration defines a couple of custom error conditions:
|
||||
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 cusom error handling. The Wercker integration defines a custom error condition for 401 as well as a custom 404 message:
|
||||
|
||||
```js
|
||||
errorMessages: {
|
||||
|
||||
@@ -83,7 +83,7 @@ const Category = ({ category, examples, baseUrl, longCache, onClick }) => {
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Link to={'/examples/' + category.id}>
|
||||
<Link to={`/examples/${category.id}`}>
|
||||
<h3 id={category.id}>{category.name}</h3>
|
||||
</Link>
|
||||
<table className="badge">
|
||||
|
||||
@@ -43,7 +43,7 @@ export default class ExamplesPage extends React.Component {
|
||||
this.searchTimeout = window.setTimeout(() => {
|
||||
this.setState({
|
||||
searchReady: true,
|
||||
query: query,
|
||||
query,
|
||||
})
|
||||
}, 500)
|
||||
}
|
||||
|
||||
@@ -6,14 +6,6 @@ const Footer = ({ baseUrl }) => (
|
||||
<section>
|
||||
<h2 id="like-this">Like This?</h2>
|
||||
|
||||
<p>
|
||||
What is your favorite badge service to use?
|
||||
<br />
|
||||
<a href="https://github.com/badges/shields/blob/master/CONTRIBUTING.md">
|
||||
Tell us
|
||||
</a>{' '}
|
||||
and we might bring it to you!
|
||||
</p>
|
||||
<p>
|
||||
<object
|
||||
data={resolveUrl(
|
||||
@@ -47,13 +39,19 @@ const Footer = ({ baseUrl }) => (
|
||||
alt="chat on Discord"
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://github.com/h5bp/lazyweb-requests/issues/150">This</a> is
|
||||
where the current server got started.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<small>:wq</small>
|
||||
What is your favorite badge service to use?
|
||||
<br />
|
||||
<a href="https://github.com/badges/shields/blob/master/CONTRIBUTING.md">
|
||||
Tell us
|
||||
</a>{' '}
|
||||
and we might bring it to you!
|
||||
</p>
|
||||
|
||||
<p className="spaced-row">
|
||||
<a href="https://status.shields.io/">Status</a>
|
||||
<a href="https://github.com/badges/shields/">GitHub</a>
|
||||
</p>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -33,7 +33,7 @@ export default class SearchResults extends React.Component {
|
||||
|
||||
renderCategoryHeadings() {
|
||||
return this.preparedExamples.map((category, i) => (
|
||||
<Link to={'/examples/' + category.category.id} key={category.category.id}>
|
||||
<Link to={`/examples/${category.category.id}`} key={category.category.id}>
|
||||
<h3 id={category.category.id}>{category.category.name}</h3>
|
||||
</Link>
|
||||
))
|
||||
|
||||
@@ -49,7 +49,9 @@ export default class SuggestionAndSearch extends React.Component {
|
||||
let suggestions
|
||||
try {
|
||||
const json = await res.json()
|
||||
suggestions = json.badges
|
||||
// This doesn't validate the response. The default value here prevents
|
||||
// a crash if the server returns {"err":"Disallowed"}.
|
||||
suggestions = json.badges || []
|
||||
} catch (e) {
|
||||
suggestions = []
|
||||
}
|
||||
|
||||
@@ -181,8 +181,8 @@ export default class Usage extends React.PureComponent {
|
||||
<h2 id="styles">Styles</h2>
|
||||
|
||||
<p>
|
||||
The following styles are available (flat is the default as of Feb 1st
|
||||
2015). Examples are shown with an optional logo:
|
||||
The following styles are available. Flat is the default. Examples are
|
||||
shown with an optional logo:
|
||||
</p>
|
||||
{this.renderStyleExamples()}
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import resolveUrl from './resolve-url'
|
||||
import { staticBadgeUrl as makeStaticBadgeUrl } from '../../lib/make-badge-url'
|
||||
|
||||
export default function resolveBadgeUrl(url, baseUrl, options) {
|
||||
const { longCache, style, queryParams: inQueryParams } = options || {}
|
||||
export default function resolveBadgeUrl(
|
||||
url,
|
||||
baseUrl,
|
||||
{ longCache, style, queryParams: inQueryParams } = {}
|
||||
) {
|
||||
const outQueryParams = Object.assign({}, inQueryParams)
|
||||
if (longCache) {
|
||||
outQueryParams.maxAge = '2592000'
|
||||
@@ -12,13 +16,9 @@ export default function resolveBadgeUrl(url, baseUrl, options) {
|
||||
return resolveUrl(url, baseUrl, outQueryParams)
|
||||
}
|
||||
|
||||
export function encodeField(s) {
|
||||
return encodeURIComponent(s.replace(/-/g, '--').replace(/_/g, '__'))
|
||||
}
|
||||
|
||||
export function staticBadgeUrl(baseUrl, subject, status, color, options) {
|
||||
const path = [subject, status, color].map(encodeField).join('-')
|
||||
return resolveUrl(`/badge/${path}.svg`, baseUrl, options)
|
||||
export function staticBadgeUrl(baseUrl, label, message, color, options) {
|
||||
const path = makeStaticBadgeUrl({ label, message, color })
|
||||
return resolveUrl(path, baseUrl, options)
|
||||
}
|
||||
|
||||
// Options can include: { prefix, suffix, color, longCache, style, queryParams }
|
||||
@@ -28,10 +28,8 @@ export function dynamicBadgeUrl(
|
||||
label,
|
||||
dataUrl,
|
||||
query,
|
||||
options = {}
|
||||
{ prefix, suffix, color, queryParams = {}, ...rest } = {}
|
||||
) {
|
||||
const { prefix, suffix, color, queryParams = {}, ...rest } = options
|
||||
|
||||
Object.assign(queryParams, {
|
||||
label,
|
||||
url: dataUrl,
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { test, given } from 'sazerac'
|
||||
import resolveBadgeUrl, {
|
||||
encodeField,
|
||||
staticBadgeUrl,
|
||||
dynamicBadgeUrl,
|
||||
} from './badge-url'
|
||||
import resolveBadgeUrl, { staticBadgeUrl, dynamicBadgeUrl } from './badge-url'
|
||||
|
||||
const resolveBadgeUrlWithLongCache = (url, baseUrl) =>
|
||||
resolveBadgeUrl(url, baseUrl, { longCache: true })
|
||||
@@ -27,14 +23,6 @@ describe('Badge URL functions', function() {
|
||||
)
|
||||
})
|
||||
|
||||
test(encodeField, () => {
|
||||
given('foo').expect('foo')
|
||||
given('').expect('')
|
||||
given('happy go lucky').expect('happy%20go%20lucky')
|
||||
given('do-right').expect('do--right')
|
||||
given('it_is_a_snake').expect('it__is__a__snake')
|
||||
})
|
||||
|
||||
test(staticBadgeUrl, () => {
|
||||
given('http://img.example.com', 'foo', 'bar', 'blue', {
|
||||
style: 'plastic',
|
||||
|
||||
2
gh-badges/.npmignore
Normal file
@@ -0,0 +1,2 @@
|
||||
lib/make-badge-test-helpers.js
|
||||
lib/**/*.spec.js
|
||||
140
gh-badges/CHANGELOG.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# Changelog
|
||||
|
||||
## 2.1.0
|
||||
|
||||
gh-badges v2.1.0 implements a new text width measurer which uses a lookup table, removing the dependency
|
||||
on PDFKit. It is no longer necessary to provide a local copy of Verdana for accurate text width computation.
|
||||
|
||||
As such, the `fontPath` and `precomputeWidths` parameters are now deprecated. The recommended call to create an instance of `BadgeFactory` is now
|
||||
|
||||
```js
|
||||
const bf = new BadgeFactory()
|
||||
```
|
||||
|
||||
For backwards compatibility you can still construct an instance of `BadgeFactory` with a call like
|
||||
|
||||
```js
|
||||
const bf = new BadgeFactory({ fontPath: '/path/to/Verdana.ttf', precomputeWidths: true })
|
||||
```
|
||||
|
||||
However, the function will issue a warning.
|
||||
|
||||
To clear the warning, change the code to:
|
||||
|
||||
```js
|
||||
const bf = new BadgeFactory()
|
||||
```
|
||||
|
||||
These arguments will be removed in a future release.
|
||||
|
||||
To upgrade from v1.3.0, change your code from:
|
||||
|
||||
```js
|
||||
const badge = require('gh-badges')
|
||||
|
||||
const format = {
|
||||
text: ['build', 'passed'],
|
||||
colorscheme: 'green',
|
||||
template: 'flat',
|
||||
}
|
||||
|
||||
badge.loadFont('/path/to/Verdana.ttf', err => {
|
||||
badge(format, (svg, err) => {
|
||||
// svg is a string containing your badge
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```js
|
||||
const { BadgeFactory } = require('gh-badges')
|
||||
|
||||
const bf = new BadgeFactory()
|
||||
|
||||
const format = {
|
||||
text: ['build', 'passed'],
|
||||
colorscheme: 'green',
|
||||
template: 'flat',
|
||||
}
|
||||
|
||||
const svg = bf.create(format)
|
||||
```
|
||||
|
||||
### Other changes in this release:
|
||||
|
||||
* Remove unnecessary dependencies
|
||||
* Documentation improvements
|
||||
|
||||
## 2.0.0 - 2018-11-09
|
||||
|
||||
gh-badges v2.0.0 declares a new public interface which is synchronous.
|
||||
If your version 1.3.0 code looked like this:
|
||||
|
||||
```js
|
||||
const badge = require('gh-badges')
|
||||
|
||||
const format = {
|
||||
text: ['build', 'passed'],
|
||||
colorscheme: 'green',
|
||||
template: 'flat',
|
||||
}
|
||||
|
||||
badge.loadFont('/path/to/Verdana.ttf', err => {
|
||||
badge(format, (svg, err) => {
|
||||
// svg is a string containing your badge
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
To upgrade to version 2.0.0, refactor you code to:
|
||||
|
||||
```js
|
||||
const { BadgeFactory } = require('gh-badges')
|
||||
|
||||
const bf = new BadgeFactory({ fontPath: '/path/to/Verdana.ttf' })
|
||||
|
||||
const format = {
|
||||
text: ['build', 'passed'],
|
||||
colorscheme: 'green',
|
||||
template: 'flat',
|
||||
}
|
||||
|
||||
const svg = bf.create(format)
|
||||
```
|
||||
|
||||
You can generate badges without a copy of Verdana, however font width computation is approximate and badges may be distorted.
|
||||
|
||||
```js
|
||||
const bf = new BadgeFactory({ fallbackFontPath: 'Helvetica' })
|
||||
```
|
||||
|
||||
|
||||
## 1.3.0 - 2016-09-07
|
||||
|
||||
Add support for optionally specifying the path to `Verdana.ttf`. In earlier versions, the file needed to be in the directory containing Shields.
|
||||
|
||||
Without font path:
|
||||
|
||||
```js
|
||||
const badge = require('gh-badges')
|
||||
|
||||
badge({ text: [ 'build', 'passed' ], colorscheme: 'green' },
|
||||
(svg, err) => {
|
||||
// svg is a string containing your badge
|
||||
})
|
||||
```
|
||||
|
||||
With font path:
|
||||
|
||||
```js
|
||||
const badge = require('gh-badges')
|
||||
|
||||
// Optional step, to have accurate text width computation.
|
||||
badge.loadFont('/path/to/Verdana.ttf', err => {
|
||||
badge({ text: ['build', 'passed'], colorscheme: 'green', template: 'flat' },
|
||||
(svg, err) => {
|
||||
// svg is a string containing your badge
|
||||
})
|
||||
})
|
||||
```
|
||||
116
gh-badges/LICENSE
Normal file
@@ -0,0 +1,116 @@
|
||||
CC0 1.0 Universal
|
||||
|
||||
Statement of Purpose
|
||||
|
||||
The laws of most jurisdictions throughout the world automatically confer
|
||||
exclusive Copyright and Related Rights (defined below) upon the creator and
|
||||
subsequent owner(s) (each and all, an "owner") of an original work of
|
||||
authorship and/or a database (each, a "Work").
|
||||
|
||||
Certain owners wish to permanently relinquish those rights to a Work for the
|
||||
purpose of contributing to a commons of creative, cultural and scientific
|
||||
works ("Commons") that the public can reliably and without fear of later
|
||||
claims of infringement build upon, modify, incorporate in other works, reuse
|
||||
and redistribute as freely as possible in any form whatsoever and for any
|
||||
purposes, including without limitation commercial purposes. These owners may
|
||||
contribute to the Commons to promote the ideal of a free culture and the
|
||||
further production of creative, cultural and scientific works, or to gain
|
||||
reputation or greater distribution for their Work in part through the use and
|
||||
efforts of others.
|
||||
|
||||
For these and/or other purposes and motivations, and without any expectation
|
||||
of additional consideration or compensation, the person associating CC0 with a
|
||||
Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
|
||||
and Related Rights in the Work, voluntarily elects to apply CC0 to the Work
|
||||
and publicly distribute the Work under its terms, with knowledge of his or her
|
||||
Copyright and Related Rights in the Work and the meaning and intended legal
|
||||
effect of CC0 on those rights.
|
||||
|
||||
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||
protected by copyright and related or neighboring rights ("Copyright and
|
||||
Related Rights"). Copyright and Related Rights include, but are not limited
|
||||
to, the following:
|
||||
|
||||
i. the right to reproduce, adapt, distribute, perform, display, communicate,
|
||||
and translate a Work;
|
||||
|
||||
ii. moral rights retained by the original author(s) and/or performer(s);
|
||||
|
||||
iii. publicity and privacy rights pertaining to a person's image or likeness
|
||||
depicted in a Work;
|
||||
|
||||
iv. rights protecting against unfair competition in regards to a Work,
|
||||
subject to the limitations in paragraph 4(a), below;
|
||||
|
||||
v. rights protecting the extraction, dissemination, use and reuse of data in
|
||||
a Work;
|
||||
|
||||
vi. database rights (such as those arising under Directive 96/9/EC of the
|
||||
European Parliament and of the Council of 11 March 1996 on the legal
|
||||
protection of databases, and under any national implementation thereof,
|
||||
including any amended or successor version of such directive); and
|
||||
|
||||
vii. other similar, equivalent or corresponding rights throughout the world
|
||||
based on applicable law or treaty, and any national implementations thereof.
|
||||
|
||||
2. Waiver. To the greatest extent permitted by, but not in contravention of,
|
||||
applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
|
||||
unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
|
||||
and Related Rights and associated claims and causes of action, whether now
|
||||
known or unknown (including existing as well as future claims and causes of
|
||||
action), in the Work (i) in all territories worldwide, (ii) for the maximum
|
||||
duration provided by applicable law or treaty (including future time
|
||||
extensions), (iii) in any current or future medium and for any number of
|
||||
copies, and (iv) for any purpose whatsoever, including without limitation
|
||||
commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes
|
||||
the Waiver for the benefit of each member of the public at large and to the
|
||||
detriment of Affirmer's heirs and successors, fully intending that such Waiver
|
||||
shall not be subject to revocation, rescission, cancellation, termination, or
|
||||
any other legal or equitable action to disrupt the quiet enjoyment of the Work
|
||||
by the public as contemplated by Affirmer's express Statement of Purpose.
|
||||
|
||||
3. Public License Fallback. Should any part of the Waiver for any reason be
|
||||
judged legally invalid or ineffective under applicable law, then the Waiver
|
||||
shall be preserved to the maximum extent permitted taking into account
|
||||
Affirmer's express Statement of Purpose. In addition, to the extent the Waiver
|
||||
is so judged Affirmer hereby grants to each affected person a royalty-free,
|
||||
non transferable, non sublicensable, non exclusive, irrevocable and
|
||||
unconditional license to exercise Affirmer's Copyright and Related Rights in
|
||||
the Work (i) in all territories worldwide, (ii) for the maximum duration
|
||||
provided by applicable law or treaty (including future time extensions), (iii)
|
||||
in any current or future medium and for any number of copies, and (iv) for any
|
||||
purpose whatsoever, including without limitation commercial, advertising or
|
||||
promotional purposes (the "License"). The License shall be deemed effective as
|
||||
of the date CC0 was applied by Affirmer to the Work. Should any part of the
|
||||
License for any reason be judged legally invalid or ineffective under
|
||||
applicable law, such partial invalidity or ineffectiveness shall not
|
||||
invalidate the remainder of the License, and in such case Affirmer hereby
|
||||
affirms that he or she will not (i) exercise any of his or her remaining
|
||||
Copyright and Related Rights in the Work or (ii) assert any associated claims
|
||||
and causes of action with respect to the Work, in either case contrary to
|
||||
Affirmer's express Statement of Purpose.
|
||||
|
||||
4. Limitations and Disclaimers.
|
||||
|
||||
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||
surrendered, licensed or otherwise affected by this document.
|
||||
|
||||
b. Affirmer offers the Work as-is and makes no representations or warranties
|
||||
of any kind concerning the Work, express, implied, statutory or otherwise,
|
||||
including without limitation warranties of title, merchantability, fitness
|
||||
for a particular purpose, non infringement, or the absence of latent or
|
||||
other defects, accuracy, or the present or absence of errors, whether or not
|
||||
discoverable, all to the greatest extent permissible under applicable law.
|
||||
|
||||
c. Affirmer disclaims responsibility for clearing rights of other persons
|
||||
that may apply to the Work or any use thereof, including without limitation
|
||||
any person's Copyright and Related Rights in the Work. Further, Affirmer
|
||||
disclaims responsibility for obtaining any necessary consents, permissions
|
||||
or other rights required for any use of the Work.
|
||||
|
||||
d. Affirmer understands and acknowledges that Creative Commons is not a
|
||||
party to this document and has no duty or obligation with respect to this
|
||||
CC0 or use of the Work.
|
||||
|
||||
For more information, please see
|
||||
<http://creativecommons.org/publicdomain/zero/1.0/>
|
||||
@@ -1,5 +1,40 @@
|
||||
Format
|
||||
------
|
||||
# gh-badges
|
||||
|
||||
[](https://npmjs.org/package/gh-badges)
|
||||
[](https://npmjs.org/package/gh-badges)
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
npm install gh-badges
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### On the console
|
||||
|
||||
```sh
|
||||
npm install -g gh-badges
|
||||
badge build passed :green .png > mybadge.png
|
||||
```
|
||||
|
||||
### As a library
|
||||
|
||||
```js
|
||||
const { BadgeFactory } = require('gh-badges')
|
||||
|
||||
const bf = new BadgeFactory()
|
||||
|
||||
const format = {
|
||||
text: ['build', 'passed'],
|
||||
colorscheme: 'green',
|
||||
template: 'flat',
|
||||
}
|
||||
|
||||
const svg = bf.create(format)
|
||||
```
|
||||
|
||||
## Format
|
||||
|
||||
The format is the following:
|
||||
|
||||
@@ -22,14 +57,13 @@ The format is the following:
|
||||
|
||||
### See also
|
||||
|
||||
- [colorscheme.json](../lib/colorscheme.json) for the `colorscheme` option
|
||||
- [templates/](../templates) for the `template` option
|
||||
- [colorscheme.json](./lib/colorscheme.json) for the `colorscheme` option
|
||||
- [templates/](./templates) for the `template` option
|
||||
|
||||
|
||||
Defaults
|
||||
--------
|
||||
## Defaults
|
||||
|
||||
If you want to add a colorscheme, head to `lib/colorscheme.json`. Each scheme
|
||||
If you want to use a colorscheme, head to `lib/colorscheme.json`. Each scheme
|
||||
has a name and a [CSS/SVG color][] for the color used in the first box (for the
|
||||
first piece of text, field `colorA`) and for the one used in the second box
|
||||
(field `colorB`).
|
||||
@@ -50,29 +84,3 @@ You can also use the `"colorA"` and `"colorB"` fields directly in the badges if
|
||||
you don't want to make a color scheme for it. In that case, remove the
|
||||
`"colorscheme"` field altogether.
|
||||
|
||||
Text Width Computation
|
||||
----------------------
|
||||
|
||||
`BadgeFactory`'s constructor takes an optional boolean
|
||||
`precomputeWidths` parameter which defaults to `false`.
|
||||
|
||||
Pre-computing the font width table adds some overhead to constructing the
|
||||
`BadgeFactory` object (so will slow down generation of a single image),
|
||||
but will speed up each badge generation if you are creating a lot of images.
|
||||
As a rule of thumb:
|
||||
|
||||
If you are generating just one image, use:
|
||||
|
||||
```js
|
||||
const bf = new BadgeFactory(
|
||||
{ fontPath: '/path/to/Verdana.ttf' }
|
||||
)
|
||||
```
|
||||
|
||||
If you are generating many images with a single instance of `BadgeFactory`:
|
||||
|
||||
```js
|
||||
const bf = new BadgeFactory(
|
||||
{ fontPath: '/path/to/Verdana.ttf', precomputeWidths: true }
|
||||
)
|
||||
```
|
||||
@@ -2,11 +2,9 @@
|
||||
|
||||
'use strict'
|
||||
|
||||
const { PDFKitTextMeasurer } = require('./text-measurer')
|
||||
const { makeBadge } = require('./make-badge')
|
||||
const makeBadge = require('./make-badge')
|
||||
const svg2img = require('./svg-to-img')
|
||||
const colorscheme = require('./colorscheme.json')
|
||||
const defaults = require('./defaults')
|
||||
|
||||
if (process.argv.length < 4) {
|
||||
console.log('Usage: badge subject status [:colorscheme] [.output] [@style]')
|
||||
@@ -14,9 +12,7 @@ if (process.argv.length < 4) {
|
||||
'Or: badge subject status right-color [left-color] [.output] [@style]'
|
||||
)
|
||||
console.log()
|
||||
console.log(
|
||||
' colorscheme: one of ' + Object.keys(colorscheme).join(', ') + '.'
|
||||
)
|
||||
console.log(` colorscheme: one of ${Object.keys(colorscheme).join(', ')}.`)
|
||||
console.log(' left-color, right-color:')
|
||||
console.log(' #xxx (three hex digits)')
|
||||
console.log(' #xxxxxx (six hex digits)')
|
||||
@@ -29,8 +25,6 @@ if (process.argv.length < 4) {
|
||||
process.exit()
|
||||
}
|
||||
|
||||
const fontPath = process.env.FONT_PATH || defaults.font.path
|
||||
|
||||
// Find a format specifier.
|
||||
let format = 'svg'
|
||||
let style = ''
|
||||
@@ -52,7 +46,7 @@ const status = process.argv[3]
|
||||
let color = process.argv[4] || ':green'
|
||||
const colorA = process.argv[5]
|
||||
|
||||
const badgeData = { text: [subject, status], format: format }
|
||||
const badgeData = { text: [subject, status], format }
|
||||
if (style) {
|
||||
badgeData.template = style
|
||||
}
|
||||
@@ -73,10 +67,7 @@ if (color[0] === ':') {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// The widths are going to be off if Helvetica-Bold is used, though this
|
||||
// should print a warning.
|
||||
const measurer = new PDFKitTextMeasurer(fontPath, 'Helvetica-Bold')
|
||||
const svg = makeBadge(measurer, badgeData)
|
||||
const svg = makeBadge(badgeData)
|
||||
|
||||
if (/png|jpg|gif/.test(format)) {
|
||||
const data = await svg2img(svg, format)
|
||||
@@ -1,15 +1,18 @@
|
||||
'use strict'
|
||||
|
||||
const { expect } = require('chai')
|
||||
const path = require('path')
|
||||
const isPng = require('is-png')
|
||||
const isSvg = require('is-svg')
|
||||
const { spawn } = require('child-process-promise')
|
||||
|
||||
// https://github.com/badges/shields/pull/1419#discussion_r159957055
|
||||
require('./register-chai-plugins.spec')
|
||||
const { expect, use } = require('chai')
|
||||
use(require('chai-string'))
|
||||
use(require('sinon-chai'))
|
||||
|
||||
function runCli(args) {
|
||||
return spawn('node', ['lib/badge-cli.js', ...args], { capture: ['stdout'] })
|
||||
return spawn('node', [path.join(__dirname, 'badge-cli.js'), ...args], {
|
||||
capture: ['stdout'],
|
||||
})
|
||||
}
|
||||
|
||||
describe('The CLI', function() {
|
||||
34
gh-badges/lib/index.js
Normal file
@@ -0,0 +1,34 @@
|
||||
'use strict'
|
||||
|
||||
const makeBadge = require('./make-badge')
|
||||
|
||||
class BadgeFactory {
|
||||
constructor(options) {
|
||||
if (options !== undefined) {
|
||||
console.error(
|
||||
'BadgeFactory: Constructor options are deprecated and will be ignored'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a badge
|
||||
*
|
||||
* @param {object} format - Object specifying badge data
|
||||
* @param {string[]} format.text
|
||||
* @param {string} format.colorscheme
|
||||
* @param {string} format.colorA
|
||||
* @param {string} format.colorB
|
||||
* @param {string} format.format
|
||||
* @param {string} format.template
|
||||
* @return {string} Badge in SVG or JSON format
|
||||
* @see https://github.com/badges/shields/tree/master/gh-badges/README.md
|
||||
*/
|
||||
create(format) {
|
||||
return makeBadge(format)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
BadgeFactory,
|
||||
}
|
||||
20
gh-badges/lib/index.spec.js
Normal file
@@ -0,0 +1,20 @@
|
||||
'use strict'
|
||||
|
||||
const { expect } = require('chai')
|
||||
const { BadgeFactory } = require('./index')
|
||||
const isSvg = require('is-svg')
|
||||
|
||||
const bf = new BadgeFactory()
|
||||
|
||||
describe('BadgeFactory class', function() {
|
||||
it('should produce badge with valid input', function() {
|
||||
expect(
|
||||
bf.create({
|
||||
text: ['build', 'passed'],
|
||||
format: 'svg',
|
||||
colorscheme: 'green',
|
||||
template: 'flat',
|
||||
})
|
||||
).to.satisfy(isSvg)
|
||||
})
|
||||
})
|
||||
@@ -107,7 +107,7 @@ Cache.prototype = {
|
||||
return 0
|
||||
}
|
||||
} else {
|
||||
console.error("Unknown heuristic '" + this.type + "' for LRU cache.")
|
||||
console.error(`Unknown heuristic '${this.type}' for LRU cache.`)
|
||||
return 1
|
||||
}
|
||||
},
|
||||
@@ -4,12 +4,9 @@ const fs = require('fs')
|
||||
const path = require('path')
|
||||
const SVGO = require('svgo')
|
||||
const dot = require('dot')
|
||||
const LruCache = require('./lru-cache')
|
||||
const anafanafo = require('anafanafo')
|
||||
const isCSSColor = require('is-css-color')
|
||||
|
||||
// Holds widths of badge keys (left hand side of badge).
|
||||
const badgeKeyWidthCache = new LruCache(1000)
|
||||
|
||||
// cache templates.
|
||||
const templates = {}
|
||||
const templateFiles = fs.readdirSync(path.join(__dirname, '..', 'templates'))
|
||||
@@ -22,16 +19,16 @@ templateFiles.forEach(async filename => {
|
||||
.readFileSync(path.join(__dirname, '..', 'templates', filename))
|
||||
.toString()
|
||||
const extension = path.extname(filename).slice(1)
|
||||
const style = filename.slice(0, -('-template.' + extension).length)
|
||||
const style = filename.slice(0, -`-template.${extension}`.length)
|
||||
// Compile the template. Necessary to always have a working template.
|
||||
templates[style + '-' + extension] = dot.template(templateData)
|
||||
templates[`${style}-${extension}`] = dot.template(templateData)
|
||||
if (extension === 'svg') {
|
||||
// Substitute dot code.
|
||||
const mapping = new Map()
|
||||
let mappingIndex = 1
|
||||
const untemplatedSvg = templateData.replace(/{{.*?}}/g, match => {
|
||||
// Weird substitution that currently works for all templates.
|
||||
const mapKey = '99999990' + mappingIndex + '.1'
|
||||
const mapKey = `99999990${mappingIndex}.1`
|
||||
mappingIndex++
|
||||
mapping.set(mapKey, match)
|
||||
return mapKey
|
||||
@@ -74,7 +71,7 @@ templateFiles.forEach(async filename => {
|
||||
return
|
||||
}
|
||||
|
||||
templates[style + '-' + extension] = dot.template(svg)
|
||||
templates[`${style}-${extension}`] = dot.template(svg)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -108,24 +105,20 @@ function assignColor(color = '', colorschemeType = 'colorB') {
|
||||
|
||||
const definedColorschemes = require(path.join(__dirname, 'colorscheme.json'))
|
||||
|
||||
// Inject the measurer to avoid placing any persistent state in this module.
|
||||
function makeBadge(
|
||||
measurer,
|
||||
{
|
||||
format,
|
||||
template,
|
||||
text,
|
||||
colorscheme,
|
||||
colorA,
|
||||
colorB,
|
||||
logo,
|
||||
logoPosition,
|
||||
logoWidth,
|
||||
links = ['', ''],
|
||||
}
|
||||
) {
|
||||
function makeBadge({
|
||||
format,
|
||||
template,
|
||||
text,
|
||||
colorscheme,
|
||||
colorA,
|
||||
colorB,
|
||||
logo,
|
||||
logoPosition,
|
||||
logoWidth,
|
||||
links = ['', ''],
|
||||
}) {
|
||||
// String coercion.
|
||||
text = text.map(value => '' + value)
|
||||
text = text.map(value => `${value}`)
|
||||
|
||||
if (format !== 'json') {
|
||||
format = 'svg'
|
||||
@@ -156,16 +149,12 @@ function makeBadge(
|
||||
colorB = assignColor(colorB, 'colorB')
|
||||
|
||||
const [left, right] = text
|
||||
let leftWidth = badgeKeyWidthCache.get(left)
|
||||
if (leftWidth === undefined) {
|
||||
leftWidth = measurer.widthOf(left) | 0
|
||||
// Increase chances of pixel grid alignment.
|
||||
if (leftWidth % 2 === 0) {
|
||||
leftWidth++
|
||||
}
|
||||
badgeKeyWidthCache.set(left, leftWidth)
|
||||
let leftWidth = (anafanafo(left) / 10) | 0
|
||||
// Increase chances of pixel grid alignment.
|
||||
if (leftWidth % 2 === 0) {
|
||||
leftWidth++
|
||||
}
|
||||
let rightWidth = measurer.widthOf(right) | 0
|
||||
let rightWidth = (anafanafo(right) / 10) | 0
|
||||
// Increase chances of pixel grid alignment.
|
||||
if (rightWidth % 2 === 0) {
|
||||
rightWidth++
|
||||
@@ -200,9 +189,4 @@ function makeBadge(
|
||||
return templateFn(context)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
makeBadge,
|
||||
makeMakeBadgeFn: measurer => data => makeBadge(measurer, data),
|
||||
// Expose for testing.
|
||||
_badgeKeyWidthCache: badgeKeyWidthCache,
|
||||
}
|
||||
module.exports = makeBadge
|
||||
@@ -4,13 +4,10 @@ const { test, given, forCases } = require('sazerac')
|
||||
const { expect } = require('chai')
|
||||
const snapshot = require('snap-shot-it')
|
||||
const eol = require('eol')
|
||||
const { _badgeKeyWidthCache } = require('./make-badge')
|
||||
const isSvg = require('is-svg')
|
||||
const testHelpers = require('./make-badge-test-helpers')
|
||||
const makeBadge = require('./make-badge')
|
||||
const colorschemes = require('./colorscheme.json')
|
||||
|
||||
const makeBadge = testHelpers.makeBadge()
|
||||
|
||||
function testColor(color = '') {
|
||||
return JSON.parse(
|
||||
makeBadge({
|
||||
@@ -23,10 +20,6 @@ function testColor(color = '') {
|
||||
}
|
||||
|
||||
describe('The badge generator', function() {
|
||||
beforeEach(function() {
|
||||
_badgeKeyWidthCache.clear()
|
||||
})
|
||||
|
||||
describe('color test', function() {
|
||||
test(testColor, () => {
|
||||
// valid hex
|
||||
@@ -82,11 +75,6 @@ describe('The badge generator', function() {
|
||||
const svg = makeBadge({ text: ['cactus', 'grown'], format: 'svg' })
|
||||
snapshot(svg)
|
||||
})
|
||||
|
||||
it('should cache width of badge key', function() {
|
||||
makeBadge({ text: ['cached', 'not-cached'], format: 'svg' })
|
||||
expect(_badgeKeyWidthCache.cache).to.have.keys('cached')
|
||||
})
|
||||
})
|
||||
|
||||
describe('JSON', function() {
|
||||
@@ -4,9 +4,7 @@ const { expect } = require('chai')
|
||||
const isPng = require('is-png')
|
||||
const sinon = require('sinon')
|
||||
const svg2img = require('./svg-to-img')
|
||||
const testHelpers = require('./make-badge-test-helpers')
|
||||
|
||||
const makeBadge = testHelpers.makeBadge()
|
||||
const makeBadge = require('./make-badge')
|
||||
|
||||
describe('The rasterizer', function() {
|
||||
let cacheGet
|
||||
45
gh-badges/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "gh-badges",
|
||||
"version": "2.1.0",
|
||||
"description": "Shields.io badge library",
|
||||
"keywords": [
|
||||
"GitHub",
|
||||
"badge",
|
||||
"SVG",
|
||||
"image",
|
||||
"shields.io"
|
||||
],
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/badges/shields.git"
|
||||
},
|
||||
"author": "Thaddée Tyl <thaddee.tyl@gmail.com>",
|
||||
"license": "CC0-1.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/badges/shields/issues"
|
||||
},
|
||||
"homepage": "http://shields.io",
|
||||
"bin": {
|
||||
"badge": "lib/badge-cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8",
|
||||
"npm": ">= 5"
|
||||
},
|
||||
"collective": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/shields",
|
||||
"logo": "https://opencollective.com/opencollective/logo.txt"
|
||||
},
|
||||
"dependencies": {
|
||||
"anafanafo": "^0.1.0",
|
||||
"dot": "~1.1.2",
|
||||
"gm": "^1.23.0",
|
||||
"is-css-color": "^1.0.0",
|
||||
"svgo": "~1.1.1"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo 'Run tests from parent dir'; false"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
@@ -1,15 +1,16 @@
|
||||
'use strict'
|
||||
|
||||
const fs = require('fs')
|
||||
const { URL } = require('url')
|
||||
|
||||
// We can either use a process-wide object regularly saved to a JSON file,
|
||||
// or a Redis equivalent (for multi-process / when the filesystem is unreliable.
|
||||
let redis
|
||||
let useRedis = false
|
||||
if (process.env.REDISTOGO_URL) {
|
||||
const redisToGo = require('url').parse(process.env.REDISTOGO_URL)
|
||||
redis = require('redis').createClient(redisToGo.port, redisToGo.hostname)
|
||||
redis.auth(redisToGo.auth.split(':')[1])
|
||||
const { port, hostname, password } = new URL(process.env.REDISTOGO_URL)
|
||||
redis = require('redis').createClient(port, hostname)
|
||||
redis.auth(password)
|
||||
useRedis = true
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ const isCSSColor = require('is-css-color')
|
||||
const logos = require('./load-logos')()
|
||||
const simpleIcons = require('./load-simple-icons')()
|
||||
const { svg2base64, isDataUri } = require('./logo-helper')
|
||||
const colorschemes = require('./colorscheme.json')
|
||||
const colorschemes = require('../gh-badges/lib/colorscheme.json')
|
||||
|
||||
function toArray(val) {
|
||||
if (val === undefined) {
|
||||
@@ -21,7 +21,7 @@ function prependPrefix(s, prefix) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
s = '' + s
|
||||
s = `${s}`
|
||||
|
||||
if (s.startsWith(prefix)) {
|
||||
return s
|
||||
@@ -36,7 +36,7 @@ function isHexColor(s = '') {
|
||||
|
||||
function makeColor(color) {
|
||||
if (isHexColor(color)) {
|
||||
return '#' + color
|
||||
return `#${color}`
|
||||
} else if (colorschemes[color] !== undefined) {
|
||||
return colorschemes[color].colorB
|
||||
} else if (isCSSColor(color)) {
|
||||
@@ -52,7 +52,7 @@ function makeColorB(defaultColor, overrides) {
|
||||
|
||||
function setBadgeColor(badgeData, color) {
|
||||
if (isHexColor(color)) {
|
||||
badgeData.colorB = '#' + color
|
||||
badgeData.colorB = `#${color}`
|
||||
delete badgeData.colorscheme
|
||||
} else if (colorschemes[color] !== undefined) {
|
||||
badgeData.colorscheme = color
|
||||
@@ -68,12 +68,11 @@ function setBadgeColor(badgeData, color) {
|
||||
}
|
||||
|
||||
function makeLabel(defaultLabel, overrides) {
|
||||
return (
|
||||
'' +
|
||||
(overrides.label === undefined
|
||||
return `${
|
||||
overrides.label === undefined
|
||||
? (defaultLabel || '').toLowerCase()
|
||||
: overrides.label)
|
||||
)
|
||||
: overrides.label
|
||||
}`
|
||||
}
|
||||
|
||||
function getShieldsIcon(icon = '', color = '') {
|
||||
|
||||
@@ -10,7 +10,7 @@ function version(version) {
|
||||
if (typeof version !== 'string' && typeof version !== 'number') {
|
||||
throw new Error(`Can't generate a version color for ${version}`)
|
||||
}
|
||||
version = '' + version
|
||||
version = `${version}`
|
||||
let first = version[0]
|
||||
if (first === 'v') {
|
||||
first = version[1]
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const path = require('path')
|
||||
|
||||
module.exports = {
|
||||
font: {
|
||||
// i.e. Verdana.ttf in the root of the project.
|
||||
path: path.join(__dirname, '..', 'Verdana.ttf'),
|
||||
},
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const { makeBadge } = require('./make-badge')
|
||||
const { PDFKitTextMeasurer, QuickTextMeasurer } = require('./text-measurer')
|
||||
|
||||
class BadgeFactory {
|
||||
constructor({ fontPath, fallbackFontPath, precomputeWidths = false }) {
|
||||
this.measurer = precomputeWidths
|
||||
? new QuickTextMeasurer(fontPath, fallbackFontPath)
|
||||
: new PDFKitTextMeasurer(fontPath, fallbackFontPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a badge
|
||||
*
|
||||
* @param {object} format - Object specifying badge data
|
||||
* @param {string[]} format.text
|
||||
* @param {string} format.colorscheme
|
||||
* @param {string} format.colorA
|
||||
* @param {string} format.colorB
|
||||
* @param {string} format.format
|
||||
* @param {string} format.template
|
||||
* @return {string} Badge in SVG or JSON format
|
||||
* @see https://github.com/badges/shields/blob/master/doc/gh-badges.md
|
||||
*/
|
||||
create(format) {
|
||||
return makeBadge(this.measurer, format)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
BadgeFactory,
|
||||
}
|
||||
@@ -2,10 +2,7 @@
|
||||
|
||||
const { EventEmitter } = require('events')
|
||||
const crypto = require('crypto')
|
||||
const log = require('./log')
|
||||
const secretIsValid = require('./sys/secret-is-valid')
|
||||
const queryString = require('query-string')
|
||||
const request = require('request')
|
||||
const serverSecrets = require('./server-secrets')
|
||||
const mapKeys = require('lodash.mapkeys')
|
||||
|
||||
@@ -25,133 +22,6 @@ if (serverSecrets && serverSecrets.gh_token) {
|
||||
addGithubToken(serverSecrets.gh_token)
|
||||
}
|
||||
|
||||
function setRoutes(server) {
|
||||
const baseUrl = process.env.BASE_URL || 'https://img.shields.io'
|
||||
|
||||
server.route(/^\/github-auth$/, (data, match, end, ask) => {
|
||||
if (!(serverSecrets && serverSecrets.gh_client_id)) {
|
||||
return end('This server is missing GitHub client secrets.')
|
||||
}
|
||||
const query = queryString.stringify({
|
||||
client_id: serverSecrets.gh_client_id,
|
||||
redirect_uri: baseUrl + '/github-auth/done',
|
||||
})
|
||||
ask.res.statusCode = 302 // Found.
|
||||
ask.res.setHeader(
|
||||
'Location',
|
||||
'https://github.com/login/oauth/authorize?' + query
|
||||
)
|
||||
end('')
|
||||
})
|
||||
|
||||
server.route(/^\/github-auth\/done$/, (data, match, end, ask) => {
|
||||
if (
|
||||
!(
|
||||
serverSecrets &&
|
||||
serverSecrets.gh_client_id &&
|
||||
serverSecrets.gh_client_secret
|
||||
)
|
||||
) {
|
||||
return end('This server is missing GitHub client secrets.')
|
||||
}
|
||||
if (!data.code) {
|
||||
log(`GitHub OAuth data.code: ${JSON.stringify(data)}`)
|
||||
return end('GitHub OAuth authentication failed to provide a code.')
|
||||
}
|
||||
const options = {
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
headers: {
|
||||
'Content-type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||
'User-Agent': 'Shields.io',
|
||||
},
|
||||
form: queryString.stringify({
|
||||
client_id: serverSecrets.gh_client_id,
|
||||
client_secret: serverSecrets.gh_client_secret,
|
||||
code: data.code,
|
||||
}),
|
||||
method: 'POST',
|
||||
}
|
||||
request(options, (err, res, body) => {
|
||||
if (err != null) {
|
||||
return end('The connection to GitHub failed.')
|
||||
}
|
||||
let content
|
||||
try {
|
||||
content = queryString.parse(body)
|
||||
} catch (e) {
|
||||
return end('The GitHub OAuth token could not be parsed.')
|
||||
}
|
||||
const token = content.access_token
|
||||
if (!token) {
|
||||
return end('The GitHub OAuth process did not return a user token.')
|
||||
}
|
||||
|
||||
ask.res.setHeader('Content-Type', 'text/html')
|
||||
end(
|
||||
'<p>Shields.io has received your app-specific GitHub user token. ' +
|
||||
'You can revoke it by going to ' +
|
||||
'<a href="https://github.com/settings/applications">GitHub</a>.</p>' +
|
||||
'<p>Until you do, you have now increased the rate limit for GitHub ' +
|
||||
'requests going through Shields.io. GitHub-related badges are ' +
|
||||
'therefore more robust.</p>' +
|
||||
'<p>Thanks for contributing to a smoother experience for ' +
|
||||
'everyone!</p>' +
|
||||
'<p><a href="/">Back to the website</a></p>'
|
||||
)
|
||||
|
||||
sendTokenToAllServers(token).catch(e => {
|
||||
console.error('GitHub user token transmission failed:', e)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
server.route(/^\/github-auth\/add-token$/, (data, match, end, ask) => {
|
||||
if (!secretIsValid(data.shieldsSecret)) {
|
||||
// An unknown entity tries to connect. Let the connection linger for 10s.
|
||||
return setTimeout(() => {
|
||||
end('Invalid secret.')
|
||||
}, 10000)
|
||||
}
|
||||
addGithubToken(data.token)
|
||||
emitter.emit('token-added', data.token)
|
||||
end('Thanks!')
|
||||
})
|
||||
}
|
||||
|
||||
function sendTokenToAllServers(token) {
|
||||
const ips = serverSecrets.shieldsIps
|
||||
return Promise.all(
|
||||
ips.map(
|
||||
ip =>
|
||||
new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
url: 'https://' + ip + '/github-auth/add-token',
|
||||
method: 'POST',
|
||||
form: {
|
||||
shieldsSecret: serverSecrets.shieldsSecret,
|
||||
token: token,
|
||||
},
|
||||
// We target servers by IP, and we use HTTPS. Assuming that
|
||||
// 1. Internet routers aren't hacked, and
|
||||
// 2. We don't unknowingly lose our IP to someone else,
|
||||
// we're not leaking people's and our information.
|
||||
// (If we did, it would have no impact, as we only ask for a token,
|
||||
// no GitHub scope. The malicious entity would only be able to use
|
||||
// our rate limit pool.)
|
||||
// FIXME: use letsencrypt.
|
||||
strictSSL: false,
|
||||
}
|
||||
request(options, (err, res, body) => {
|
||||
if (err != null) {
|
||||
return reject(err)
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// token: client token as a string.
|
||||
// reqs: number of requests remaining.
|
||||
// reset: timestamp when the number of remaining requests is reset.
|
||||
@@ -214,6 +84,7 @@ function addGithubToken(token) {
|
||||
if (githubUserTokens.indexOf(token) === -1) {
|
||||
githubUserTokens.push(token)
|
||||
}
|
||||
emitter.emit('token-added', token)
|
||||
}
|
||||
|
||||
function rmGithubToken(token) {
|
||||
@@ -298,7 +169,7 @@ function githubRequest(request, url, query, cb) {
|
||||
|
||||
if (githubToken != null) {
|
||||
// Typically, GitHub user tokens grants us 12500 req/hour.
|
||||
headers['Authorization'] = 'token ' + githubToken
|
||||
headers['Authorization'] = `token ${githubToken}`
|
||||
} else if (serverSecrets && serverSecrets.gh_client_id) {
|
||||
// Using our OAuth App secret grants us 5000 req/hour
|
||||
// instead of the standard 60 req/hour.
|
||||
@@ -308,10 +179,10 @@ function githubRequest(request, url, query, cb) {
|
||||
|
||||
const qs = queryString.stringify(query)
|
||||
if (qs) {
|
||||
url += '?' + qs
|
||||
url += `?${qs}`
|
||||
}
|
||||
|
||||
request(url, { headers: headers }, (err, res, buffer) => {
|
||||
request(url, { headers }, (err, res, buffer) => {
|
||||
if (globalToken !== null && githubToken !== null && err === null) {
|
||||
if (res.statusCode === 401) {
|
||||
// Unauthorized.
|
||||
@@ -333,7 +204,6 @@ function githubRequest(request, url, query, cb) {
|
||||
|
||||
module.exports = {
|
||||
request: githubRequest,
|
||||
setRoutes,
|
||||
serializeDebugInfo,
|
||||
addGithubToken,
|
||||
rmGithubToken,
|
||||
|
||||
@@ -14,7 +14,7 @@ function loadLogos() {
|
||||
return
|
||||
}
|
||||
// filename is eg, github.svg
|
||||
const svg = fs.readFileSync(logoDir + '/' + filename).toString()
|
||||
const svg = fs.readFileSync(`${logoDir}/${filename}`).toString()
|
||||
const base64 = svg2base64(svg)
|
||||
|
||||
// eg, github
|
||||
|
||||
@@ -8,7 +8,7 @@ const listeners = []
|
||||
// eg. 4 becomes 04 but 17 stays 17.
|
||||
function pad(string) {
|
||||
string = String(string)
|
||||
return string.length < 2 ? '0' + string : string
|
||||
return string.length < 2 ? `0${string}` : string
|
||||
}
|
||||
|
||||
// Compact date representation.
|
||||
|
||||
@@ -11,7 +11,7 @@ function svg2base64(svg) {
|
||||
// Check if logo is already base64
|
||||
return isDataUri(svg)
|
||||
? svg
|
||||
: 'data:image/svg+xml;base64,' + Buffer.from(svg).toString('base64')
|
||||
: `data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}`
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const path = require('path')
|
||||
const { PDFKitTextMeasurer } = require('./text-measurer')
|
||||
const { makeMakeBadgeFn } = require('./make-badge')
|
||||
|
||||
module.exports = {
|
||||
font: {
|
||||
path: path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'node_modules',
|
||||
'dejavu-fonts-ttf',
|
||||
'ttf',
|
||||
'DejaVuSans.ttf'
|
||||
),
|
||||
},
|
||||
measurer() {
|
||||
return new PDFKitTextMeasurer(this.font.path)
|
||||
},
|
||||
makeBadge() {
|
||||
return makeMakeBadgeFn(this.measurer())
|
||||
},
|
||||
}
|
||||
31
lib/make-badge-url.js
Normal file
@@ -0,0 +1,31 @@
|
||||
'use strict'
|
||||
|
||||
const queryString = require('query-string')
|
||||
|
||||
function encodeField(s) {
|
||||
return encodeURIComponent(s.replace(/-/g, '--').replace(/_/g, '__'))
|
||||
}
|
||||
|
||||
function staticBadgeUrl({
|
||||
baseUrl,
|
||||
label,
|
||||
message,
|
||||
color = 'lightgray',
|
||||
style,
|
||||
format = 'svg',
|
||||
}) {
|
||||
if (!label || !message) {
|
||||
throw Error('label and message are required')
|
||||
}
|
||||
const path = [label, message, color].map(encodeField).join('-')
|
||||
const outQueryString = queryString.stringify({
|
||||
style,
|
||||
})
|
||||
const suffix = outQueryString ? `?${outQueryString}` : ''
|
||||
return `/badge/${path}.${format}${suffix}`
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
encodeField,
|
||||
staticBadgeUrl,
|
||||
}
|
||||
42
lib/make-badge-url.spec.js
Normal file
@@ -0,0 +1,42 @@
|
||||
'use strict'
|
||||
|
||||
const { test, given } = require('sazerac')
|
||||
const { encodeField, staticBadgeUrl } = require('./make-badge-url')
|
||||
|
||||
describe('Badge URL generation functions', function() {
|
||||
test(encodeField, () => {
|
||||
given('foo').expect('foo')
|
||||
given('').expect('')
|
||||
given('happy go lucky').expect('happy%20go%20lucky')
|
||||
given('do-right').expect('do--right')
|
||||
given('it_is_a_snake').expect('it__is__a__snake')
|
||||
})
|
||||
|
||||
test(staticBadgeUrl, () => {
|
||||
given({
|
||||
label: 'foo',
|
||||
message: 'bar',
|
||||
color: 'blue',
|
||||
style: 'flat-square',
|
||||
}).expect('/badge/foo-bar-blue.svg?style=flat-square')
|
||||
given({
|
||||
label: 'foo',
|
||||
message: 'bar',
|
||||
color: 'blue',
|
||||
style: 'flat-square',
|
||||
format: 'png',
|
||||
}).expect('/badge/foo-bar-blue.png?style=flat-square')
|
||||
given({
|
||||
label: 'Hello World',
|
||||
message: 'Привет Мир',
|
||||
color: '#aabbcc',
|
||||
}).expect(
|
||||
'/badge/Hello%20World-%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80-%23aabbcc.svg'
|
||||
)
|
||||
given({
|
||||
label: '123-123',
|
||||
message: 'abc-abc',
|
||||
color: 'blue',
|
||||
}).expect('/badge/123--123-abc--abc-blue.svg')
|
||||
})
|
||||
})
|
||||
@@ -1,17 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
// Execute a synchronous block and invoke a standard error-first callback with
|
||||
// the result.
|
||||
function nodeifySync(resultFn, callback) {
|
||||
let result, error
|
||||
|
||||
try {
|
||||
result = resultFn()
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
callback(error, result)
|
||||
}
|
||||
|
||||
module.exports = nodeifySync
|
||||
@@ -1,32 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const { expect } = require('chai')
|
||||
const nodeifySync = require('./nodeify-sync')
|
||||
|
||||
describe('nodeifySync()', function() {
|
||||
it('Should return the result via the callback', function(done) {
|
||||
const exampleValue = {}
|
||||
nodeifySync(
|
||||
() => exampleValue,
|
||||
(err, result) => {
|
||||
expect(err).to.be.undefined
|
||||
expect(result).to.equal(exampleValue)
|
||||
done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('Should catch an error and return it via the callback', function(done) {
|
||||
const exampleError = Error('This is my error!')
|
||||
nodeifySync(
|
||||
() => {
|
||||
throw exampleError
|
||||
},
|
||||
(err, result) => {
|
||||
expect(err).to.equal(exampleError)
|
||||
expect(result).to.be.undefined
|
||||
done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -184,7 +184,7 @@ function minorVersion(version) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return result[1] + '.' + (result[2] ? result[2] : '0')
|
||||
return `${result[1]}.${result[2] ? result[2] : '0'}`
|
||||
}
|
||||
|
||||
function versionReduction(versions, phpReleases) {
|
||||
@@ -208,10 +208,10 @@ function versionReduction(versions, phpReleases) {
|
||||
// no missed versions
|
||||
if (first + versions.length - 1 === last) {
|
||||
if (last === phpReleases.length - 1) {
|
||||
return '>= ' + (versions[0][2] === '0' ? versions[0][0] : versions[0]) // 7.0 -> 7
|
||||
return `>= ${versions[0][2] === '0' ? versions[0][0] : versions[0]}` // 7.0 -> 7
|
||||
}
|
||||
|
||||
return versions[0] + ' - ' + versions[versions.length - 1]
|
||||
return `${versions[0]} - ${versions[versions.length - 1]}`
|
||||
}
|
||||
|
||||
return versions.join(', ')
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
'use strict'
|
||||
|
||||
const { Inaccessible, InvalidResponse } = require('../services/errors')
|
||||
|
||||
// Map from URL to { timestamp: last fetch time, data: data }.
|
||||
let regularUpdateCache = Object.create(null)
|
||||
|
||||
@@ -42,16 +44,34 @@ function regularUpdate(
|
||||
}
|
||||
request(url, options, (err, res, buffer) => {
|
||||
if (err != null) {
|
||||
cb(err)
|
||||
cb(
|
||||
new Inaccessible({
|
||||
prettyMessage: 'intermediate resource inaccessible',
|
||||
underlyingError: err,
|
||||
})
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (res.statusCode < 200 || res.statusCode >= 300) {
|
||||
cb(
|
||||
new InvalidResponse({
|
||||
prettyMessage: 'intermediate resource inaccessible',
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
let reqData
|
||||
if (json) {
|
||||
try {
|
||||
reqData = JSON.parse(buffer)
|
||||
} catch (e) {
|
||||
cb(e)
|
||||
cb(
|
||||
new InvalidResponse({
|
||||
prettyMessage: 'unparseable intermediate json response',
|
||||
underlyingError: e,
|
||||
})
|
||||
)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -5,7 +5,8 @@ const domain = require('domain')
|
||||
const request = require('request')
|
||||
const { makeBadgeData: getBadgeData } = require('./badge-data')
|
||||
const log = require('./log')
|
||||
const LruCache = require('./lru-cache')
|
||||
const LruCache = require('../gh-badges/lib/lru-cache')
|
||||
const makeBadge = require('../gh-badges/lib/make-badge')
|
||||
const analytics = require('./analytics')
|
||||
const { makeSend } = require('./result-sender')
|
||||
const queryString = require('query-string')
|
||||
@@ -61,13 +62,13 @@ function getBadgeMaxAge(handlerOptions, queryParams) {
|
||||
? parseInt(process.env.BADGE_MAX_AGE_SECONDS)
|
||||
: 120
|
||||
if (handlerOptions.cacheLength) {
|
||||
// if we've set a more specific cache length for this badge (or category),
|
||||
// use that instead of env.BADGE_MAX_AGE_SECONDS
|
||||
maxAge = parseInt(handlerOptions.cacheLength)
|
||||
// If we've set a more specific cache length for this badge (or category),
|
||||
// use that instead of env.BADGE_MAX_AGE_SECONDS.
|
||||
maxAge = handlerOptions.cacheLength
|
||||
}
|
||||
if (isInt(queryParams.maxAge) && parseInt(queryParams.maxAge) > maxAge) {
|
||||
// only allow queryParams.maxAge to override the default
|
||||
// if it is greater than the default
|
||||
// Only allow queryParams.maxAge to override the default if it is greater
|
||||
// than the default.
|
||||
maxAge = parseInt(queryParams.maxAge)
|
||||
}
|
||||
return maxAge
|
||||
@@ -88,9 +89,7 @@ function getBadgeMaxAge(handlerOptions, queryParams) {
|
||||
// (undesirable and hard to debug).
|
||||
//
|
||||
// Pass just the handler function as shorthand.
|
||||
//
|
||||
// Inject `makeBadge` as a dependency.
|
||||
function handleRequest(makeBadge, handlerOptions) {
|
||||
function handleRequest(handlerOptions) {
|
||||
if (typeof handlerOptions === 'function') {
|
||||
handlerOptions = { handler: handlerOptions }
|
||||
}
|
||||
@@ -107,7 +106,7 @@ function handleRequest(makeBadge, handlerOptions) {
|
||||
ask.res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||
ask.res.setHeader('Expires', reqTime.toGMTString())
|
||||
} else {
|
||||
ask.res.setHeader('Cache-Control', 'max-age=' + maxAge)
|
||||
ask.res.setHeader('Cache-Control', `max-age=${maxAge}`)
|
||||
ask.res.setHeader(
|
||||
'Expires',
|
||||
new Date(+reqTime + maxAge * 1000).toGMTString()
|
||||
@@ -180,7 +179,7 @@ function handleRequest(makeBadge, handlerOptions) {
|
||||
if (options && typeof options === 'object') {
|
||||
options.uri = uri
|
||||
} else if (typeof uri === 'string') {
|
||||
options = { uri: uri }
|
||||
options = { uri }
|
||||
} else {
|
||||
options = uri
|
||||
}
|
||||
@@ -247,7 +246,7 @@ function handleRequest(makeBadge, handlerOptions) {
|
||||
: 1,
|
||||
time: +reqTime,
|
||||
interval: cacheInterval,
|
||||
data: { format: format, badgeData: badgeData },
|
||||
data: { format, badgeData },
|
||||
}
|
||||
requestCache.set(cacheIndex, updatedCache)
|
||||
if (!cachedVersionSent) {
|
||||
@@ -276,8 +275,6 @@ function isInt(number) {
|
||||
|
||||
module.exports = {
|
||||
handleRequest,
|
||||
makeHandleRequestFn: makeBadge => handlerOptions =>
|
||||
handleRequest(makeBadge, handlerOptions),
|
||||
clearRequestCache,
|
||||
// Expose for testing.
|
||||
_requestCache: requestCache,
|
||||
|
||||
@@ -8,14 +8,11 @@ const Camp = require('camp')
|
||||
const analytics = require('./analytics')
|
||||
const { makeBadgeData: getBadgeData } = require('./badge-data')
|
||||
const {
|
||||
makeHandleRequestFn,
|
||||
handleRequest,
|
||||
clearRequestCache,
|
||||
_requestCache,
|
||||
getBadgeMaxAge,
|
||||
} = require('./request-handler')
|
||||
const testHelpers = require('./make-badge-test-helpers')
|
||||
|
||||
const handleRequest = makeHandleRequestFn(testHelpers.makeBadge())
|
||||
|
||||
const baseUri = `http://127.0.0.1:${config.port}`
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
const stream = require('stream')
|
||||
const log = require('./log')
|
||||
const svg2img = require('./svg-to-img')
|
||||
const svg2img = require('../gh-badges/lib/svg-to-img')
|
||||
|
||||
function streamFromString(str) {
|
||||
const newStream = new stream.Readable()
|
||||
@@ -29,8 +29,10 @@ function sendSVG(res, askres, end) {
|
||||
}
|
||||
|
||||
function sendOther(format, res, askres, end) {
|
||||
askres.setHeader('Content-Type', 'image/' + format)
|
||||
askres.setHeader('Content-Type', `image/${format}`)
|
||||
svg2img(res, format)
|
||||
// This interacts with callback code and can't use async/await.
|
||||
// eslint-disable-next-line promise/prefer-await-to-then
|
||||
.then(data => {
|
||||
end(null, { template: streamFromString(data) })
|
||||
})
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
const url = require('url')
|
||||
const envFlag = require('node-env-flag')
|
||||
const defaults = require('./defaults')
|
||||
|
||||
function envArray(envVar, defaultValue, delimiter) {
|
||||
delimiter = delimiter || ','
|
||||
@@ -39,6 +38,12 @@ const config = {
|
||||
port,
|
||||
address,
|
||||
},
|
||||
metrics: {
|
||||
prometheus: {
|
||||
enabled: envFlag(process.env.METRICS_PROMETHEUS_ENABLED, false),
|
||||
allowedIps: process.env.METRICS_PROMETHEUS_ALLOWED_IPS,
|
||||
},
|
||||
},
|
||||
ssl: {
|
||||
isSecure,
|
||||
key: process.env.HTTPS_KEY,
|
||||
@@ -63,10 +68,6 @@ const config = {
|
||||
},
|
||||
trace: envFlag(process.env.TRACE_SERVICES),
|
||||
},
|
||||
font: {
|
||||
path: process.env.FONT_PATH || defaults.font.path,
|
||||
fallbackPath: process.env.FALLBACK_FONT_PATH,
|
||||
},
|
||||
profiling: {
|
||||
makeBadge: envFlag(process.env.PROFILE_MAKE_BADGE),
|
||||
},
|
||||
@@ -74,8 +75,4 @@ const config = {
|
||||
handleInternalErrors: envFlag(process.env.HANDLE_INTERNAL_ERRORS, true),
|
||||
}
|
||||
|
||||
if (config.font.fallbackPath) {
|
||||
console.log('FALLBACK_FONT_PATH is deprecated. Please use FONT_PATH.')
|
||||
}
|
||||
|
||||
module.exports = config
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict'
|
||||
|
||||
const { parse: urlParse, format: urlFormat } = require('url')
|
||||
const { URL, format: urlFormat } = require('url')
|
||||
|
||||
function formatSlug(owner, repo, pullRequest) {
|
||||
return `${owner}/${repo}#${pullRequest}`
|
||||
@@ -9,19 +9,19 @@ function formatSlug(owner, repo, pullRequest) {
|
||||
function parseGithubPullRequestUrl(url, options = {}) {
|
||||
const { verifyBaseUrl } = options
|
||||
|
||||
const parsed = urlParse(url)
|
||||
const components = parsed.path.substr(1).split('/')
|
||||
const parsed = new URL(url)
|
||||
const components = parsed.pathname.substr(1).split('/')
|
||||
if (components[2] !== 'pull' || components.length !== 4) {
|
||||
throw Error(`Invalid GitHub pull request URL: ${url}`)
|
||||
}
|
||||
const [owner, repo, , pullRequest] = components
|
||||
|
||||
delete parsed.pathname
|
||||
parsed.pathname = ''
|
||||
const baseUrl = urlFormat(parsed, {
|
||||
auth: false,
|
||||
fragment: false,
|
||||
search: false,
|
||||
})
|
||||
}).replace(/\/$/, '')
|
||||
|
||||
if (verifyBaseUrl && baseUrl !== verifyBaseUrl) {
|
||||
throw Error(`Expected base URL to be ${verifyBaseUrl} but got ${baseUrl}`)
|
||||
|
||||
@@ -51,7 +51,7 @@ class Runner {
|
||||
|
||||
// Throw at the end, to provide a better error message.
|
||||
if (missingServices.length > 0) {
|
||||
throw Error('Unknown services: ' + missingServices.join(', '))
|
||||
throw Error(`Unknown services: ${missingServices.join(', ')}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
246
lib/suggest.js
@@ -1,46 +1,83 @@
|
||||
// Suggestion API
|
||||
//
|
||||
// eg. /$suggest/v1?url=https://github.com/badges/shields
|
||||
//
|
||||
// Tests for this endpoint are in services/suggest/suggest.spec.js. The
|
||||
// endpoint is called from frontend/components/suggestion-and-search.js.
|
||||
|
||||
'use strict'
|
||||
|
||||
const nodeUrl = require('url')
|
||||
const { URL } = require('url')
|
||||
const request = require('request')
|
||||
|
||||
// data: {url}, JSON-serializable object.
|
||||
// end: function(json), with json of the form:
|
||||
// - badges: list of objects of the form:
|
||||
// - link: target as a string URL.
|
||||
// - badge: shields image URL.
|
||||
// - name: string
|
||||
function suggest(allowedOrigin, githubApiProvider, data, end, ask) {
|
||||
// The typical dev and production setups are cross-origin. However, in
|
||||
// Heroku deploys and some self-hosted deploys these requests may come from
|
||||
// the same host.
|
||||
const origin = ask.req.headers.origin
|
||||
if (origin) {
|
||||
if (allowedOrigin.includes(origin)) {
|
||||
ask.res.setHeader('Access-Control-Allow-Origin', origin)
|
||||
} else {
|
||||
ask.res.setHeader('Access-Control-Allow-Origin', 'null')
|
||||
end({ err: 'Disallowed' })
|
||||
return
|
||||
}
|
||||
function twitterPage(url) {
|
||||
if (url.protocol === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
let url
|
||||
try {
|
||||
url = nodeUrl.parse(data.url)
|
||||
} catch (e) {
|
||||
end({ err: '' + e })
|
||||
return
|
||||
const schema = url.protocol.slice(0, -1)
|
||||
const host = url.host
|
||||
const path = url.pathname
|
||||
return {
|
||||
name: 'Twitter',
|
||||
link: `https://twitter.com/intent/tweet?text=Wow:&url=${encodeURIComponent(
|
||||
url.href
|
||||
)}`,
|
||||
badge: `https://img.shields.io/twitter/url/${schema}/${host}${path}.svg?style=social`,
|
||||
}
|
||||
findSuggestions(githubApiProvider, url, end)
|
||||
}
|
||||
|
||||
// url: string
|
||||
// cb: function({badges})
|
||||
function findSuggestions(githubApiProvider, url, cb) {
|
||||
function githubIssues(user, repo) {
|
||||
const repoSlug = `${user}/${repo}`
|
||||
return {
|
||||
name: 'GitHub issues',
|
||||
link: `https://github.com/${repoSlug}/issues`,
|
||||
badge: `https://img.shields.io/github/issues/${repoSlug}.svg`,
|
||||
}
|
||||
}
|
||||
|
||||
function githubForks(user, repo) {
|
||||
const repoSlug = `${user}/${repo}`
|
||||
return {
|
||||
name: 'GitHub forks',
|
||||
link: `https://github.com/${repoSlug}/network`,
|
||||
badge: `https://img.shields.io/github/forks/${repoSlug}.svg`,
|
||||
}
|
||||
}
|
||||
|
||||
function githubStars(user, repo) {
|
||||
const repoSlug = `${user}/${repo}`
|
||||
return {
|
||||
name: 'GitHub stars',
|
||||
link: `https://github.com/${repoSlug}/stargazers`,
|
||||
badge: `https://img.shields.io/github/stars/${repoSlug}.svg`,
|
||||
}
|
||||
}
|
||||
|
||||
async function githubLicense(githubApiProvider, user, repo) {
|
||||
const repoSlug = `${user}/${repo}`
|
||||
|
||||
let link = `https://github.com/${repoSlug}`
|
||||
|
||||
const { buffer } = await githubApiProvider.requestAsPromise(
|
||||
request,
|
||||
`/repos/${repoSlug}/license`
|
||||
)
|
||||
try {
|
||||
const data = JSON.parse(buffer)
|
||||
if ('html_url' in data) {
|
||||
link = data.html_url
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
return {
|
||||
name: 'GitHub license',
|
||||
badge: `https://img.shields.io/github/license/${repoSlug}.svg`,
|
||||
link,
|
||||
}
|
||||
}
|
||||
|
||||
async function findSuggestions(githubApiProvider, url) {
|
||||
let promises = []
|
||||
if (url.hostname === 'github.com') {
|
||||
const userRepo = url.pathname.slice(1).split('/')
|
||||
@@ -54,106 +91,69 @@ function findSuggestions(githubApiProvider, url, cb) {
|
||||
])
|
||||
}
|
||||
promises.push(twitterPage(url))
|
||||
Promise.all(promises)
|
||||
.then(badges => {
|
||||
// eslint-disable-next-line standard/no-callback-literal
|
||||
cb({
|
||||
badges: badges.filter(b => b != null),
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
// eslint-disable-next-line standard/no-callback-literal
|
||||
cb({ badges: [], err: err })
|
||||
})
|
||||
|
||||
const suggestions = await Promise.all(promises)
|
||||
|
||||
return suggestions.filter(b => b != null)
|
||||
}
|
||||
|
||||
function twitterPage(url) {
|
||||
if (url.protocol === null) {
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
const schema = url.protocol.slice(0, -1)
|
||||
const host = url.host
|
||||
const path = url.path
|
||||
return Promise.resolve({
|
||||
name: 'Twitter',
|
||||
link:
|
||||
'https://twitter.com/intent/tweet?text=Wow:&url=' +
|
||||
encodeURIComponent(url.href),
|
||||
badge:
|
||||
'https://img.shields.io/twitter/url/' +
|
||||
schema +
|
||||
'/' +
|
||||
host +
|
||||
path +
|
||||
'.svg?style=social',
|
||||
})
|
||||
}
|
||||
|
||||
function githubIssues(user, repo) {
|
||||
const userRepo = user + '/' + repo
|
||||
return Promise.resolve({
|
||||
name: 'GitHub issues',
|
||||
link: 'https://github.com/' + userRepo + '/issues',
|
||||
badge: 'https://img.shields.io/github/issues/' + userRepo + '.svg',
|
||||
})
|
||||
}
|
||||
|
||||
function githubForks(user, repo) {
|
||||
const userRepo = user + '/' + repo
|
||||
return Promise.resolve({
|
||||
name: 'GitHub forks',
|
||||
link: 'https://github.com/' + userRepo + '/network',
|
||||
badge: 'https://img.shields.io/github/forks/' + userRepo + '.svg',
|
||||
})
|
||||
}
|
||||
|
||||
function githubStars(user, repo) {
|
||||
const userRepo = user + '/' + repo
|
||||
return Promise.resolve({
|
||||
name: 'GitHub stars',
|
||||
link: 'https://github.com/' + userRepo + '/stargazers',
|
||||
badge: 'https://img.shields.io/github/stars/' + userRepo + '.svg',
|
||||
})
|
||||
}
|
||||
|
||||
function githubLicense(githubApiProvider, user, repo) {
|
||||
return new Promise(resolve => {
|
||||
const apiUrl = `/repos/${user}/${repo}/license`
|
||||
githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => {
|
||||
if (err !== null) {
|
||||
resolve(null)
|
||||
// data: {url}, JSON-serializable object.
|
||||
// end: function(json), with json of the form:
|
||||
// - badges: list of objects of the form:
|
||||
// - link: target as a string URL.
|
||||
// - badge: shields image URL.
|
||||
// - name: string
|
||||
function setRoutes(allowedOrigin, githubApiProvider, server) {
|
||||
server.ajax.on('suggest/v1', (data, end, ask) => {
|
||||
// The typical dev and production setups are cross-origin. However, in
|
||||
// Heroku deploys and some self-hosted deploys these requests may come from
|
||||
// the same host. Chrome does not send an Origin header on same-origin
|
||||
// requests, but Firefox does.
|
||||
//
|
||||
// It would be better to solve this problem using some well-tested
|
||||
// middleware.
|
||||
const origin = ask.req.headers.origin
|
||||
if (origin) {
|
||||
let host
|
||||
try {
|
||||
host = new URL(origin).hostname
|
||||
} catch (e) {
|
||||
ask.res.setHeader('Access-Control-Allow-Origin', 'null')
|
||||
end({ err: 'Disallowed' })
|
||||
return
|
||||
}
|
||||
const defaultBadge = {
|
||||
name: 'GitHub license',
|
||||
link: `https://github.com/${user}/${repo}`,
|
||||
badge: `https://img.shields.io/github/license/${user}/${repo}.svg`,
|
||||
}
|
||||
if (res.statusCode !== 200) {
|
||||
resolve(defaultBadge)
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(buffer)
|
||||
if (data.html_url) {
|
||||
defaultBadge.link = data.html_url
|
||||
resolve(defaultBadge)
|
||||
} else {
|
||||
resolve(defaultBadge)
|
||||
}
|
||||
} catch (e) {
|
||||
resolve(defaultBadge)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function setRoutes(allowedOrigin, githubApiProvider, server) {
|
||||
server.ajax.on('suggest/v1', (data, end, ask) =>
|
||||
suggest(allowedOrigin, githubApiProvider, data, end, ask)
|
||||
)
|
||||
if (host !== ask.req.headers.host) {
|
||||
if (allowedOrigin.includes(origin)) {
|
||||
ask.res.setHeader('Access-Control-Allow-Origin', origin)
|
||||
} else {
|
||||
ask.res.setHeader('Access-Control-Allow-Origin', 'null')
|
||||
end({ err: 'Disallowed' })
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let url
|
||||
try {
|
||||
url = new URL(data.url)
|
||||
} catch (e) {
|
||||
end({ err: `${e}` })
|
||||
return
|
||||
}
|
||||
|
||||
findSuggestions(githubApiProvider, url)
|
||||
// This interacts with callback code and can't use async/await.
|
||||
// eslint-disable-next-line promise/prefer-await-to-then
|
||||
.then(badges => {
|
||||
end({ badges })
|
||||
})
|
||||
.catch(err => {
|
||||
end({ badges: [], err })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
suggest,
|
||||
setRoutes,
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const nodeifySync = require('./nodeify-sync')
|
||||
|
||||
const leadingWhitespace = /(?:\r\n\s*|\r\s*|\n\s*)/g
|
||||
|
||||
function valueFromSvgBadge(svg, valueMatcher) {
|
||||
if (typeof svg !== 'string') {
|
||||
throw TypeError('Parameter should be a string')
|
||||
}
|
||||
const stripped = svg.replace(leadingWhitespace, '')
|
||||
const match = valueMatcher.exec(stripped)
|
||||
if (match) {
|
||||
return match[1]
|
||||
} else {
|
||||
throw Error(`Can't get value from SVG:\n${svg}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Get data from a svg-style badge.
|
||||
// cb: function(err, string)
|
||||
function fetchFromSvg(request, url, valueMatcher, cb) {
|
||||
request(url, (err, res, buffer) => {
|
||||
if (err !== null) {
|
||||
cb(err)
|
||||
} else {
|
||||
nodeifySync(() => valueFromSvgBadge(buffer, valueMatcher), cb)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
valueFromSvgBadge,
|
||||
fetchFromSvg,
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const { expect } = require('chai')
|
||||
const { makeBadgeData } = require('./badge-data')
|
||||
const { valueFromSvgBadge } = require('./svg-badge-parser')
|
||||
const testHelpers = require('./make-badge-test-helpers')
|
||||
|
||||
const makeBadge = testHelpers.makeBadge()
|
||||
|
||||
describe('The SVG badge parser', function() {
|
||||
it('should find the correct value', function() {
|
||||
const badgeData = makeBadgeData('this is the label', {})
|
||||
badgeData.text[1] = 'this is the result!'
|
||||
|
||||
const exampleSvg = makeBadge(badgeData)
|
||||
|
||||
expect(valueFromSvgBadge(exampleSvg, />([^<>]+)<\/text><\/g>/)).to.equal(
|
||||
'this is the result!'
|
||||
)
|
||||
})
|
||||
})
|
||||
43
lib/sys/prometheus-metrics.js
Normal file
@@ -0,0 +1,43 @@
|
||||
'use strict'
|
||||
|
||||
const prometheus = require('prom-client')
|
||||
|
||||
class PrometheusMetrics {
|
||||
constructor(config = {}) {
|
||||
this.enabled = config.enabled || false
|
||||
const matchNothing = /(?!)/
|
||||
this.allowedIps = config.allowedIps
|
||||
? new RegExp(config.allowedIps)
|
||||
: matchNothing
|
||||
if (this.enabled) {
|
||||
console.log(
|
||||
`Metrics are enabled. Access to /metrics resoure is limited to IP addresses matching: ${
|
||||
this.allowedIps
|
||||
}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async initialize(server) {
|
||||
if (this.enabled) {
|
||||
const register = prometheus.register
|
||||
prometheus.collectDefaultMetrics()
|
||||
this.setRoutes(server, register)
|
||||
}
|
||||
}
|
||||
|
||||
setRoutes(server, register) {
|
||||
server.route(/^\/metrics$/, (data, match, end, ask) => {
|
||||
const ip = ask.req.socket.remoteAddress
|
||||
if (this.allowedIps.test(ip)) {
|
||||
ask.res.setHeader('Content-Type', register.contentType)
|
||||
ask.res.end(register.metrics())
|
||||
} else {
|
||||
ask.res.statusCode = 403
|
||||
ask.res.end()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PrometheusMetrics
|
||||
93
lib/sys/prometheus-metrics.spec.js
Normal file
@@ -0,0 +1,93 @@
|
||||
'use strict'
|
||||
|
||||
const { expect } = require('chai')
|
||||
const Camp = require('camp')
|
||||
const fetch = require('node-fetch')
|
||||
const config = require('../test-config')
|
||||
const Metrics = require('./prometheus-metrics')
|
||||
|
||||
describe('Prometheus metrics route', function() {
|
||||
const baseUrl = `http://127.0.0.1:${config.port}`
|
||||
|
||||
let camp
|
||||
afterEach(function(done) {
|
||||
if (camp) {
|
||||
camp.close(() => done())
|
||||
camp = undefined
|
||||
}
|
||||
})
|
||||
|
||||
function startServer(metricsConfig) {
|
||||
return new Promise((resolve, reject) => {
|
||||
camp = Camp.start({ port: config.port, hostname: '::' })
|
||||
const metrics = new Metrics(metricsConfig)
|
||||
metrics.initialize(camp)
|
||||
camp.on('listening', () => resolve())
|
||||
})
|
||||
}
|
||||
|
||||
it('returns 404 when metrics are disabled', async function() {
|
||||
startServer({ enabled: false })
|
||||
|
||||
const res = await fetch(`${baseUrl}/metrics`)
|
||||
|
||||
expect(res.status).to.be.equal(404)
|
||||
expect(await res.text()).to.not.contains('nodejs_version_info')
|
||||
})
|
||||
|
||||
it('returns 404 when there is no configuration', async function() {
|
||||
startServer()
|
||||
|
||||
const res = await fetch(`${baseUrl}/metrics`)
|
||||
|
||||
expect(res.status).to.be.equal(404)
|
||||
expect(await res.text()).to.not.contains('nodejs_version_info')
|
||||
})
|
||||
|
||||
it('returns metrics for allowed IP', async function() {
|
||||
startServer({
|
||||
enabled: true,
|
||||
allowedIps: '^(127\\.0\\.0\\.1|::1|::ffff:127\\.0\\.0\\.1)$',
|
||||
})
|
||||
|
||||
const res = await fetch(`${baseUrl}/metrics`)
|
||||
|
||||
expect(res.status).to.be.equal(200)
|
||||
expect(await res.text()).to.contains('nodejs_version_info')
|
||||
})
|
||||
|
||||
it('returns metrics for request from allowed remote address', async function() {
|
||||
startServer({
|
||||
enabled: true,
|
||||
allowedIps: '^(127\\.0\\.0\\.1|::1|::ffff:127\\.0\\.0\\.1)$',
|
||||
})
|
||||
|
||||
const res = await fetch(`${baseUrl}/metrics`)
|
||||
|
||||
expect(res.status).to.be.equal(200)
|
||||
expect(await res.text()).to.contains('nodejs_version_info')
|
||||
})
|
||||
|
||||
it('returns 403 for not allowed IP', async function() {
|
||||
startServer({
|
||||
enabled: true,
|
||||
allowedIps: '^127\\.0\\.0\\.200$',
|
||||
})
|
||||
|
||||
const res = await fetch(`${baseUrl}/metrics`)
|
||||
|
||||
expect(res.status).to.be.equal(403)
|
||||
expect(await res.text()).to.not.contains('nodejs_version_info')
|
||||
})
|
||||
|
||||
it('returns 403 for every request when list with allowed IPs not defined', async function() {
|
||||
startServer({
|
||||
enabled: true,
|
||||
})
|
||||
|
||||
const res = await fetch(`${baseUrl}/metrics`)
|
||||
|
||||
expect(res.status).to.be.equal(403)
|
||||
expect(await res.text()).to.not.contains('nodejs_version_info')
|
||||
})
|
||||
})
|
||||
@@ -57,13 +57,13 @@ function metric(n) {
|
||||
if (n >= limit) {
|
||||
n = Math.round(n / limit)
|
||||
if (n < 1000) {
|
||||
return '' + n + metricPrefix[i]
|
||||
return `${n}${metricPrefix[i]}`
|
||||
} else {
|
||||
return '1' + metricPrefix[i + 1]
|
||||
return `1${metricPrefix[i + 1]}`
|
||||
}
|
||||
}
|
||||
}
|
||||
return '' + n
|
||||
return `${n}`
|
||||
}
|
||||
|
||||
// Remove the starting v in a string.
|
||||
@@ -79,7 +79,7 @@ function omitv(version) {
|
||||
// - it is a date (yyyy-mm-dd)
|
||||
const ignoredVersionPatterns = /^[^0-9]|[0-9]{4}-[0-9]{2}-[0-9]{2}/
|
||||
function addv(version) {
|
||||
version = '' + version
|
||||
version = `${version}`
|
||||
if (version.startsWith('v') || ignoredVersionPatterns.test(version)) {
|
||||
return version
|
||||
} else {
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const PDFDocument = require('pdfkit')
|
||||
|
||||
class PDFKitTextMeasurer {
|
||||
constructor(fontPath, fallbackFontPath) {
|
||||
this.document = new PDFDocument({
|
||||
size: 'A4',
|
||||
layout: 'landscape',
|
||||
}).fontSize(11)
|
||||
try {
|
||||
this.document.font(fontPath)
|
||||
} catch (e) {
|
||||
if (fallbackFontPath) {
|
||||
console.error(
|
||||
`Text-width computation may be incorrect. Unable to load font at ${fontPath}. Using fallback font ${fallbackFontPath} instead.`
|
||||
)
|
||||
this.document.font(fallbackFontPath)
|
||||
} else {
|
||||
console.error('No fallback font set.')
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
widthOf(str) {
|
||||
return this.document.widthOfString(str)
|
||||
}
|
||||
}
|
||||
|
||||
class QuickTextMeasurer {
|
||||
constructor(fontPath, fallbackFontPath) {
|
||||
this.baseMeasurer = new PDFKitTextMeasurer(fontPath, fallbackFontPath)
|
||||
|
||||
// This will be a Map of characters -> numbers.
|
||||
this.characterWidths = new Map()
|
||||
// This will be Map of Maps of characters -> numbers.
|
||||
this.kerningPairs = new Map()
|
||||
this._prepare()
|
||||
}
|
||||
|
||||
static printableAsciiCharacters() {
|
||||
const printableRange = [32, 126]
|
||||
const length = printableRange[1] - printableRange[0] + 1
|
||||
return Array.from({ length }, (value, i) => printableRange[0] + i).map(
|
||||
charCode => String.fromCharCode(charCode)
|
||||
)
|
||||
}
|
||||
|
||||
_prepare() {
|
||||
const charactersToCache = this.constructor.printableAsciiCharacters()
|
||||
|
||||
charactersToCache.forEach(char => {
|
||||
this.characterWidths.set(char, this.baseMeasurer.widthOf(char))
|
||||
this.kerningPairs.set(char, new Map())
|
||||
})
|
||||
|
||||
charactersToCache.forEach(first => {
|
||||
charactersToCache.forEach(second => {
|
||||
const individually =
|
||||
this.characterWidths.get(first) + this.characterWidths.get(second)
|
||||
const asPair = this.baseMeasurer.widthOf(`${first}${second}`)
|
||||
const kerningAdjustment = asPair - individually
|
||||
this.kerningPairs.get(first).set(second, kerningAdjustment)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
widthOf(str) {
|
||||
const { characterWidths, kerningPairs } = this
|
||||
|
||||
let result = 0
|
||||
let previous = null
|
||||
for (const character of str) {
|
||||
if (!characterWidths.has(character)) {
|
||||
// Bail.
|
||||
return this.baseMeasurer.widthOf(str)
|
||||
}
|
||||
|
||||
result += characterWidths.get(character)
|
||||
if (previous !== null) {
|
||||
result += kerningPairs.get(previous).get(character)
|
||||
}
|
||||
|
||||
previous = character
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
PDFKitTextMeasurer,
|
||||
QuickTextMeasurer,
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const { expect } = require('chai')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const sinon = require('sinon')
|
||||
const { PDFKitTextMeasurer, QuickTextMeasurer } = require('./text-measurer')
|
||||
const { starRating } = require('./text-formatters')
|
||||
const defaults = require('./defaults')
|
||||
const testHelpers = require('./make-badge-test-helpers')
|
||||
const almostEqual = require('almost-equal')
|
||||
|
||||
const EPSILON_PIXELS = 1e-3
|
||||
|
||||
describe('PDFKitTextMeasurer with DejaVu Sans', function() {
|
||||
it('should produce the same length as before', function() {
|
||||
const measurer = new PDFKitTextMeasurer(testHelpers.font.path)
|
||||
expect(
|
||||
measurer.widthOf('This is the dawning of the Age of Aquariums')
|
||||
).to.equal(243.546875)
|
||||
})
|
||||
})
|
||||
|
||||
function registerTests(fontPath, skip) {
|
||||
// Invoke `.skip()` within the `it`'s so we get logging of the skipped tests.
|
||||
const displayName = path.basename(fontPath, path.extname(fontPath))
|
||||
|
||||
describe(`QuickTextMeasurer with ${displayName}`, function() {
|
||||
let quickMeasurer
|
||||
if (!skip) {
|
||||
before(function() {
|
||||
// Since this is slow, share it across all tests.
|
||||
quickMeasurer = new QuickTextMeasurer(fontPath)
|
||||
})
|
||||
}
|
||||
|
||||
let sandbox
|
||||
let pdfKitWidthOf
|
||||
let pdfKitMeasurer
|
||||
if (!skip) {
|
||||
// Boo, the sandbox doesn't get cleaned up after a skipped test.
|
||||
beforeEach(function() {
|
||||
// This often times out: https://circleci.com/gh/badges/shields/2786
|
||||
this.timeout(5000)
|
||||
sandbox = sinon.createSandbox()
|
||||
pdfKitWidthOf = sandbox.spy(PDFKitTextMeasurer.prototype, 'widthOf')
|
||||
pdfKitMeasurer = new PDFKitTextMeasurer(fontPath)
|
||||
})
|
||||
|
||||
afterEach(function() {
|
||||
if (sandbox) {
|
||||
sandbox.restore()
|
||||
sandbox = null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
context('when given ASCII strings', function() {
|
||||
const strings = [
|
||||
'This is the dawning of the Age of Aquariums',
|
||||
'v1.2.511',
|
||||
'5 passed, 2 failed, 1 skipped',
|
||||
'[prismic "1.1"]',
|
||||
]
|
||||
|
||||
strings.forEach(str => {
|
||||
it(`should measure '${str}' in parity with PDFKit`, function() {
|
||||
if (skip) {
|
||||
this.skip()
|
||||
}
|
||||
expect(quickMeasurer.widthOf(str)).to.be.closeTo(
|
||||
pdfKitMeasurer.widthOf(str),
|
||||
EPSILON_PIXELS
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
strings.forEach(str => {
|
||||
it(`should measure '${str}' without invoking PDFKit`, function() {
|
||||
if (skip) {
|
||||
this.skip()
|
||||
}
|
||||
quickMeasurer.widthOf(str)
|
||||
expect(pdfKitWidthOf).not.to.have.been.called
|
||||
})
|
||||
})
|
||||
|
||||
context('when the font includes a kerning pair', function() {
|
||||
const stringsWithKerningPairs = [
|
||||
'Q-tips', // In DejaVu, Q- is a kerning pair.
|
||||
'B-flat', // In Verdana, B- is a kerning pair.
|
||||
]
|
||||
|
||||
function widthByMeasuringCharacters(str) {
|
||||
let result = 0
|
||||
for (const char of str) {
|
||||
result += pdfKitMeasurer.widthOf(char)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
it(`should apply a width correction`, function() {
|
||||
if (skip) {
|
||||
this.skip()
|
||||
}
|
||||
|
||||
const adjustedStrings = []
|
||||
|
||||
stringsWithKerningPairs.forEach(str => {
|
||||
const actual = quickMeasurer.widthOf(str)
|
||||
const unadjusted = widthByMeasuringCharacters(str)
|
||||
if (!almostEqual(actual, unadjusted, EPSILON_PIXELS)) {
|
||||
adjustedStrings.push(str)
|
||||
}
|
||||
})
|
||||
|
||||
expect(adjustedStrings).to.be.an('array').that.is.not.empty
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('when given non-ASCII strings', function() {
|
||||
const strings = [starRating(3.5), '\u2026']
|
||||
|
||||
strings.forEach(str => {
|
||||
it(`should measure '${str}' in parity with PDFKit`, function() {
|
||||
if (skip) {
|
||||
this.skip()
|
||||
}
|
||||
expect(quickMeasurer.widthOf(str)).to.be.closeTo(
|
||||
pdfKitMeasurer.widthOf(str),
|
||||
EPSILON_PIXELS
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
strings.forEach(str => {
|
||||
it(`should invoke the base when measuring '${str}'`, function() {
|
||||
if (skip) {
|
||||
this.skip()
|
||||
}
|
||||
quickMeasurer.widthOf(str)
|
||||
expect(pdfKitWidthOf).to.have.been.called
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// i.e. Verdana
|
||||
registerTests(defaults.font.path, !fs.existsSync(defaults.font.path))
|
||||
|
||||
// i.e. DejaVu Sans
|
||||
registerTests(testHelpers.font.path)
|
||||
43
lib/validate.js
Normal file
@@ -0,0 +1,43 @@
|
||||
'use strict'
|
||||
|
||||
const emojic = require('emojic')
|
||||
const Joi = require('joi')
|
||||
const trace = require('../services/trace')
|
||||
|
||||
function validate(
|
||||
{
|
||||
ErrorClass,
|
||||
prettyErrorMessage = 'data does not match schema',
|
||||
traceErrorMessage = 'Data did not match schema',
|
||||
traceSuccessMessage = 'Data after validation',
|
||||
},
|
||||
data,
|
||||
schema
|
||||
) {
|
||||
if (!schema || !schema.isJoi) {
|
||||
throw Error('A Joi schema is required')
|
||||
}
|
||||
const { error, value } = Joi.validate(data, schema, {
|
||||
allowUnknown: true,
|
||||
stripUnknown: true,
|
||||
})
|
||||
if (error) {
|
||||
trace.logTrace(
|
||||
'validate',
|
||||
emojic.womanShrugging,
|
||||
traceErrorMessage,
|
||||
error.message
|
||||
)
|
||||
throw new ErrorClass({
|
||||
prettyMessage: prettyErrorMessage,
|
||||
underlyingError: error,
|
||||
})
|
||||
} else {
|
||||
trace.logTrace('validate', emojic.bathtub, traceSuccessMessage, value, {
|
||||
deep: true,
|
||||
})
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = validate
|
||||
87
lib/validate.spec.js
Normal file
@@ -0,0 +1,87 @@
|
||||
'use strict'
|
||||
|
||||
const Joi = require('joi')
|
||||
const { expect } = require('chai')
|
||||
const sinon = require('sinon')
|
||||
const trace = require('../services/trace')
|
||||
const { InvalidParameter } = require('../services/errors')
|
||||
const validate = require('./validate')
|
||||
|
||||
describe('validate', function() {
|
||||
const schema = Joi.object({
|
||||
requiredString: Joi.string().required(),
|
||||
}).required()
|
||||
|
||||
let sandbox
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
afterEach(function() {
|
||||
sandbox.restore()
|
||||
})
|
||||
beforeEach(function() {
|
||||
sandbox.stub(trace, 'logTrace')
|
||||
})
|
||||
|
||||
const ErrorClass = InvalidParameter
|
||||
const prettyErrorMessage = 'parameter does not match schema'
|
||||
const traceErrorMessage = 'Params did not match schema'
|
||||
const traceSuccessMessage = 'Params after validation'
|
||||
|
||||
const options = {
|
||||
ErrorClass,
|
||||
prettyErrorMessage,
|
||||
traceErrorMessage,
|
||||
traceSuccessMessage,
|
||||
}
|
||||
|
||||
context('schema is not provided', function() {
|
||||
it('throws the expected programmer error', function() {
|
||||
try {
|
||||
validate(options, { requiredString: 'bar' }, undefined)
|
||||
expect.fail('Expected to throw')
|
||||
} catch (e) {
|
||||
expect(e).to.be.an.instanceof(Error)
|
||||
expect(e.message).to.equal('A Joi schema is required')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
context('data matches schema', function() {
|
||||
it('logs the data', function() {
|
||||
validate(options, { requiredString: 'bar' }, schema)
|
||||
expect(trace.logTrace).to.be.calledWithMatch(
|
||||
'validate',
|
||||
sinon.match.string,
|
||||
traceSuccessMessage,
|
||||
{ requiredString: 'bar' },
|
||||
{ deep: true }
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
context('data does not match schema', function() {
|
||||
it('logs the data and throws the expected error', async function() {
|
||||
try {
|
||||
validate(
|
||||
options,
|
||||
{ requiredString: ['this', "shouldn't", 'work'] },
|
||||
schema
|
||||
)
|
||||
expect.fail('Expected to throw')
|
||||
} catch (e) {
|
||||
expect(e).to.be.an.instanceof(InvalidParameter)
|
||||
expect(e.message).to.equal(
|
||||
'Invalid Parameter: child "requiredString" fails because ["requiredString" must be a string]'
|
||||
)
|
||||
expect(e.prettyMessage).to.equal(prettyErrorMessage)
|
||||
}
|
||||
expect(trace.logTrace).to.be.calledWithMatch(
|
||||
'validate',
|
||||
sinon.match.string,
|
||||
traceErrorMessage,
|
||||
'child "requiredString" fails because ["requiredString" must be a string]'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -30,8 +30,8 @@ function latest(versions, { pre = false } = {}) {
|
||||
// coerce to string then lowercase otherwise alpha > RC
|
||||
version = versions.sort((a, b) =>
|
||||
semver.rcompare(
|
||||
('' + a).toLowerCase(),
|
||||
('' + b).toLowerCase(),
|
||||
`${a}`.toLowerCase(),
|
||||
`${b}`.toLowerCase(),
|
||||
/* loose */ true
|
||||
)
|
||||
)[0]
|
||||
@@ -95,8 +95,8 @@ function compareDottedVersion(v1, v2) {
|
||||
return distinguisher1 < distinguisher2
|
||||
? -1
|
||||
: distinguisher1 > distinguisher2
|
||||
? 1
|
||||
: 0
|
||||
? 1
|
||||
: 0
|
||||
}
|
||||
}
|
||||
return v1 < v2 ? -1 : v1 > v2 ? 1 : 0
|
||||
|
||||
2
now.json
@@ -4,8 +4,10 @@
|
||||
"server.js",
|
||||
"favicon.png",
|
||||
"next.config.js",
|
||||
"package-lock.json",
|
||||
"build/",
|
||||
"frontend/",
|
||||
"gh-badges/",
|
||||
"lib/",
|
||||
"logo/",
|
||||
"pages/",
|
||||
|
||||
7745
package-lock.json
generated
87
package.json
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "gh-badges",
|
||||
"version": "2.0.0-beta1",
|
||||
"description": "Official Shields.io badge library.",
|
||||
"name": "shields.io",
|
||||
"version": "0.0.0",
|
||||
"description": "Shields.io server and frontend",
|
||||
"private": true,
|
||||
"keywords": [
|
||||
"GitHub",
|
||||
"badge",
|
||||
@@ -16,25 +17,23 @@
|
||||
},
|
||||
"license": "CC0-1.0",
|
||||
"author": "Thaddée Tyl <thaddee.tyl@gmail.com>",
|
||||
"main": "lib/gh-badges.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/badges/shields"
|
||||
},
|
||||
"dependencies": {
|
||||
"camp": "~17.2.1",
|
||||
"camp": "~17.2.2",
|
||||
"chalk": "^2.4.1",
|
||||
"check-node-version": "^3.1.0",
|
||||
"chrome-web-store-item-property": "~1.1.2",
|
||||
"dot": "~1.1.2",
|
||||
"emojic": "^1.1.14",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
"fast-xml-parser": "^3.12.0",
|
||||
"fast-xml-parser": "^3.12.7",
|
||||
"fsos": "^1.1.3",
|
||||
"gh-badges": "file:gh-badges",
|
||||
"glob": "^7.1.1",
|
||||
"gm": "^1.23.0",
|
||||
"is-css-color": "^1.0.0",
|
||||
"joi": "14.0.2",
|
||||
"joi": "14.0.4",
|
||||
"js-yaml": "^3.11.0",
|
||||
"jsonpath": "~1.0.0",
|
||||
"lodash.countby": "^4.6.0",
|
||||
@@ -44,26 +43,27 @@
|
||||
"lodash.uniq": "~4.5.0",
|
||||
"moment": "^2.19.3",
|
||||
"node-env-flag": "^0.1.0",
|
||||
"pdfkit": "~0.8.0",
|
||||
"path-to-regexp": "^2.4.0",
|
||||
"pretty-bytes": "^5.0.0",
|
||||
"priorityqueuejs": "^1.0.0",
|
||||
"prom-client": "^11.2.0",
|
||||
"query-string": "^6.0.0",
|
||||
"raven": "^2.4.2",
|
||||
"redis": "~2.8.0",
|
||||
"request": "~2.88.0",
|
||||
"semver": "~5.6.0",
|
||||
"simple-icons": "1.9.9",
|
||||
"svgo": "~1.1.1",
|
||||
"simple-icons": "1.9.13",
|
||||
"xml2js": "~0.4.16",
|
||||
"xmldom": "~0.1.27",
|
||||
"xpath": "~0.0.27"
|
||||
},
|
||||
"scripts": {
|
||||
"coverage:test:frontend": "NODE_ENV=mocha nyc node_modules/mocha/bin/_mocha --require @babel/polyfill --require @babel/register \"frontend/**/*.spec.js\"",
|
||||
"coverage:test:server": "HANDLE_INTERNAL_ERRORS=false nyc node_modules/mocha/bin/_mocha \"*.spec.js\" \"lib/**/*.spec.js\" \"services/**/*.spec.js\"",
|
||||
"coverage:test:frontend": "NODE_ENV=mocha nyc node_modules/mocha/bin/_mocha --require babel-polyfill --require babel-register \"frontend/**/*.spec.js\"",
|
||||
"coverage:test:package": "nyc node_modules/mocha/bin/_mocha \"gh-badges/**/*.spec.js\"",
|
||||
"coverage:test:integration": "nyc node_modules/mocha/bin/_mocha \"lib/**/*.integration.js\" \"services/**/*.integration.js\"",
|
||||
"coverage:test:services": "nyc node_modules/mocha/bin/_mocha --delay lib/service-test-runner/cli.js",
|
||||
"coverage:test": "rimraf .nyc_output coverage; npm run coverage:test:server; npm run coverage:test:frontend; npm run coverage:test:integration; npm run coverage:test:services",
|
||||
"coverage:test": "rimraf .nyc_output coverage; npm run coverage:test:server; npm run coverage:test:package; npm run coverage:test:frontend; npm run coverage:test:integration; npm run coverage:test:services",
|
||||
"coverage:report": "nyc report",
|
||||
"coverage:report:reopen": "opn coverage/lcov-report/index.html",
|
||||
"coverage:report:open": "npm run coverage:report && npm run coverage:report:reopen",
|
||||
@@ -71,22 +71,21 @@
|
||||
"prettier": "prettier --write \"**/*.js\"",
|
||||
"prettier-check": "prettier-check \"**/*.js\"",
|
||||
"danger": "danger",
|
||||
"test:js:frontend": "NODE_ENV=mocha mocha --require babel-polyfill --require babel-register \"frontend/**/*.spec.js\"",
|
||||
"test:js:frontend": "NODE_ENV=mocha mocha --require @babel/polyfill --require @babel/register \"frontend/**/*.spec.js\"",
|
||||
"test:js:server": "HANDLE_INTERNAL_ERRORS=false mocha \"*.spec.js\" \"lib/**/*.spec.js\" \"services/**/*.spec.js\"",
|
||||
"test:js:package": "mocha \"gh-badges/**/*.spec.js\"",
|
||||
"test:integration": "mocha \"lib/**/*.integration.js\" \"services/**/*.integration.js\"",
|
||||
"test:services": "HANDLE_INTERNAL_ERRORS=false mocha --delay lib/service-test-runner/cli.js",
|
||||
"test:services:trace": "TRACE_SERVICES=true npm run test:services -- $*",
|
||||
"test:services:pr:prepare": "node lib/service-test-runner/pull-request-services-cli.js > pull-request-services.log",
|
||||
"test:services:pr:run": "HANDLE_INTERNAL_ERRORS=false mocha --delay lib/service-test-runner/cli.js --stdin < pull-request-services.log",
|
||||
"test:services:pr": "npm run test:services:pr:prepare && npm run test:services:pr:run",
|
||||
"test": "npm run lint && npm run test:js:frontend && npm run test:js:server",
|
||||
"circle-images:build": "docker build -t shieldsio/shields-ci-node-8:${IMAGE_TAG} -f .circleci/images/node-8/Dockerfile . && docker build -t shieldsio/shields-ci-node-latest:${IMAGE_TAG} -f .circleci/images/node-latest/Dockerfile .",
|
||||
"circle-images:push": "docker push shieldsio/shields-ci-node-8:${IMAGE_TAG} && docker push shieldsio/shields-ci-node-latest:${IMAGE_TAG}",
|
||||
"test": "npm run lint && npm run test:js:frontend && npm run test:js:package && npm run test:js:server",
|
||||
"depcheck": "check-node-version --node \">= 8.0\"",
|
||||
"postinstall": "npm run depcheck",
|
||||
"prebuild": "npm run depcheck",
|
||||
"features": "node lib/export-supported-features-cli.js > supported-features.json",
|
||||
"examples": "node lib/export-badge-examples-cli.js > badge-examples.json",
|
||||
"features": "node scripts/export-supported-features-cli.js > supported-features.json",
|
||||
"examples": "node scripts/export-badge-examples-cli.js > badge-examples.json",
|
||||
"build": "npm run examples && npm run features && next build && next export -o build/",
|
||||
"heroku-postbuild": "npm run build",
|
||||
"analyze": "ANALYZE=true LONG_CACHE=false BASE_URL=https://img.shields.io npm run build",
|
||||
@@ -94,7 +93,7 @@
|
||||
"now-start": "node server",
|
||||
"prestart": "npm run depcheck && npm run examples && npm run features",
|
||||
"start": "concurrently --names server,frontend \"ALLOWED_ORIGIN=http://localhost:3000 npm run start:server\" \"BASE_URL=http://[::]:8080 next dev\"",
|
||||
"refactoring-report": "node lib/refactoring-cli.js"
|
||||
"refactoring-report": "node scripts/refactoring-cli.js"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*.js": [
|
||||
@@ -103,27 +102,16 @@
|
||||
"git add"
|
||||
]
|
||||
},
|
||||
"bin": {
|
||||
"badge": "lib/badge-cli.js"
|
||||
},
|
||||
"files": [
|
||||
"README.md",
|
||||
"lib/badge-cli.js",
|
||||
"lib/make-badge.js",
|
||||
"lib/colorscheme.json",
|
||||
"lib/lru-cache.js",
|
||||
"lib/text-measurer.js",
|
||||
"lib/svg-to-img.js",
|
||||
"lib/defaults.js",
|
||||
"templates",
|
||||
"logo"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.1.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.1.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.0.0",
|
||||
"@babel/polyfill": "^7.0.0",
|
||||
"@babel/preset-env": "^7.1.0",
|
||||
"@babel/register": "7.0.0",
|
||||
"@mapbox/react-click-to-select": "^2.2.0",
|
||||
"almost-equal": "^1.1.0",
|
||||
"babel-eslint": "^10.0.0",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"caller": "^1.0.1",
|
||||
"chai": "^4.1.2",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
@@ -132,11 +120,10 @@
|
||||
"child-process-promise": "^2.2.1",
|
||||
"classnames": "^2.2.5",
|
||||
"concurrently": "^4.0.1",
|
||||
"danger": "^4.0.1",
|
||||
"danger": "^6.1.4",
|
||||
"danger-plugin-no-test-shortcuts": "^2.0.0",
|
||||
"dejavu-fonts-ttf": "^2.37.3",
|
||||
"eol": "^0.9.1",
|
||||
"eslint": "^5.0.1",
|
||||
"eslint": "^5.9.0",
|
||||
"eslint-config-prettier": "^3.0.1",
|
||||
"eslint-config-standard": "^12.0.0",
|
||||
"eslint-config-standard-jsx": "^6.0.2",
|
||||
@@ -151,6 +138,7 @@
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"fetch-ponyfill": "^6.0.0",
|
||||
"fs-readfile-promise": "^3.0.1",
|
||||
"got": "^9.2.2",
|
||||
"husky": "^1.1.2",
|
||||
"icedfrisby": "2.0.0-alpha.2",
|
||||
"icedfrisby-nock": "^1.0.0",
|
||||
@@ -164,12 +152,12 @@
|
||||
"minimist": "^1.2.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"mocha": "^5.0.0",
|
||||
"next": "^5.0.0",
|
||||
"next": "^6.1.1",
|
||||
"nock": "^10.0.0",
|
||||
"node-fetch": "^2.0.0",
|
||||
"node-fetch": "^2.3.0",
|
||||
"nyc": "^13.0.1",
|
||||
"opn-cli": "^3.1.0",
|
||||
"prettier": "1.14.3",
|
||||
"opn-cli": "^4.0.0",
|
||||
"prettier": "1.15.2",
|
||||
"prettier-check": "^2.0.0",
|
||||
"pretty": "^2.0.0",
|
||||
"prop-types": "^15.6.0",
|
||||
@@ -189,20 +177,21 @@
|
||||
"wait-promise": "^0.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.x",
|
||||
"npm": "5.x"
|
||||
"node": ">= 8",
|
||||
"npm": ">= 5"
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
"next/babel"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-class-properties"
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-proposal-object-rest-spread"
|
||||
],
|
||||
"env": {
|
||||
"mocha": {
|
||||
"presets": [
|
||||
"env"
|
||||
"@babel/preset-env"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use strict'
|
||||
|
||||
const allBadgeExamples = require('./all-badge-examples')
|
||||
const allBadgeExamples = require('../lib/all-badge-examples')
|
||||
|
||||
process.stdout.write(JSON.stringify(allBadgeExamples))
|
||||
@@ -3,9 +3,9 @@
|
||||
const chalk = require('chalk')
|
||||
const mapValues = require('lodash.mapvalues')
|
||||
|
||||
const colorscheme = require('../lib/colorscheme.json')
|
||||
const colorscheme = require('../gh-badges/lib/colorscheme.json')
|
||||
const colorsMap = mapValues(colorscheme, 'colorB')
|
||||
const { floorCount } = require('./color-formatters')
|
||||
const { floorCount } = require('../lib/color-formatters')
|
||||
const { loadServiceClasses } = require('../services')
|
||||
|
||||
const serviceClasses = loadServiceClasses()
|
||||
120
server.js
@@ -16,26 +16,22 @@ const { checkErrorResponse } = require('./lib/error-helper')
|
||||
const analytics = require('./lib/analytics')
|
||||
const config = require('./lib/server-config')
|
||||
const GithubConstellation = require('./services/github/github-constellation')
|
||||
const PrometheusMetrics = require('./lib/sys/prometheus-metrics')
|
||||
const sysMonitor = require('./lib/sys/monitor')
|
||||
const log = require('./lib/log')
|
||||
const { makeMakeBadgeFn } = require('./lib/make-badge')
|
||||
const { QuickTextMeasurer } = require('./lib/text-measurer')
|
||||
const { staticBadgeUrl } = require('./lib/make-badge-url')
|
||||
const makeBadge = require('./gh-badges/lib/make-badge')
|
||||
const suggest = require('./lib/suggest')
|
||||
const {
|
||||
makeColorB,
|
||||
makeLabel: getLabel,
|
||||
makeBadgeData: getBadgeData,
|
||||
setBadgeColor,
|
||||
} = require('./lib/badge-data')
|
||||
const {
|
||||
makeHandleRequestFn,
|
||||
handleRequest: cache,
|
||||
clearRequestCache,
|
||||
} = require('./lib/request-handler')
|
||||
const { clearRegularUpdateCache } = require('./lib/regular-update')
|
||||
const { makeSend } = require('./lib/result-sender')
|
||||
const { escapeFormat } = require('./lib/path-helpers')
|
||||
|
||||
const serverStartTime = new Date(new Date().toGMTString())
|
||||
|
||||
const camp = require('camp').start({
|
||||
documentRoot: path.join(__dirname, 'public'),
|
||||
@@ -50,6 +46,7 @@ const githubConstellation = new GithubConstellation({
|
||||
persistence: config.persistence,
|
||||
service: config.services.github,
|
||||
})
|
||||
const metrics = new PrometheusMetrics(config.metrics.prometheus)
|
||||
const { apiProvider: githubApiProvider } = githubConstellation
|
||||
|
||||
function reset() {
|
||||
@@ -73,16 +70,6 @@ module.exports = {
|
||||
|
||||
log(`Server is starting up: ${config.baseUri}`)
|
||||
|
||||
let measurer
|
||||
try {
|
||||
measurer = new QuickTextMeasurer(config.font.path, config.font.fallbackPath)
|
||||
} catch (e) {
|
||||
console.log(`Unable to load fallback font. Using Helvetica-Bold instead.`)
|
||||
measurer = new QuickTextMeasurer('Helvetica')
|
||||
}
|
||||
const makeBadge = makeMakeBadgeFn(measurer)
|
||||
const cache = makeHandleRequestFn(makeBadge)
|
||||
|
||||
analytics.load()
|
||||
analytics.scheduleAutosaving()
|
||||
analytics.setRoutes(camp)
|
||||
@@ -92,6 +79,7 @@ if (serverSecrets && serverSecrets.shieldsSecret) {
|
||||
}
|
||||
|
||||
githubConstellation.initialize(camp)
|
||||
metrics.initialize(camp)
|
||||
|
||||
suggest.setRoutes(config.cors.allowedOrigin, githubApiProvider, camp)
|
||||
|
||||
@@ -115,7 +103,10 @@ camp.notfound(/.*/, (query, match, end, request) => {
|
||||
loadServiceClasses().forEach(serviceClass =>
|
||||
serviceClass.register(
|
||||
{ camp, handleRequest: cache, githubApiProvider },
|
||||
{ handleInternalErrors: config.handleInternalErrors }
|
||||
{
|
||||
handleInternalErrors: config.handleInternalErrors,
|
||||
profiling: config.profiling,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -235,58 +226,11 @@ camp.route(
|
||||
})
|
||||
)
|
||||
|
||||
// Any badge.
|
||||
camp.route(
|
||||
/^\/(:|badge\/)(([^-]|--)*?)-?(([^-]|--)*)-(([^-]|--)+)\.(svg|png|gif|jpg)$/,
|
||||
(data, match, end, ask) => {
|
||||
const subject = escapeFormat(match[2])
|
||||
const status = escapeFormat(match[4])
|
||||
const color = escapeFormat(match[6])
|
||||
const format = match[8]
|
||||
|
||||
analytics.noteRequest(data, match)
|
||||
|
||||
// Cache management - the badge is constant.
|
||||
const cacheDuration = (3600 * 24 * 1) | 0 // 1 day.
|
||||
ask.res.setHeader('Cache-Control', 'max-age=' + cacheDuration)
|
||||
if (+new Date(ask.req.headers['if-modified-since']) >= +serverStartTime) {
|
||||
ask.res.statusCode = 304
|
||||
ask.res.end() // not modified.
|
||||
return
|
||||
}
|
||||
ask.res.setHeader('Last-Modified', serverStartTime.toGMTString())
|
||||
|
||||
// Badge creation.
|
||||
try {
|
||||
const badgeData = getBadgeData(subject, data)
|
||||
badgeData.text[0] = getLabel(undefined, { label: subject })
|
||||
badgeData.text[1] = status
|
||||
badgeData.colorB = makeColorB(color, data)
|
||||
badgeData.template = data.style
|
||||
if (config.profiling.makeBadge) {
|
||||
console.time('makeBadge total')
|
||||
}
|
||||
const svg = makeBadge(badgeData)
|
||||
if (config.profiling.makeBadge) {
|
||||
console.timeEnd('makeBadge total')
|
||||
}
|
||||
makeSend(format, ask.res, end)(svg)
|
||||
} catch (e) {
|
||||
log.error(e.stack)
|
||||
const svg = makeBadge({
|
||||
text: ['error', 'bad badge'],
|
||||
colorscheme: 'red',
|
||||
})
|
||||
makeSend(format, ask.res, end)(svg)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Production cache debugging.
|
||||
let bitFlip = false
|
||||
camp.route(/^\/flip\.svg$/, (data, match, end, ask) => {
|
||||
const cacheSecs = 60
|
||||
ask.res.setHeader('Cache-Control', 'max-age=' + cacheSecs)
|
||||
ask.res.setHeader('Cache-Control', `max-age=${cacheSecs}`)
|
||||
const reqTime = new Date()
|
||||
const date = new Date(+reqTime + cacheSecs * 1000).toGMTString()
|
||||
ask.res.setHeader('Expires', date)
|
||||
@@ -298,32 +242,26 @@ camp.route(/^\/flip\.svg$/, (data, match, end, ask) => {
|
||||
makeSend('svg', ask.res, end)(svg)
|
||||
})
|
||||
|
||||
// Any badge, old version.
|
||||
camp.route(/^\/([^/]+)\/(.+).png$/, (data, match, end, ask) => {
|
||||
const subject = match[1]
|
||||
const status = match[2]
|
||||
const color = data.color
|
||||
// Any badge, old version. This route must be registered last.
|
||||
camp.route(/^\/([^/]+)\/(.+).png$/, (queryParams, match, end, ask) => {
|
||||
const [, label, message] = match
|
||||
const { color } = queryParams
|
||||
|
||||
// Cache management - the badge is constant.
|
||||
const cacheDuration = (3600 * 24 * 1) | 0 // 1 day.
|
||||
ask.res.setHeader('Cache-Control', 'max-age=' + cacheDuration)
|
||||
if (+new Date(ask.req.headers['if-modified-since']) >= +serverStartTime) {
|
||||
ask.res.statusCode = 304
|
||||
ask.res.end() // not modified.
|
||||
return
|
||||
}
|
||||
ask.res.setHeader('Last-Modified', serverStartTime.toGMTString())
|
||||
const redirectUrl = staticBadgeUrl({
|
||||
label,
|
||||
message,
|
||||
color,
|
||||
format: 'png',
|
||||
})
|
||||
|
||||
// Badge creation.
|
||||
try {
|
||||
const badgeData = { text: [subject, status] }
|
||||
badgeData.colorscheme = color
|
||||
const svg = makeBadge(badgeData)
|
||||
makeSend('png', ask.res, end)(svg)
|
||||
} catch (e) {
|
||||
const svg = makeBadge({ text: ['error', 'bad badge'], colorscheme: 'red' })
|
||||
makeSend('png', ask.res, end)(svg)
|
||||
}
|
||||
ask.res.statusCode = 301
|
||||
ask.res.setHeader('Location', redirectUrl)
|
||||
|
||||
// The redirect is permanent.
|
||||
const cacheDuration = (365 * 24 * 3600) | 0 // 1 year
|
||||
ask.res.setHeader('Cache-Control', `max-age=${cacheDuration}`)
|
||||
|
||||
ask.res.end()
|
||||
})
|
||||
|
||||
if (config.redirectUri) {
|
||||
|
||||
@@ -9,7 +9,7 @@ const isSvg = require('is-svg')
|
||||
const path = require('path')
|
||||
const serverHelpers = require('./lib/in-process-server-test-helpers')
|
||||
const sinon = require('sinon')
|
||||
const svg2img = require('./lib/svg-to-img')
|
||||
const svg2img = require('./gh-badges/lib/svg-to-img')
|
||||
|
||||
describe('The server', function() {
|
||||
const baseUri = `http://127.0.0.1:${config.port}`
|
||||
|
||||
@@ -26,8 +26,7 @@ module.exports = class Amo extends LegacyService {
|
||||
const addonId = match[2]
|
||||
const format = match[3]
|
||||
const badgeData = getBadgeData('mozilla add-on', queryData)
|
||||
const url =
|
||||
'https://services.addons.mozilla.org/api/1.5/addon/' + addonId
|
||||
const url = `https://services.addons.mozilla.org/api/1.5/addon/${addonId}`
|
||||
|
||||
request(url, (err, res, buffer) => {
|
||||
if (err) {
|
||||
@@ -62,7 +61,7 @@ module.exports = class Amo extends LegacyService {
|
||||
case 'rating':
|
||||
rating = parseInt(data.addon.rating, 10)
|
||||
badgeData.text[0] = getLabel('rating', queryData)
|
||||
badgeData.text[1] = rating + '/5'
|
||||
badgeData.text[1] = `${rating}/5`
|
||||
badgeData.colorscheme = floorCountColor(rating, 2, 3, 4)
|
||||
break
|
||||
case 'stars':
|
||||
|
||||
@@ -47,11 +47,10 @@ class AnsibleGalaxyRoleDownloads extends AnsibleGalaxyRole {
|
||||
return 'downloads'
|
||||
}
|
||||
|
||||
static get url() {
|
||||
static get route() {
|
||||
return {
|
||||
base: 'ansible/role/d',
|
||||
format: '(.+)',
|
||||
capture: ['roleId'],
|
||||
pattern: ':roleId',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +58,7 @@ class AnsibleGalaxyRoleDownloads extends AnsibleGalaxyRole {
|
||||
return [
|
||||
{
|
||||
title: `Ansible Role`,
|
||||
urlPattern: ':roleId',
|
||||
pattern: ':roleId',
|
||||
exampleUrl: '3078',
|
||||
staticExample: this.render({ downloads: 76 }),
|
||||
},
|
||||
@@ -86,7 +85,7 @@ class AnsibleGalaxyRoleName extends AnsibleGalaxyRole {
|
||||
return 'other'
|
||||
}
|
||||
|
||||
static get url() {
|
||||
static get route() {
|
||||
return {
|
||||
base: 'ansible/role',
|
||||
format: '(.+)',
|
||||
@@ -98,7 +97,7 @@ class AnsibleGalaxyRoleName extends AnsibleGalaxyRole {
|
||||
return [
|
||||
{
|
||||
title: `Ansible Role`,
|
||||
urlPattern: ':roleId',
|
||||
pattern: ':roleId',
|
||||
exampleUrl: '3078',
|
||||
staticExample: this.render({
|
||||
name: 'ansible-roles.sublimetext3_packagecontrol',
|
||||
|
||||
@@ -50,11 +50,10 @@ class APMDownloads extends BaseAPMService {
|
||||
return { label: 'downloads' }
|
||||
}
|
||||
|
||||
static get url() {
|
||||
static get route() {
|
||||
return {
|
||||
base: 'apm/dm',
|
||||
format: '(.+)',
|
||||
capture: ['repo'],
|
||||
pattern: ':repo',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +61,7 @@ class APMDownloads extends BaseAPMService {
|
||||
return [
|
||||
{
|
||||
exampleUrl: 'vim-mode',
|
||||
urlPattern: ':package',
|
||||
pattern: ':package',
|
||||
staticExample: this.render({ downloads: '60043' }),
|
||||
keywords: ['atom'],
|
||||
},
|
||||
@@ -90,7 +89,7 @@ class APMVersion extends BaseAPMService {
|
||||
return 'version'
|
||||
}
|
||||
|
||||
static get url() {
|
||||
static get route() {
|
||||
return {
|
||||
base: 'apm/v',
|
||||
format: '(.+)',
|
||||
@@ -102,7 +101,7 @@ class APMVersion extends BaseAPMService {
|
||||
return [
|
||||
{
|
||||
exampleUrl: 'vim-mode',
|
||||
urlPattern: ':package',
|
||||
pattern: ':package',
|
||||
staticExample: this.render({ version: '0.6.0' }),
|
||||
keywords: ['atom'],
|
||||
},
|
||||
@@ -134,7 +133,7 @@ class APMLicense extends BaseAPMService {
|
||||
return 'license'
|
||||
}
|
||||
|
||||
static get url() {
|
||||
static get route() {
|
||||
return {
|
||||
base: 'apm/l',
|
||||
format: '(.+)',
|
||||
@@ -146,7 +145,7 @@ class APMLicense extends BaseAPMService {
|
||||
return [
|
||||
{
|
||||
exampleUrl: 'vim-mode',
|
||||
urlPattern: ':package',
|
||||
pattern: ':package',
|
||||
staticExample: this.render({ license: 'MIT' }),
|
||||
keywords: ['atom'],
|
||||
},
|
||||
|
||||
@@ -34,7 +34,7 @@ module.exports = class AppVeyorBase extends BaseJsonService {
|
||||
})
|
||||
}
|
||||
|
||||
static buildUrl(base) {
|
||||
static buildRoute(base) {
|
||||
return {
|
||||
base,
|
||||
format: '([^/]+/[^/]+)(?:/(.+))?',
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
const AppVeyorBase = require('./appveyor-base')
|
||||
|
||||
module.exports = class AppVeyorCi extends AppVeyorBase {
|
||||
static get url() {
|
||||
return this.buildUrl('appveyor/ci')
|
||||
static get route() {
|
||||
return this.buildRoute('appveyor/ci')
|
||||
}
|
||||
|
||||
static get examples() {
|
||||
@@ -12,13 +12,13 @@ module.exports = class AppVeyorCi extends AppVeyorBase {
|
||||
{
|
||||
title: 'AppVeyor',
|
||||
exampleUrl: 'gruntjs/grunt',
|
||||
urlPattern: ':user/:repo',
|
||||
pattern: ':user/:repo',
|
||||
staticExample: this.render({ status: 'success' }),
|
||||
},
|
||||
{
|
||||
title: 'AppVeyor branch',
|
||||
exampleUrl: 'gruntjs/grunt/master',
|
||||
urlPattern: ':user/:repo/:branch',
|
||||
pattern: ':user/:repo/:branch',
|
||||
staticExample: this.render({ status: 'success' }),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -20,9 +20,9 @@ const documentation = `
|
||||
`
|
||||
|
||||
module.exports = class AppVeyorTests extends AppVeyorBase {
|
||||
static get url() {
|
||||
static get route() {
|
||||
return {
|
||||
...this.buildUrl('appveyor/tests'),
|
||||
...this.buildRoute('appveyor/tests'),
|
||||
queryParams: [
|
||||
'compact_message',
|
||||
'passed_label',
|
||||
|
||||