Compare commits

..

5 Commits

Author SHA1 Message Date
Caleb Cartwright
c6e31d7f32 load and manage persisted tokens with scope support 2021-09-19 11:41:26 -05:00
Caleb Cartwright
3aadb79325 allow github service classes to define scope requirements 2021-09-19 11:40:21 -05:00
Caleb Cartwright
b8412fd80b support scoped and unscoped tokens in API Provider 2021-09-19 11:39:48 -05:00
Caleb Cartwright
345188e34b expose token pools internal counts of held tokens 2021-09-19 11:38:53 -05:00
Caleb Cartwright
a92dc72ff5 support including scopes on oauth authorization 2021-09-19 11:37:18 -05:00
469 changed files with 16280 additions and 20784 deletions

View File

@@ -6,8 +6,7 @@ main_steps: &main_steps
- run:
name: Install dependencies
command: |
npm ci
command: npm ci
environment:
# https://docs.cypress.io/guides/getting-started/installing-cypress.html#Skipping-installation
# We don't need to install the Cypress binary in jobs that aren't actually running Cypress.
@@ -48,8 +47,7 @@ integration_steps: &integration_steps
- run:
name: Install dependencies
command: |
npm ci
command: npm ci
environment:
CYPRESS_INSTALL_BINARY: 0
@@ -70,8 +68,7 @@ services_steps: &services_steps
- run:
name: Install dependencies
command: |
npm ci
command: npm ci
environment:
CYPRESS_INSTALL_BINARY: 0
@@ -113,7 +110,6 @@ package_steps: &package_steps
MOCHA_FILE: junit/badge-maker/v12/results.xml
NODE_VERSION: v12
CYPRESS_INSTALL_BINARY: 0
NPM_CONFIG_ENGINE_STRICT: 'false'
name: Run package tests on Node 12
command: scripts/run_package_tests.sh
@@ -123,7 +119,6 @@ package_steps: &package_steps
MOCHA_FILE: junit/badge-maker/v14/results.xml
NODE_VERSION: v14
CYPRESS_INSTALL_BINARY: 0
NPM_CONFIG_ENGINE_STRICT: 'false'
name: Run package tests on Node 14
command: scripts/run_package_tests.sh
@@ -142,37 +137,33 @@ package_steps: &package_steps
jobs:
main:
docker:
- image: cimg/node:16.15
- image: circleci/node:14
<<: *main_steps
main@node-17:
main@node-16:
docker:
- image: cimg/node:17.9
environment:
NPM_CONFIG_ENGINE_STRICT: 'false'
- image: circleci/node:16
<<: *main_steps
integration:
docker:
- image: cimg/node:16.15
- image: circleci/node:14
- image: redis
<<: *integration_steps
integration@node-17:
integration@node-16:
docker:
- image: cimg/node:17.9
- image: circleci/node:16
- image: redis
environment:
NPM_CONFIG_ENGINE_STRICT: 'false'
<<: *integration_steps
danger:
docker:
- image: cimg/node:16.15
- image: circleci/node:14
steps:
- checkout
@@ -192,15 +183,13 @@ jobs:
frontend:
docker:
- image: cimg/node:16.15
- image: circleci/node:14
steps:
- checkout
- run:
name: Install dependencies
command: |
npm ci
command: npm ci
environment:
CYPRESS_INSTALL_BINARY: 0
@@ -229,29 +218,25 @@ jobs:
command: npm run build
package:
machine:
image: 'ubuntu-2004:202111-02'
machine: true
<<: *package_steps
services:
docker:
- image: cimg/node:16.15
- image: circleci/node:14
<<: *services_steps
services@node-17:
services@node-16:
docker:
- image: cimg/node:17.9
environment:
NPM_CONFIG_ENGINE_STRICT: 'false'
- image: circleci/node:16
<<: *services_steps
e2e:
docker:
- image: cypress/base:16.14.0
- image: cypress/base:14.16.0
steps:
- checkout
@@ -262,8 +247,7 @@ jobs:
- run:
name: Install dependencies
command: |
npm ci
command: npm ci
- run:
name: Frontend build
@@ -301,11 +285,11 @@ workflows:
filters:
branches:
ignore: gh-pages
- main@node-17:
- main@node-16:
filters:
branches:
ignore: gh-pages
- integration@node-17:
- integration@node-16:
filters:
branches:
ignore: gh-pages
@@ -323,7 +307,7 @@ workflows:
ignore:
- master
- gh-pages
- services@node-17:
- services@node-16:
filters:
branches:
ignore:

View File

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

View File

@@ -56,12 +56,6 @@ function isPointlessVersionBump(body) {
line => !line.startsWith('See <a href="https://conventionalcommits.org">')
)
.filter(line => !line.startsWith('<!--'))
.filter(
line =>
!line.startsWith(
'<p><a href="https://www.gatsbyjs.com/docs/reference/release-notes/'
)
)
return allChangelogLinesAreVersionBump(changelogLines)
}

View File

@@ -9,53 +9,50 @@
"version": "0.0.0",
"license": "CC0",
"dependencies": {
"@actions/core": "^1.9.0",
"@actions/github": "^5.0.3"
"@actions/core": "^1.5.0",
"@actions/github": "^5.0.0"
}
},
"node_modules/@actions/core": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.0.tgz",
"integrity": "sha512-5pbM693Ih59ZdUhgk+fts+bUWTnIdHV3kwOSr+QIoFHMLg7Gzhwm0cifDY/AG68ekEJAkHnQVpcy4f6GjmzBCA==",
"dependencies": {
"@actions/http-client": "^2.0.1"
}
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.5.0.tgz",
"integrity": "sha512-eDOLH1Nq9zh+PJlYLqEMkS/jLQxhksPNmUGNBHfa4G+tQmnIhzpctxmchETtVGyBOvXgOVVpYuE40+eS4cUnwQ=="
},
"node_modules/@actions/github": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-5.0.3.tgz",
"integrity": "sha512-myjA/pdLQfhUGLtRZC/J4L1RXOG4o6aYdiEq+zr5wVVKljzbFld+xv10k1FX6IkIJtNxbAq44BdwSNpQ015P0A==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-5.0.0.tgz",
"integrity": "sha512-QvE9eAAfEsS+yOOk0cylLBIO/d6WyWIOvsxxzdrPFaud39G6BOkUwScXZn1iBzQzHyu9SBkkLSWlohDWdsasAQ==",
"dependencies": {
"@actions/http-client": "^2.0.1",
"@octokit/core": "^3.6.0",
"@octokit/plugin-paginate-rest": "^2.17.0",
"@octokit/plugin-rest-endpoint-methods": "^5.13.0"
"@actions/http-client": "^1.0.11",
"@octokit/core": "^3.4.0",
"@octokit/plugin-paginate-rest": "^2.13.3",
"@octokit/plugin-rest-endpoint-methods": "^5.1.1"
}
},
"node_modules/@actions/http-client": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz",
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==",
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
"integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
"dependencies": {
"tunnel": "^0.0.6"
"tunnel": "0.0.6"
}
},
"node_modules/@octokit/auth-token": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz",
"integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==",
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.5.tgz",
"integrity": "sha512-BpGYsPgJt05M7/L/5FoE1PiAbdxXFZkX/3kDYcsvd1v6UhlnE5e96dTDr0ezX/EFwciQxf3cNV0loipsURU+WA==",
"dependencies": {
"@octokit/types": "^6.0.3"
}
},
"node_modules/@octokit/core": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz",
"integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.4.0.tgz",
"integrity": "sha512-6/vlKPP8NF17cgYXqucdshWqmMZGXkuvtcrWCgU5NOI0Pl2GjlmZyWgBMrU8zJ3v2MJlM6++CiB45VKYmhiWWg==",
"dependencies": {
"@octokit/auth-token": "^2.4.4",
"@octokit/graphql": "^4.5.8",
"@octokit/request": "^5.6.3",
"@octokit/request": "^5.4.12",
"@octokit/request-error": "^2.0.5",
"@octokit/types": "^6.0.3",
"before-after-hook": "^2.2.0",
@@ -63,9 +60,9 @@
}
},
"node_modules/@octokit/endpoint": {
"version": "6.0.12",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz",
"integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==",
"version": "6.0.11",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.11.tgz",
"integrity": "sha512-fUIPpx+pZyoLW4GCs3yMnlj2LfoXTWDUVPTC4V3MUEKZm48W+XYpeWSZCv+vYF1ZABUm2CqnDVf1sFtIYrj7KQ==",
"dependencies": {
"@octokit/types": "^6.0.3",
"is-plain-object": "^5.0.0",
@@ -73,37 +70,37 @@
}
},
"node_modules/@octokit/graphql": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz",
"integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==",
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.6.2.tgz",
"integrity": "sha512-WmsIR1OzOr/3IqfG9JIczI8gMJUMzzyx5j0XXQ4YihHtKlQc+u35VpVoOXhlKAlaBntvry1WpAzPl/a+s3n89Q==",
"dependencies": {
"@octokit/request": "^5.6.0",
"@octokit/request": "^5.3.0",
"@octokit/types": "^6.0.3",
"universal-user-agent": "^6.0.0"
}
},
"node_modules/@octokit/openapi-types": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-11.2.0.tgz",
"integrity": "sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA=="
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-7.0.0.tgz",
"integrity": "sha512-gV/8DJhAL/04zjTI95a7FhQwS6jlEE0W/7xeYAzuArD0KVAVWDLP2f3vi98hs3HLTczxXdRK/mF0tRoQPpolEw=="
},
"node_modules/@octokit/plugin-paginate-rest": {
"version": "2.17.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.17.0.tgz",
"integrity": "sha512-tzMbrbnam2Mt4AhuyCHvpRkS0oZ5MvwwcQPYGtMv4tUa5kkzG58SVB0fcsLulOZQeRnOgdkZWkRUiyBlh0Bkyw==",
"version": "2.13.3",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.13.3.tgz",
"integrity": "sha512-46lptzM9lTeSmIBt/sVP/FLSTPGx6DCzAdSX3PfeJ3mTf4h9sGC26WpaQzMEq/Z44cOcmx8VsOhO+uEgE3cjYg==",
"dependencies": {
"@octokit/types": "^6.34.0"
"@octokit/types": "^6.11.0"
},
"peerDependencies": {
"@octokit/core": ">=2"
}
},
"node_modules/@octokit/plugin-rest-endpoint-methods": {
"version": "5.13.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.13.0.tgz",
"integrity": "sha512-uJjMTkN1KaOIgNtUPMtIXDOjx6dGYysdIFhgA52x4xSadQCz3b/zJexvITDVpANnfKPW/+E0xkOvLntqMYpviA==",
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.1.1.tgz",
"integrity": "sha512-u4zy0rVA8darm/AYsIeWkRalhQR99qPL1D/EXHejV2yaECMdHfxXiTXtba8NMBSajOJe8+C9g+EqMKSvysx0dg==",
"dependencies": {
"@octokit/types": "^6.34.0",
"@octokit/types": "^6.14.1",
"deprecation": "^2.3.1"
},
"peerDependencies": {
@@ -111,22 +108,22 @@
}
},
"node_modules/@octokit/request": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz",
"integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==",
"version": "5.4.15",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.4.15.tgz",
"integrity": "sha512-6UnZfZzLwNhdLRreOtTkT9n57ZwulCve8q3IT/Z477vThu6snfdkBuhxnChpOKNGxcQ71ow561Qoa6uqLdPtag==",
"dependencies": {
"@octokit/endpoint": "^6.0.1",
"@octokit/request-error": "^2.1.0",
"@octokit/types": "^6.16.1",
"@octokit/request-error": "^2.0.0",
"@octokit/types": "^6.7.1",
"is-plain-object": "^5.0.0",
"node-fetch": "^2.6.7",
"node-fetch": "^2.6.1",
"universal-user-agent": "^6.0.0"
}
},
"node_modules/@octokit/request-error": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz",
"integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==",
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.0.5.tgz",
"integrity": "sha512-T/2wcCFyM7SkXzNoyVNWjyVlUwBvW3igM3Btr/eKYiPmucXTtkxt2RBsf6gn3LTzaLSLTQtNmvg+dGsOxQrjZg==",
"dependencies": {
"@octokit/types": "^6.0.3",
"deprecation": "^2.0.0",
@@ -134,17 +131,17 @@
}
},
"node_modules/@octokit/types": {
"version": "6.34.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.34.0.tgz",
"integrity": "sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw==",
"version": "6.14.2",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.14.2.tgz",
"integrity": "sha512-wiQtW9ZSy4OvgQ09iQOdyXYNN60GqjCL/UdMsepDr1Gr0QzpW6irIKbH3REuAHXAhxkEk9/F2a3Gcs1P6kW5jA==",
"dependencies": {
"@octokit/openapi-types": "^11.2.0"
"@octokit/openapi-types": "^7.0.0"
}
},
"node_modules/before-after-hook": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz",
"integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ=="
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.1.tgz",
"integrity": "sha512-/6FKxSTWoJdbsLDF8tdIjaRiFXiE6UHsEHE3OPI/cwPURCVi1ukP0gmLn7XWEiFk5TcwQjjY5PWsU+j+tgXgmw=="
},
"node_modules/deprecation": {
"version": "2.3.1",
@@ -160,22 +157,11 @@
}
},
"node_modules/node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==",
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/once": {
@@ -186,11 +172,6 @@
"wrappy": "1"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
},
"node_modules/tunnel": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
@@ -204,20 +185,6 @@
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -226,48 +193,45 @@
},
"dependencies": {
"@actions/core": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.0.tgz",
"integrity": "sha512-5pbM693Ih59ZdUhgk+fts+bUWTnIdHV3kwOSr+QIoFHMLg7Gzhwm0cifDY/AG68ekEJAkHnQVpcy4f6GjmzBCA==",
"requires": {
"@actions/http-client": "^2.0.1"
}
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.5.0.tgz",
"integrity": "sha512-eDOLH1Nq9zh+PJlYLqEMkS/jLQxhksPNmUGNBHfa4G+tQmnIhzpctxmchETtVGyBOvXgOVVpYuE40+eS4cUnwQ=="
},
"@actions/github": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-5.0.3.tgz",
"integrity": "sha512-myjA/pdLQfhUGLtRZC/J4L1RXOG4o6aYdiEq+zr5wVVKljzbFld+xv10k1FX6IkIJtNxbAq44BdwSNpQ015P0A==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-5.0.0.tgz",
"integrity": "sha512-QvE9eAAfEsS+yOOk0cylLBIO/d6WyWIOvsxxzdrPFaud39G6BOkUwScXZn1iBzQzHyu9SBkkLSWlohDWdsasAQ==",
"requires": {
"@actions/http-client": "^2.0.1",
"@octokit/core": "^3.6.0",
"@octokit/plugin-paginate-rest": "^2.17.0",
"@octokit/plugin-rest-endpoint-methods": "^5.13.0"
"@actions/http-client": "^1.0.11",
"@octokit/core": "^3.4.0",
"@octokit/plugin-paginate-rest": "^2.13.3",
"@octokit/plugin-rest-endpoint-methods": "^5.1.1"
}
},
"@actions/http-client": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz",
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==",
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
"integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
"requires": {
"tunnel": "^0.0.6"
"tunnel": "0.0.6"
}
},
"@octokit/auth-token": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz",
"integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==",
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.5.tgz",
"integrity": "sha512-BpGYsPgJt05M7/L/5FoE1PiAbdxXFZkX/3kDYcsvd1v6UhlnE5e96dTDr0ezX/EFwciQxf3cNV0loipsURU+WA==",
"requires": {
"@octokit/types": "^6.0.3"
}
},
"@octokit/core": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz",
"integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.4.0.tgz",
"integrity": "sha512-6/vlKPP8NF17cgYXqucdshWqmMZGXkuvtcrWCgU5NOI0Pl2GjlmZyWgBMrU8zJ3v2MJlM6++CiB45VKYmhiWWg==",
"requires": {
"@octokit/auth-token": "^2.4.4",
"@octokit/graphql": "^4.5.8",
"@octokit/request": "^5.6.3",
"@octokit/request": "^5.4.12",
"@octokit/request-error": "^2.0.5",
"@octokit/types": "^6.0.3",
"before-after-hook": "^2.2.0",
@@ -275,9 +239,9 @@
}
},
"@octokit/endpoint": {
"version": "6.0.12",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz",
"integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==",
"version": "6.0.11",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.11.tgz",
"integrity": "sha512-fUIPpx+pZyoLW4GCs3yMnlj2LfoXTWDUVPTC4V3MUEKZm48W+XYpeWSZCv+vYF1ZABUm2CqnDVf1sFtIYrj7KQ==",
"requires": {
"@octokit/types": "^6.0.3",
"is-plain-object": "^5.0.0",
@@ -285,54 +249,54 @@
}
},
"@octokit/graphql": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz",
"integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==",
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.6.2.tgz",
"integrity": "sha512-WmsIR1OzOr/3IqfG9JIczI8gMJUMzzyx5j0XXQ4YihHtKlQc+u35VpVoOXhlKAlaBntvry1WpAzPl/a+s3n89Q==",
"requires": {
"@octokit/request": "^5.6.0",
"@octokit/request": "^5.3.0",
"@octokit/types": "^6.0.3",
"universal-user-agent": "^6.0.0"
}
},
"@octokit/openapi-types": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-11.2.0.tgz",
"integrity": "sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA=="
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-7.0.0.tgz",
"integrity": "sha512-gV/8DJhAL/04zjTI95a7FhQwS6jlEE0W/7xeYAzuArD0KVAVWDLP2f3vi98hs3HLTczxXdRK/mF0tRoQPpolEw=="
},
"@octokit/plugin-paginate-rest": {
"version": "2.17.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.17.0.tgz",
"integrity": "sha512-tzMbrbnam2Mt4AhuyCHvpRkS0oZ5MvwwcQPYGtMv4tUa5kkzG58SVB0fcsLulOZQeRnOgdkZWkRUiyBlh0Bkyw==",
"version": "2.13.3",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.13.3.tgz",
"integrity": "sha512-46lptzM9lTeSmIBt/sVP/FLSTPGx6DCzAdSX3PfeJ3mTf4h9sGC26WpaQzMEq/Z44cOcmx8VsOhO+uEgE3cjYg==",
"requires": {
"@octokit/types": "^6.34.0"
"@octokit/types": "^6.11.0"
}
},
"@octokit/plugin-rest-endpoint-methods": {
"version": "5.13.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.13.0.tgz",
"integrity": "sha512-uJjMTkN1KaOIgNtUPMtIXDOjx6dGYysdIFhgA52x4xSadQCz3b/zJexvITDVpANnfKPW/+E0xkOvLntqMYpviA==",
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.1.1.tgz",
"integrity": "sha512-u4zy0rVA8darm/AYsIeWkRalhQR99qPL1D/EXHejV2yaECMdHfxXiTXtba8NMBSajOJe8+C9g+EqMKSvysx0dg==",
"requires": {
"@octokit/types": "^6.34.0",
"@octokit/types": "^6.14.1",
"deprecation": "^2.3.1"
}
},
"@octokit/request": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz",
"integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==",
"version": "5.4.15",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.4.15.tgz",
"integrity": "sha512-6UnZfZzLwNhdLRreOtTkT9n57ZwulCve8q3IT/Z477vThu6snfdkBuhxnChpOKNGxcQ71ow561Qoa6uqLdPtag==",
"requires": {
"@octokit/endpoint": "^6.0.1",
"@octokit/request-error": "^2.1.0",
"@octokit/types": "^6.16.1",
"@octokit/request-error": "^2.0.0",
"@octokit/types": "^6.7.1",
"is-plain-object": "^5.0.0",
"node-fetch": "^2.6.7",
"node-fetch": "^2.6.1",
"universal-user-agent": "^6.0.0"
}
},
"@octokit/request-error": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz",
"integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==",
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.0.5.tgz",
"integrity": "sha512-T/2wcCFyM7SkXzNoyVNWjyVlUwBvW3igM3Btr/eKYiPmucXTtkxt2RBsf6gn3LTzaLSLTQtNmvg+dGsOxQrjZg==",
"requires": {
"@octokit/types": "^6.0.3",
"deprecation": "^2.0.0",
@@ -340,17 +304,17 @@
}
},
"@octokit/types": {
"version": "6.34.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.34.0.tgz",
"integrity": "sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw==",
"version": "6.14.2",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.14.2.tgz",
"integrity": "sha512-wiQtW9ZSy4OvgQ09iQOdyXYNN60GqjCL/UdMsepDr1Gr0QzpW6irIKbH3REuAHXAhxkEk9/F2a3Gcs1P6kW5jA==",
"requires": {
"@octokit/openapi-types": "^11.2.0"
"@octokit/openapi-types": "^7.0.0"
}
},
"before-after-hook": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz",
"integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ=="
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.1.tgz",
"integrity": "sha512-/6FKxSTWoJdbsLDF8tdIjaRiFXiE6UHsEHE3OPI/cwPURCVi1ukP0gmLn7XWEiFk5TcwQjjY5PWsU+j+tgXgmw=="
},
"deprecation": {
"version": "2.3.1",
@@ -363,12 +327,9 @@
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="
},
"node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"requires": {
"whatwg-url": "^5.0.0"
}
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
},
"once": {
"version": "1.4.0",
@@ -378,11 +339,6 @@
"wrappy": "1"
}
},
"tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
},
"tunnel": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
@@ -393,20 +349,6 @@
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
},
"webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
},
"whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
"requires": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",

View File

@@ -10,7 +10,7 @@
"author": "chris48s",
"license": "CC0",
"dependencies": {
"@actions/core": "^1.9.0",
"@actions/github": "^5.0.3"
"@actions/core": "^1.5.0",
"@actions/github": "^5.0.0"
}
}

View File

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

View File

@@ -10,7 +10,7 @@ jobs:
if: github.actor == 'dependabot[bot]'
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v2
- name: Install action dependencies
run: cd .github/actions/close-bot && npm ci

View File

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

View File

@@ -4,9 +4,6 @@ on:
pull_request:
types: [closed]
permissions:
contents: write
jobs:
create-release:
if: |
@@ -23,7 +20,7 @@ jobs:
run: echo "::set-output name=date::$(date --rfc-3339=date)"
- name: Checkout branch "master"
uses: actions/checkout@v3
uses: actions/checkout@v2
with:
ref: 'master'
@@ -34,19 +31,17 @@ jobs:
tag: server-${{ steps.date.outputs.date }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v2
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push snapshot release to DockerHub
uses: docker/build-push-action@v3
uses: docker/build-push-action@v2
with:
context: .
push: true
tags: shieldsio/shields:server-${{ steps.date.outputs.date }}
build-args: |
version=server-${{ steps.date.outputs.date }}

View File

@@ -3,16 +3,12 @@ on:
push:
branches:
- master
permissions:
contents: write
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v2.3.1
with:
persist-credentials: false
@@ -22,8 +18,9 @@ jobs:
npm run build-docs
- name: Deploy
uses: JamesIves/github-pages-deploy-action@v4
uses: JamesIves/github-pages-deploy-action@3.7.1
with:
branch: gh-pages
folder: api-docs
clean: true
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: gh-pages
FOLDER: api-docs
CLEAN: true

View File

@@ -5,16 +5,12 @@ on:
# At 01:00 on the first day of every month
workflow_dispatch:
permissions:
pull-requests: write
contents: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v2
- name: Draft Release
uses: ./.github/actions/draft-release

View File

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

View File

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

2
.npmrc
View File

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

View File

@@ -4,165 +4,6 @@ Note: this changelog is for the shields.io server. The changelog for the badge-m
---
## server-2022-08-01
- [pypi] Add Framework Version Badges support [#8261](https://github.com/badges/shields/issues/8261)
- feat: add [GitlabForks] server [#8208](https://github.com/badges/shields/issues/8208)
- Update PyPI api according to https://warehouse.pypa.io/api-reference/json.html [#8251](https://github.com/badges/shields/issues/8251)
- Add [galaxytoolshed] Activity [#8164](https://github.com/badges/shields/issues/8164)
- [greasyfork] Add Greasy Fork rating badges [#8087](https://github.com/badges/shields/issues/8087)
- refactor(deps): Replace moment with dayjs [#8192](https://github.com/badges/shields/issues/8192)
- add spaces round pipe in [conda] badge [#8189](https://github.com/badges/shields/issues/8189)
- Add [ROS] version service [#8169](https://github.com/badges/shields/issues/8169)
- feat: add [gitlabissues] service [#8108](https://github.com/badges/shields/issues/8108)
- Dependency updates
## server-2022-07-03
- Add [galaxytoolshed] services [#8114](https://github.com/badges/shields/issues/8114)
- fix [gitlab] auth [#8145](https://github.com/badges/shields/issues/8145) [#8162](https://github.com/badges/shields/issues/8162)
- increase cache length on AUR version badge, run [AUR] [#8110](https://github.com/badges/shields/issues/8110)
- Use GraphQL to fix GitHub file count badges [github] [#8112](https://github.com/badges/shields/issues/8112)
- feat: add [gitlab] contributors service [#8084](https://github.com/badges/shields/issues/8084)
- [greasyfork] Add Greasy Fork service badges [#8080](https://github.com/badges/shields/issues/8080)
- Add [gitlablicense] services [#8024](https://github.com/badges/shields/issues/8024)
- [Spack] Package Manager: Update Domain [#8046](https://github.com/badges/shields/issues/8046)
- switch [jitpack] to use latestOk endpoint [#8041](https://github.com/badges/shields/issues/8041)
- Dependency updates
## server-2022-06-01
- Update GitLab logo (2022) [#7984](https://github.com/badges/shields/issues/7984)
- [GitHub] Added milestone property to GitHub issue details service [#7864](https://github.com/badges/shields/issues/7864)
- [Spack] Package Manager: Update Endpoint [#7957](https://github.com/badges/shields/issues/7957)
- Update Chocolatey API endpoint URL [#7952](https://github.com/badges/shields/issues/7952)
- [Flathub]Add downloads badge [#7724](https://github.com/badges/shields/issues/7724)
- replace the outdated Telegram logo with the newest [#7831](https://github.com/badges/shields/issues/7831)
- add [PUB] points badge [#7918](https://github.com/badges/shields/issues/7918)
- add [PUB] popularity badge [#7920](https://github.com/badges/shields/issues/7920)
- add [PUB] likes badge [#7916](https://github.com/badges/shields/issues/7916)
- Dependency updates
## server-2022-05-03
- [OSSFScorecard] Create scorecard badge service [#7687](https://github.com/badges/shields/issues/7687)
- Stringify [githublanguagecount] message [#7881](https://github.com/badges/shields/issues/7881)
- Stringify and trim whitespace from a few services [#7880](https://github.com/badges/shields/issues/7880)
- add labels to Dockerfile [#7862](https://github.com/badges/shields/issues/7862)
- handle missing 'fly-client-ip' [#7814](https://github.com/badges/shields/issues/7814)
- Dependency updates
## server-2022-04-03
- Breaking change: This release updates ioredis from v4 to v5.
If you are using redis for GitHub token pooling, redis connection strings of the form
`redis://junkusername:authpassword@example.com:1234` will need to be updated to
`redis://:authpassword@example.com:1234`. See the
[ioredis upgrade guide](https://github.com/luin/ioredis/wiki/Upgrading-from-v4-to-v5)
for further details.
- fix installation issue on npm >= 8.5.5 [#7809](https://github.com/badges/shields/issues/7809)
- two fixes for [packagist] schemas [#7782](https://github.com/badges/shields/issues/7782)
- allow requireCloudflare setting to work when hosted on fly.io [#7781](https://github.com/badges/shields/issues/7781)
- fix [pypi] badges when package has null license [#7761](https://github.com/badges/shields/issues/7761)
- Add a [pub] publisher badge [#7715](https://github.com/badges/shields/issues/7715)
- Switch Steam file size badge to informational color [#7722](https://github.com/badges/shields/issues/7722)
- Make W3C and Youtube documentation links clickable [#7721](https://github.com/badges/shields/issues/7721)
- Improve Wercker examples [#7720](https://github.com/badges/shields/issues/7720)
- Improve Cirrus CI examples [#7719](https://github.com/badges/shields/issues/7719)
- Support [CodeClimate] responses with multiple data items [#7716](https://github.com/badges/shields/issues/7716)
- Delete [TeamCityCoverage] and [BowerVersion] redirectors [#7718](https://github.com/badges/shields/issues/7718)
- Deprecate [Shippable] service [#7717](https://github.com/badges/shields/issues/7717)
- fix: restore version comparison updates from #4173 [#4254](https://github.com/badges/shields/issues/4254)
- [piwheels], filter out versions with no files [#7696](https://github.com/badges/shields/issues/7696)
- set a longer cacheLength on [librariesio] badges [#7692](https://github.com/badges/shields/issues/7692)
- improve python version formatting [#7682](https://github.com/badges/shields/issues/7682)
- Clarify GitHub All Contributors badge [#7690](https://github.com/badges/shields/issues/7690)
- Support [HexPM] packages with no stable release [#7685](https://github.com/badges/shields/issues/7685)
- Add Test at Scale Badge [#7612](https://github.com/badges/shields/issues/7612)
- [packagist] api v2 support [#7681](https://github.com/badges/shields/issues/7681)
- Add [piwheels] version badge [#7656](https://github.com/badges/shields/issues/7656)
- Dependency updates
## server-2022-03-01
- Add [Conan] version service (#7460)
- remove suspended [github] tokens from the pool [#7654](https://github.com/badges/shields/issues/7654)
- generate links without trailing : if port not set [#7655](https://github.com/badges/shields/issues/7655)
- Use the latest build status when checking docs.rs [#7613](https://github.com/badges/shields/issues/7613)
- Remove no download handling and add API warning to [Wordpress] badges [#7606](https://github.com/badges/shields/issues/7606)
- set a higher default cacheLength on rating/star category [#7587](https://github.com/badges/shields/issues/7587)
- Update [amo] to use v4 API, set custom `cacheLength`s [#7586](https://github.com/badges/shields/issues/7586)
- fix(amo): include trailing slash in API call [#7585](https://github.com/badges/shields/issues/7585)
- fix docker image user agent [#7582](https://github.com/badges/shields/issues/7582)
- Delete deprecated Codetally and continuousphp services [#7572](https://github.com/badges/shields/issues/7572)
- Deprecate [Requires] service [#7571](https://github.com/badges/shields/issues/7571)
- [AUR] Fix RPC URL [#7570](https://github.com/badges/shields/issues/7570)
- Dependency updates
## server-2022-02-01
- [Depfu] Add support for Gitlab [#7475](https://github.com/badges/shields/issues/7475)
- replace label in hn-user-karma with U/ [#7500](https://github.com/badges/shields/issues/7500)
- Support [Feedz] response with multiple pages without items [#7476](https://github.com/badges/shields/issues/7476)
- revert decamelize and humanize-string to old versions [#7449](https://github.com/badges/shields/issues/7449)
- Dependency updates
## server-2022-01-01
- minor [reddit] improvements [#7436](https://github.com/badges/shields/issues/7436)
- [HackerNews] Show User Karma [#7411](https://github.com/badges/shields/issues/7411)
- [YouTube] Drop support for removed dislikes [#7410](https://github.com/badges/shields/issues/7410)
- change closed GitHub issue color to purple [#7374](https://github.com/badges/shields/issues/7374)
- restore cors header injection from #4171 [#4255](https://github.com/badges/shields/issues/4255)
- [GithubPackageJson] Get version from monorepo subfolder package.json [#7350](https://github.com/badges/shields/issues/7350)
- Dependency updates
## server-2021-12-01
- Send better user-agent values [#7309](https://github.com/badges/shields/issues/7309)
Self-hosting users now send a user agent which indicates the server version and starts `shields (self-hosted)/` by default.
This can be configured using the env var `USER_AGENT_BASE`
- upgrade to node 16 [#7271](https://github.com/badges/shields/issues/7271)
- feat: deprecate dependabot badges [#7274](https://github.com/badges/shields/issues/7274)
- fix: npmversion tagged service test [#7269](https://github.com/badges/shields/issues/7269)
- feat: create new Test Results category [#7218](https://github.com/badges/shields/issues/7218)
- Migration from Request to Got for all HTTP requests is completed in this release
- Dependency updates
## server-2021-11-04
- migrate regularUpdate() from request-->got [#7215](https://github.com/badges/shields/issues/7215)
- migrate github badges to use got instead of request; affects [github librariesio] [#7212](https://github.com/badges/shields/issues/7212)
- deprecate David badges [#7197](https://github.com/badges/shields/issues/7197)
- fix: ensure libraries.io header values are processed numerically [#7196](https://github.com/badges/shields/issues/7196)
- Add authentication for Libraries.io-based badges, run [Libraries Bower] [#7080](https://github.com/badges/shields/issues/7080)
- fixes and tests for pipenv helpers [#7194](https://github.com/badges/shields/issues/7194)
- add GitLab Release badge, run all [GitLab] [#7021](https://github.com/badges/shields/issues/7021)
- set content-length header on badge responses [#7179](https://github.com/badges/shields/issues/7179)
- fix [github] release/tag/download schema [#7170](https://github.com/badges/shields/issues/7170)
- Supported nested groups on [GitLabPipeline] badge [#7159](https://github.com/badges/shields/issues/7159)
- Support nested groups on [GitLabTag] badge [#7158](https://github.com/badges/shields/issues/7158)
- Fixing incorrect JetBrains Plugin rating values for [JetBrainsRating] [#7140](https://github.com/badges/shields/issues/7140)
- support using release or tag name in [GitHub] Release version badge [#7075](https://github.com/badges/shields/issues/7075)
- feat: support branches in sonar badges [#7065](https://github.com/badges/shields/issues/7065)
- Add [Modrinth] total downloads badge [#7132](https://github.com/badges/shields/issues/7132)
- remove [github] admin routes [#7105](https://github.com/badges/shields/issues/7105)
- Dependency updates
## server-2021-10-04
- feat: add 2021 support to GitHub Hacktoberfest [#7086](https://github.com/badges/shields/issues/7086)
- Add [ClearlyDefined] service [#6944](https://github.com/badges/shields/issues/6944)
- handle null licenses in crates.io response schema, run [crates] [#7074](https://github.com/badges/shields/issues/7074)
- [OBS] add Open Build Service service-badge [#6993](https://github.com/badges/shields/issues/6993)
- Correction of badges url in self-hosting configuration with a custom port. Issue 7025 [#7036](https://github.com/badges/shields/issues/7036)
- fix: support gitlab token via env var [#7023](https://github.com/badges/shields/issues/7023)
- Add API-based support for [GitLab] badges, add new GitLab Tag badge [#6988](https://github.com/badges/shields/issues/6988)
- [freecodecamp]: allow + symbol in username [#7016](https://github.com/badges/shields/issues/7016)
- Rename Riot to Element in Matrix badge help [#6996](https://github.com/badges/shields/issues/6996)
- Fixed Reddit Negative Karma Issue [#6992](https://github.com/badges/shields/issues/6992)
- Dependency updates
## server-2021-09-01
- use multi-stage build to reduce size of docker images [#6938](https://github.com/badges/shields/issues/6938)

View File

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

View File

@@ -35,7 +35,7 @@ and legible badges in SVG and raster format, which can easily be included in
GitHub readmes or any other web page. The service supports dozens of
continuous integration services, package registries, distributions, app
stores, social networks, code coverage services, and code analysis services.
Every month it serves over 870 million images and is used by some of the
Every month it serves over 770 million images and is used by some of the
world's most popular open-source projects, [VS Code][vscode], [Vue.js][vue]
and [Bootstrap][bootstrap] to name a few.
@@ -101,8 +101,8 @@ You can read a [tutorial on how to add a badge][tutorial].
## Development
1. Install Node 16 or later. You can use the [package manager][] of your choice.
Tests need to pass in Node 16 and 17.
1. Install Node 14 or later. You can use the [package manager][] of your choice.
Tests need to pass in Node 14 and 16.
2. Clone this repository.
3. Run `npm ci` to install the dependencies.
4. Run `npm start` to start the badge server and the frontend dev server.

View File

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

View File

@@ -35,16 +35,6 @@
"WEBLATE_API_KEY": {
"description": "Configure the API key to be used for the Weblate service.",
"required": false
},
"METRICS_INFLUX_ENABLED": {
"description": "Disable influx metrics",
"value": "false",
"required": false
},
"REQUIRE_CLOUDFLARE": {
"description": "Allow direct traffic",
"value": "false",
"required": false
}
},
"formation": {

View File

@@ -33,7 +33,7 @@ class XmlElement {
* @param {object} attrs Refer to individual attrs
* @param {string} attrs.name
* Name of the XML tag
* @param {Array.<string|module:badge-maker/lib/xml~XmlElement>} [attrs.content=[]]
* @param {Array.<string|module:badge-maker/lib/xml-element~XmlElement>} [attrs.content=[]]
* Array of objects to render inside the tag. content may contain a mix of
* string and XmlElement objects. If content is `[]` or ommitted the
* element will be rendered as a self-closing element.

View File

@@ -50,8 +50,6 @@ public:
authorizedOrigins: 'NEXUS_ORIGINS'
npm:
authorizedOrigins: 'NPM_ORIGINS'
obs:
authorizedOrigins: 'OBS_ORIGINS'
sonar:
authorizedOrigins: 'SONAR_ORIGINS'
teamcity:
@@ -64,7 +62,6 @@ public:
defaultCacheLengthSeconds: 'BADGE_MAX_AGE_SECONDS'
fetchLimit: 'FETCH_LIMIT'
userAgentBase: 'USER_AGENT_BASE'
requestTimeoutSeconds: 'REQUEST_TIMEOUT_SECONDS'
requestTimeoutMaxAgeSeconds: 'REQUEST_TIMEOUT_MAX_AGE_SECONDS'
@@ -87,14 +84,12 @@ private:
jenkins_pass: 'JENKINS_PASS'
jira_user: 'JIRA_USER'
jira_pass: 'JIRA_PASS'
librariesio_tokens: 'LIBRARIESIO_TOKENS'
nexus_user: 'NEXUS_USER'
nexus_pass: 'NEXUS_PASS'
npm_token: 'NPM_TOKEN'
obs_user: 'OBS_USER'
obs_pass: 'OBS_PASS'
redis_url: 'REDIS_URL'
sentry_dsn: 'SENTRY_DSN'
shields_secret: 'SHIELDS_SECRET'
sl_insight_userUuid: 'SL_INSIGHT_USER_UUID'
sl_insight_apiToken: 'SL_INSIGHT_API_TOKEN'
sonarqube_token: 'SONARQUBE_TOKEN'

View File

@@ -22,8 +22,6 @@ public:
debug:
enabled: false
intervalSeconds: 200
obs:
authorizedOrigins: 'https://api.opensuse.org'
weblate:
authorizedOrigins: 'https://hosted.weblate.org'
trace: false
@@ -34,7 +32,6 @@ public:
handleInternalErrors: true
fetchLimit: '10MB'
userAgentBase: 'shields (self-hosted)'
requestTimeoutSeconds: 120
requestTimeoutMaxAgeSeconds: 30

View File

@@ -6,8 +6,6 @@ private:
# preferable for self hosting.
gh_token: '...'
gitlab_token: '...'
obs_user: '...'
obs_pass: '...'
twitch_client_id: '...'
twitch_client_secret: '...'
weblate_api_key: '...'

View File

@@ -6,20 +6,13 @@ public:
enabled: true
url: https://metrics.shields.io/telegraf
instanceIdFrom: env-var
instanceIdEnvVarName: FLY_ALLOC_ID
instanceIdEnvVarName: HEROKU_DYNO_ID
envLabel: shields-production
ssl:
isSecure: false
isSecure: true
cors:
allowedOrigin: ['http://shields.io', 'https://shields.io']
services:
gitlab:
authorizedOrigins: 'https://gitlab.com'
rasterUrl: 'https://raster.shields.io'
userAgentBase: 'Shields.io'
requireCloudflare: true
requestTimeoutSeconds: 20

View File

@@ -74,7 +74,7 @@ class AuthHelper {
}
static _isInsecureSslRequest({ options = {} }) {
const strictSSL = options?.https?.rejectUnauthorized ?? true
const { strictSSL = true } = options
return strictSSL !== true
}
@@ -107,10 +107,8 @@ class AuthHelper {
}
get _basicAuth() {
const { _user: username, _pass: password } = this
return this.isConfigured
? { username: username || '', password: password || '' }
: undefined
const { _user: user, _pass: pass } = this
return this.isConfigured ? { user, pass } : undefined
}
/*
@@ -133,7 +131,7 @@ class AuthHelper {
const { options, ...rest } = requestParams
return {
options: {
...auth,
auth,
...options,
},
...rest,
@@ -183,13 +181,11 @@ class AuthHelper {
}
static _mergeQueryParams(requestParams, query) {
const {
options: { searchParams: existingQuery, ...restOptions } = {},
...rest
} = requestParams
const { options: { qs: existingQuery, ...restOptions } = {}, ...rest } =
requestParams
return {
options: {
searchParams: {
qs: {
...existingQuery,
...query,
},

View File

@@ -104,14 +104,14 @@ describe('AuthHelper', function () {
{ userKey: 'myci_user', passKey: 'myci_pass' },
{ myci_user: 'admin', myci_pass: 'abc123' }
),
]).expect({ username: 'admin', password: 'abc123' })
]).expect({ user: 'admin', pass: 'abc123' })
given({ userKey: 'myci_user' }, { myci_user: 'admin' }).expect({
username: 'admin',
password: '',
user: 'admin',
pass: undefined,
})
given({ passKey: 'myci_pass' }, { myci_pass: 'abc123' }).expect({
username: '',
password: 'abc123',
user: undefined,
pass: 'abc123',
})
given({ userKey: 'myci_user', passKey: 'myci_pass' }, {}).expect(
undefined
@@ -120,8 +120,8 @@ describe('AuthHelper', function () {
{ passKey: 'myci_pass', defaultToEmptyStringForUser: true },
{ myci_pass: 'abc123' }
).expect({
username: '',
password: 'abc123',
user: '',
pass: 'abc123',
})
})
})
@@ -131,18 +131,15 @@ describe('AuthHelper', function () {
forCases([
given({ url: 'http://example.test' }),
given({ url: 'http://example.test', options: {} }),
given({ url: 'http://example.test', options: { strictSSL: true } }),
given({
url: 'http://example.test',
options: { https: { rejectUnauthorized: true } },
}),
given({
url: 'http://example.test',
options: { https: { rejectUnauthorized: undefined } },
options: { strictSSL: undefined },
}),
]).expect(false)
given({
url: 'http://example.test',
options: { https: { rejectUnauthorized: false } },
options: { strictSSL: false },
}).expect(true)
})
})
@@ -166,9 +163,7 @@ describe('AuthHelper', function () {
})
it('throws for insecure requests', function () {
expect(() =>
authHelper.enforceStrictSsl({
options: { https: { rejectUnauthorized: false } },
})
authHelper.enforceStrictSsl({ options: { strictSSL: false } })
).to.throw(InvalidParameter)
})
})
@@ -190,9 +185,7 @@ describe('AuthHelper', function () {
})
it('does not throw for insecure requests', function () {
expect(() =>
authHelper.enforceStrictSsl({
options: { https: { rejectUnauthorized: false } },
})
authHelper.enforceStrictSsl({ options: { strictSSL: false } })
).not.to.throw()
})
})
@@ -227,7 +220,7 @@ describe('AuthHelper', function () {
test(shouldAuthenticateRequest, () => {
given({
url: 'https://myci.test/api',
options: { https: { rejectUnauthorized: false } },
options: { strictSSL: false },
}).expect(false)
})
})
@@ -265,7 +258,7 @@ describe('AuthHelper', function () {
test(shouldAuthenticateRequest, () => {
given({
url: 'https://myci.test',
options: { https: { rejectUnauthorized: false } },
options: { strictSSL: false },
}).expect(true)
})
})
@@ -330,8 +323,7 @@ describe('AuthHelper', function () {
}).expect({
url: 'https://myci.test/api',
options: {
username: 'admin',
password: 'abc123',
auth: { user: 'admin', pass: 'abc123' },
},
})
given({
@@ -343,8 +335,7 @@ describe('AuthHelper', function () {
url: 'https://myci.test/api',
options: {
headers: { Accept: 'application/json' },
username: 'admin',
password: 'abc123',
auth: { user: 'admin', pass: 'abc123' },
},
})
})
@@ -375,7 +366,7 @@ describe('AuthHelper', function () {
expect(() =>
withBasicAuth({
url: 'https://myci.test/api',
options: { https: { rejectUnauthorized: false } },
options: { strictSSL: false },
})
).to.throw(InvalidParameter)
})

View File

@@ -38,8 +38,8 @@ class BaseGraphqlService extends BaseService {
* representing the query clause of GraphQL POST body
* e.g. gql`{ query { ... } }`
* @param {object} attrs.variables Variables clause of GraphQL POST body
* @param {object} [attrs.options={}] Options to pass to got. See
* [documentation](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md)
* @param {object} [attrs.options={}] Options to pass to request. See
* [documentation](https://github.com/request/request#requestoptions-callback)
* @param {object} [attrs.httpErrorMessages={}] Key-value map of HTTP status codes
* and custom error messages e.g: `{ 404: 'package not found' }`.
* This can be used to extend or override the
@@ -53,7 +53,7 @@ class BaseGraphqlService extends BaseService {
* The default is to return the first entry of the `errors` array as
* an InvalidResponse.
* @returns {object} Parsed response
* @see https://github.com/sindresorhus/got/blob/main/documentation/2-options.md
* @see https://github.com/request/request#requestoptions-callback
*/
async _requestGraphql({
schema,

View File

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

View File

@@ -28,14 +28,14 @@ class BaseJsonService extends BaseService {
* @param {object} attrs Refer to individual attrs
* @param {Joi} attrs.schema Joi schema to validate the response against
* @param {string} attrs.url URL to request
* @param {object} [attrs.options={}] Options to pass to got. See
* [documentation](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md)
* @param {object} [attrs.options={}] Options to pass to request. See
* [documentation](https://github.com/request/request#requestoptions-callback)
* @param {object} [attrs.errorMessages={}] Key-value map of status codes
* and custom error messages e.g: `{ 404: 'package not found' }`.
* This can be used to extend or override the
* [default](https://github.com/badges/shields/blob/master/core/base-service/check-error-response.js#L5)
* @returns {object} Parsed response
* @see https://github.com/sindresorhus/got/blob/main/documentation/2-options.md
* @see https://github.com/request/request#requestoptions-callback
*/
async _requestJson({ schema, url, options = {}, errorMessages = {} }) {
const mergedOptions = {

View File

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

View File

@@ -51,14 +51,14 @@ class BaseSvgScrapingService extends BaseService {
* @param {RegExp} attrs.valueMatcher
* RegExp to match the value we want to parse from the SVG
* @param {string} attrs.url URL to request
* @param {object} [attrs.options={}] Options to pass to got. See
* [documentation](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md)
* @param {object} [attrs.options={}] Options to pass to request. See
* [documentation](https://github.com/request/request#requestoptions-callback)
* @param {object} [attrs.errorMessages={}] Key-value map of status codes
* and custom error messages e.g: `{ 404: 'package not found' }`.
* This can be used to extend or override the
* [default](https://github.com/badges/shields/blob/master/core/base-service/check-error-response.js#L5)
* @returns {object} Parsed response
* @see https://github.com/sindresorhus/got/blob/main/documentation/2-options.md
* @see https://github.com/request/request#requestoptions-callback
*/
async _requestSvg({
schema,

View File

@@ -34,9 +34,9 @@ describe('BaseSvgScrapingService', function () {
})
describe('Making requests', function () {
let requestFetcher
let sendAndCacheRequest
beforeEach(function () {
requestFetcher = sinon.stub().returns(
sendAndCacheRequest = sinon.stub().returns(
Promise.resolve({
buffer: exampleSvg,
res: { statusCode: 200 },
@@ -44,13 +44,13 @@ describe('BaseSvgScrapingService', function () {
)
})
it('invokes _requestFetcher with the expected header', async function () {
it('invokes _sendAndCacheRequest with the expected header', async function () {
await DummySvgScrapingService.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
expect(requestFetcher).to.have.been.calledOnceWith(
expect(sendAndCacheRequest).to.have.been.calledOnceWith(
'http://example.com/foo.svg',
{
headers: { Accept: 'image/svg+xml' },
@@ -58,7 +58,7 @@ describe('BaseSvgScrapingService', function () {
)
})
it('forwards options to _requestFetcher', async function () {
it('forwards options to _sendAndCacheRequest', async function () {
class WithCustomOptions extends DummySvgScrapingService {
async handle() {
const { message } = await this._requestSvg({
@@ -66,7 +66,7 @@ describe('BaseSvgScrapingService', function () {
url: 'http://example.com/foo.svg',
options: {
method: 'POST',
searchParams: { queryParam: 123 },
qs: { queryParam: 123 },
},
})
return { message }
@@ -74,16 +74,16 @@ describe('BaseSvgScrapingService', function () {
}
await WithCustomOptions.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
expect(requestFetcher).to.have.been.calledOnceWith(
expect(sendAndCacheRequest).to.have.been.calledOnceWith(
'http://example.com/foo.svg',
{
method: 'POST',
headers: { Accept: 'image/svg+xml' },
searchParams: { queryParam: 123 },
qs: { queryParam: 123 },
}
)
})
@@ -91,13 +91,13 @@ describe('BaseSvgScrapingService', function () {
describe('Making badges', function () {
it('handles valid svg responses', async function () {
const requestFetcher = async () => ({
const sendAndCacheRequest = async () => ({
buffer: exampleSvg,
res: { statusCode: 200 },
})
expect(
await DummySvgScrapingService.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
).to.deep.equal({
@@ -117,13 +117,13 @@ describe('BaseSvgScrapingService', function () {
})
}
}
const requestFetcher = async () => ({
const sendAndCacheRequest = async () => ({
buffer: '<desc>a different message</desc>',
res: { statusCode: 200 },
})
expect(
await WithValueMatcher.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
).to.deep.equal({
@@ -132,13 +132,13 @@ describe('BaseSvgScrapingService', function () {
})
it('handles unparseable svg responses', async function () {
const requestFetcher = async () => ({
const sendAndCacheRequest = async () => ({
buffer: 'not svg yo',
res: { statusCode: 200 },
})
expect(
await DummySvgScrapingService.invoke(
{ requestFetcher },
{ sendAndCacheRequest },
{ handleInternalErrors: false }
)
).to.deep.equal({

View File

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

View File

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

View File

@@ -21,15 +21,15 @@ class BaseYamlService extends BaseService {
* @param {object} attrs Refer to individual attrs
* @param {Joi} attrs.schema Joi schema to validate the response against
* @param {string} attrs.url URL to request
* @param {object} [attrs.options={}] Options to pass to got. See
* [documentation](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md)
* @param {object} [attrs.options={}] Options to pass to request. See
* [documentation](https://github.com/request/request#requestoptions-callback)
* @param {object} [attrs.errorMessages={}] Key-value map of status codes
* and custom error messages e.g: `{ 404: 'package not found' }`.
* This can be used to extend or override the
* [default](https://github.com/badges/shields/blob/master/core/base-service/check-error-response.js#L5)
* @param {object} [attrs.encoding='utf8'] Character encoding
* @returns {object} Parsed response
* @see https://github.com/sindresorhus/got/blob/main/documentation/2-options.md
* @see https://github.com/request/request#requestoptions-callback
*/
async _requestYaml({
schema,

View File

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

View File

@@ -20,7 +20,7 @@ import {
Deprecated,
} from './errors.js'
import { validateExample, transformExample } from './examples.js'
import { fetch } from './got.js'
import { fetchFactory } from './got.js'
import {
makeFullUrl,
assertValidRoute,
@@ -108,14 +108,11 @@ class BaseService {
*
* See also the config schema in `./server.js` and `doc/server-secrets.md`.
*
* To use the configured auth in the handler or fetch method, wrap the
* _request() input params in a call to one of:
* - this.authHelper.withBasicAuth()
* - this.authHelper.withBearerAuthHeader()
* - this.authHelper.withQueryStringAuth()
*
* For example:
* this._request(this.authHelper.withBasicAuth({ url, schema, options }))
* To use the configured auth in the handler or fetch method, pass the
* credentials to the request. For example:
* - `{ options: { auth: this.authHelper.basicAuth } }`
* - `{ options: { headers: this.authHelper.bearerAuthHeader } }`
* - `{ options: { qs: { token: this.authHelper._pass } } }`
*
* @abstract
* @type {module:core/base-service/base~Auth}
@@ -147,7 +144,6 @@ class BaseService {
version: 300,
debug: 60,
downloads: 900,
rating: 900,
social: 900,
}
return cacheLengths[this.category]
@@ -208,10 +204,10 @@ class BaseService {
}
constructor(
{ requestFetcher, authHelper, metricHelper },
{ sendAndCacheRequest, authHelper, metricHelper },
{ handleInternalErrors }
) {
this._requestFetcher = requestFetcher
this._requestFetcher = sendAndCacheRequest
this.authHelper = authHelper
this._handleInternalErrors = handleInternalErrors
this._metricHelper = metricHelper
@@ -221,10 +217,10 @@ class BaseService {
const logTrace = (...args) => trace.logTrace('fetch', ...args)
let logUrl = url
const logOptions = Object.assign({}, options)
if ('searchParams' in options) {
const params = new URLSearchParams(options.searchParams)
if ('qs' in options) {
const params = new URLSearchParams(options.qs)
logUrl = `${url}?${params.toString()}`
delete logOptions.searchParams
delete logOptions.qs
}
logTrace(
emojic.bowAndArrow,
@@ -279,7 +275,7 @@ class BaseService {
/**
* Asynchronous function to handle requests for this service. Take the route
* parameters (as defined in the `route` property), perform a request using
* `this._requestFetcher`, and return the badge data.
* `this._sendAndCacheRequest`, and return the badge data.
*
* @abstract
* @param {object} namedParams Params parsed from route pattern
@@ -424,16 +420,10 @@ class BaseService {
}
static register(
{
camp,
handleRequest,
githubApiProvider,
librariesIoApiProvider,
metricInstance,
},
{ camp, handleRequest, githubApiProvider, metricInstance },
serviceConfig
) {
const { cacheHeaders: cacheHeaderConfig } = serviceConfig
const { cacheHeaders: cacheHeaderConfig, fetchLimitBytes } = serviceConfig
const { regex, captureNames } = prepareRoute(this.route)
const queryParams = getQueryParamNames(this.route)
@@ -442,19 +432,21 @@ class BaseService {
ServiceClass: this,
})
const fetcher = fetchFactory(fetchLimitBytes)
camp.route(
regex,
handleRequest(cacheHeaderConfig, {
queryParams,
handler: async (queryParams, match, sendBadge) => {
handler: async (queryParams, match, sendBadge, request) => {
const metricHandle = metricHelper.startRequest()
const namedParams = namedParamsForMatch(captureNames, match, this)
const serviceData = await this.invoke(
{
requestFetcher: fetch,
sendAndCacheRequest: fetcher,
sendAndCacheRequestWithCallbacks: request,
githubApiProvider,
librariesIoApiProvider,
metricHelper,
},
serviceConfig,
@@ -475,6 +467,7 @@ class BaseService {
metricHandle.noteResponseSent()
},
cacheLength: this._cacheLength,
fetchLimitBytes,
})
)
}

View File

@@ -124,11 +124,15 @@ describe('BaseService', function () {
})
describe('Logging', function () {
let sandbox
beforeEach(function () {
sinon.stub(trace, 'logTrace')
sandbox = sinon.createSandbox()
})
afterEach(function () {
sinon.restore()
sandbox.restore()
})
beforeEach(function () {
sandbox.stub(trace, 'logTrace')
})
it('Invokes the logger as expected', async function () {
await DummyService.invoke(
@@ -422,20 +426,24 @@ describe('BaseService', function () {
})
describe('request', function () {
let sandbox
beforeEach(function () {
sinon.stub(trace, 'logTrace')
sandbox = sinon.createSandbox()
})
afterEach(function () {
sinon.restore()
sandbox.restore()
})
beforeEach(function () {
sandbox.stub(trace, 'logTrace')
})
it('logs appropriate information', async function () {
const requestFetcher = async () => ({
const sendAndCacheRequest = async () => ({
buffer: '',
res: { statusCode: 200 },
})
const serviceInstance = new DummyService(
{ requestFetcher },
{ sendAndCacheRequest },
defaultConfig
)
@@ -458,12 +466,12 @@ describe('BaseService', function () {
})
it('handles errors', async function () {
const requestFetcher = async () => ({
const sendAndCacheRequest = async () => ({
buffer: '',
res: { statusCode: 404 },
})
const serviceInstance = new DummyService(
{ requestFetcher },
{ sendAndCacheRequest },
defaultConfig
)
@@ -490,13 +498,13 @@ describe('BaseService', function () {
metricInstance: new PrometheusMetrics({ register }),
ServiceClass: DummyServiceWithServiceResponseSizeMetricEnabled,
})
const requestFetcher = async () => ({
const sendAndCacheRequest = async () => ({
buffer: 'x'.repeat(65536 + 1),
res: { statusCode: 200 },
})
const serviceInstance =
new DummyServiceWithServiceResponseSizeMetricEnabled(
{ requestFetcher, metricHelper },
{ sendAndCacheRequest, metricHelper },
defaultConfig
)
@@ -516,12 +524,12 @@ describe('BaseService', function () {
metricInstance: new PrometheusMetrics({ register }),
ServiceClass: DummyService,
})
const requestFetcher = async () => ({
const sendAndCacheRequest = async () => ({
buffer: 'x',
res: { statusCode: 200 },
})
const serviceInstance = new DummyService(
{ requestFetcher, metricHelper },
{ sendAndCacheRequest, metricHelper },
defaultConfig
)

View File

@@ -99,11 +99,14 @@ describe('Cache header functions', function () {
})
describe('setHeadersForCacheLength', function () {
let sandbox
beforeEach(function () {
sinon.useFakeTimers()
sandbox = sinon.createSandbox()
sandbox.useFakeTimers()
})
afterEach(function () {
sinon.restore()
sandbox.restore()
sandbox = undefined
})
it('should set the correct Date header', function () {

View File

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

View File

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

View File

@@ -1,23 +1,61 @@
import got, { CancelError } from 'got'
import got from 'got'
import { Inaccessible, InvalidResponse } from './errors.js'
import {
fetchLimitBytes as fetchLimitBytesDefault,
getUserAgent,
} from './got-config.js'
const userAgent = getUserAgent()
const userAgent = 'Shields.io/2003a'
function requestOptions2GotOptions(options) {
const requestOptions = Object.assign({}, options)
const gotOptions = {}
const interchangableOptions = ['body', 'form', 'headers', 'method', 'url']
interchangableOptions.forEach(function (opt) {
if (opt in requestOptions) {
gotOptions[opt] = requestOptions[opt]
delete requestOptions[opt]
}
})
if ('qs' in requestOptions) {
gotOptions.searchParams = requestOptions.qs
delete requestOptions.qs
}
if ('gzip' in requestOptions) {
gotOptions.decompress = requestOptions.gzip
delete requestOptions.gzip
}
if ('strictSSL' in requestOptions) {
gotOptions.https = {
rejectUnauthorized: requestOptions.strictSSL,
}
delete requestOptions.strictSSL
}
if ('auth' in requestOptions) {
gotOptions.username = requestOptions.auth.user
gotOptions.password = requestOptions.auth.pass
delete requestOptions.auth
}
if (Object.keys(requestOptions).length > 0) {
throw new Error(`Found unrecognised options ${Object.keys(requestOptions)}`)
}
return gotOptions
}
async function sendRequest(gotWrapper, url, options) {
const gotOptions = Object.assign({}, options)
const gotOptions = requestOptions2GotOptions(options)
gotOptions.throwHttpErrors = false
gotOptions.retry = { limit: 0 }
gotOptions.retry = 0
gotOptions.headers = gotOptions.headers || {}
gotOptions.headers['User-Agent'] = userAgent
try {
const resp = await gotWrapper(url, gotOptions)
return { res: resp, buffer: resp.body }
} catch (err) {
if (err instanceof CancelError) {
if (err instanceof got.CancelError) {
throw new InvalidResponse({
underlyingError: new Error('Maximum response size exceeded'),
})
@@ -26,7 +64,7 @@ async function sendRequest(gotWrapper, url, options) {
}
}
function _fetchFactory(fetchLimitBytes = fetchLimitBytesDefault) {
function fetchFactory(fetchLimitBytes) {
const gotWithLimit = got.extend({
handlers: [
(options, next) => {
@@ -55,6 +93,4 @@ function _fetchFactory(fetchLimitBytes = fetchLimitBytesDefault) {
return sendRequest.bind(sendRequest, gotWithLimit)
}
const fetch = _fetchFactory()
export { fetch, _fetchFactory }
export { requestOptions2GotOptions, fetchFactory }

View File

@@ -1,15 +1,50 @@
import { expect } from 'chai'
import nock from 'nock'
import { _fetchFactory } from './got.js'
import { requestOptions2GotOptions, fetchFactory } from './got.js'
import { Inaccessible, InvalidResponse } from './errors.js'
describe('requestOptions2GotOptions function', function () {
it('translates valid options', function () {
expect(
requestOptions2GotOptions({
body: 'body',
form: 'form',
headers: 'headers',
method: 'method',
url: 'url',
qs: 'qs',
gzip: 'gzip',
strictSSL: 'strictSSL',
auth: { user: 'user', pass: 'pass' },
})
).to.deep.equal({
body: 'body',
form: 'form',
headers: 'headers',
method: 'method',
url: 'url',
searchParams: 'qs',
decompress: 'gzip',
https: { rejectUnauthorized: 'strictSSL' },
username: 'user',
password: 'pass',
})
})
it('throws if unrecognised options are found', function () {
expect(() =>
requestOptions2GotOptions({ body: 'body', foobar: 'foobar' })
).to.throw(Error, 'Found unrecognised options foobar')
})
})
describe('got wrapper', function () {
it('should not throw an error if the response <= fetchLimitBytes', async function () {
nock('https://www.google.com')
.get('/foo/bar')
.once()
.reply(200, 'x'.repeat(100))
const sendRequest = _fetchFactory(100)
const sendRequest = fetchFactory(100)
const { res } = await sendRequest('https://www.google.com/foo/bar')
expect(res.statusCode).to.equal(200)
})
@@ -19,7 +54,7 @@ describe('got wrapper', function () {
.get('/foo/bar')
.once()
.reply(200, 'x'.repeat(101))
const sendRequest = _fetchFactory(100)
const sendRequest = fetchFactory(100)
return expect(
sendRequest('https://www.google.com/foo/bar')
).to.be.rejectedWith(InvalidResponse, 'Maximum response size exceeded')
@@ -27,7 +62,7 @@ describe('got wrapper', function () {
it('should throw an Inaccessible error if the request throws a (non-HTTP) error', async function () {
nock('https://www.google.com').get('/foo/bar').replyWithError('oh no')
const sendRequest = _fetchFactory(1024)
const sendRequest = fetchFactory(1024)
return expect(
sendRequest('https://www.google.com/foo/bar')
).to.be.rejectedWith(Inaccessible, 'oh no')
@@ -36,7 +71,7 @@ describe('got wrapper', function () {
it('should throw an Inaccessible error if the host can not be accessed', async function () {
this.timeout(5000)
nock.disableNetConnect()
const sendRequest = _fetchFactory(1024)
const sendRequest = fetchFactory(1024)
return expect(
sendRequest('https://www.google.com/foo/bar')
).to.be.rejectedWith(
@@ -49,14 +84,14 @@ describe('got wrapper', function () {
nock('https://www.google.com', {
reqheaders: {
'user-agent': function (agent) {
return agent.startsWith('shields (self-hosted)')
return agent.startsWith('Shields.io')
},
},
})
.get('/foo/bar')
.once()
.reply(200)
const sendRequest = _fetchFactory(1024)
const sendRequest = fetchFactory(1024)
await sendRequest('https://www.google.com/foo/bar')
})

View File

@@ -13,7 +13,6 @@ import {
Inaccessible,
InvalidParameter,
Deprecated,
ImproperlyConfigured,
} from './errors.js'
export {
@@ -30,6 +29,5 @@ export {
InvalidResponse,
Inaccessible,
InvalidParameter,
ImproperlyConfigured,
Deprecated,
}

View File

@@ -1,8 +1,12 @@
import request from 'request'
import makeBadge from '../../badge-maker/lib/make-badge.js'
import { setCacheHeaders } from './cache-headers.js'
import { Inaccessible, InvalidResponse, ShieldsRuntimeError } from './errors.js'
import { makeSend } from './legacy-result-sender.js'
import coalesceBadge from './coalesce-badge.js'
const userAgent = 'Shields.io/2003a'
// These query parameters are available to any badge. They are handled by
// `coalesceBadge`.
const globalQueryParams = new Set([
@@ -28,12 +32,32 @@ function flattenQueryParams(queryParams) {
return Array.from(union).sort()
}
function promisify(cachingRequest) {
return (uri, options) =>
new Promise((resolve, reject) => {
cachingRequest(uri, options, (err, res, buffer) => {
if (err) {
if (err instanceof ShieldsRuntimeError) {
reject(err)
} else {
// Wrap the error in an Inaccessible so it can be identified
// by the BaseService handler.
reject(new Inaccessible({ underlyingError: err }))
}
} else {
resolve({ res, buffer })
}
})
})
}
// handlerOptions can contain:
// - handler: The service's request handler function
// - queryParams: An array of the field names of any custom query parameters
// the service uses
// - cacheLength: An optional badge or category-specific cache length
// (in number of seconds) to be used in preference to the default
// - fetchLimitBytes: A limit on the response size we're willing to parse
//
// For safety, the service must declare the query parameters it wants to use.
// Only the declared parameters (and the global parameters) are provided to
@@ -53,7 +77,8 @@ function handleRequest(cacheHeaderConfig, handlerOptions) {
}
const allowedKeys = flattenQueryParams(handlerOptions.queryParams)
const { cacheLength: serviceDefaultCacheLengthSeconds } = handlerOptions
const { cacheLength: serviceDefaultCacheLengthSeconds, fetchLimitBytes } =
handlerOptions
return (queryParams, match, end, ask) => {
/*
@@ -114,6 +139,40 @@ function handleRequest(cacheHeaderConfig, handlerOptions) {
makeSend(extension, ask.res, end)(svg)
}, 25000)
function cachingRequest(uri, options, callback) {
if (typeof options === 'function' && !callback) {
callback = options
}
if (options && typeof options === 'object') {
options.uri = uri
} else if (typeof uri === 'string') {
options = { uri }
} else {
options = uri
}
options.headers = options.headers || {}
options.headers['User-Agent'] = userAgent
let bufferLength = 0
const r = request(options, callback)
r.on('data', chunk => {
bufferLength += chunk.length
if (bufferLength > fetchLimitBytes) {
r.abort()
r.emit(
'error',
new InvalidResponse({
prettyMessage: 'Maximum response size exceeded',
})
)
}
})
}
// Wrapper around `cachingRequest` that returns a promise rather than needing
// to pass a callback.
cachingRequest.asPromise = promisify(cachingRequest)
const result = handlerOptions.handler(
filteredQueryParams,
match,
@@ -128,7 +187,8 @@ function handleRequest(cacheHeaderConfig, handlerOptions) {
const svg = makeBadge(badgeData)
setCacheHeadersOnResponse(ask.res, badgeData.cacheLengthSeconds)
makeSend(format, ask.res, end)(svg)
}
},
cachingRequest
)
// eslint-disable-next-line promise/prefer-await-to-then
if (result && result.catch) {
@@ -140,4 +200,4 @@ function handleRequest(cacheHeaderConfig, handlerOptions) {
}
}
export { handleRequest }
export { handleRequest, promisify, userAgent }

View File

@@ -1,4 +1,5 @@
import { expect } from 'chai'
import nock from 'nock'
import portfinder from 'portfinder'
import Camp from '@shields_io/camp'
import got from '../got-test-client.js'
@@ -41,6 +42,28 @@ function createFakeHandlerWithCacheLength(cacheLengthSeconds) {
}
}
function fakeHandlerWithNetworkIo(queryParams, match, sendBadge, request) {
const [, someValue, format] = match
request('https://www.google.com/foo/bar', (err, res, buffer) => {
let message
if (err) {
message = err.prettyMessage
} else {
message = someValue
}
const badgeData = coalesceBadge(
queryParams,
{
label: 'testing',
message,
format,
},
{}
)
sendBadge(format, badgeData)
})
}
describe('The request handler', function () {
let port, baseUrl
beforeEach(async function () {
@@ -110,6 +133,60 @@ describe('The request handler', function () {
})
})
describe('the response size limit', function () {
beforeEach(function () {
camp.route(
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
handleRequest(standardCacheHeaders, {
handler: fakeHandlerWithNetworkIo,
fetchLimitBytes: 100,
})
)
})
it('should not throw an error if the response <= fetchLimitBytes', async function () {
nock('https://www.google.com')
.get('/foo/bar')
.once()
.reply(200, 'x'.repeat(100))
const { statusCode, body } = await got(`${baseUrl}/testing/123.json`, {
responseType: 'json',
})
expect(statusCode).to.equal(200)
expect(body).to.deep.equal({
name: 'testing',
value: '123',
label: 'testing',
message: '123',
color: 'lightgrey',
link: [],
})
})
it('should throw an error if the response is > fetchLimitBytes', async function () {
nock('https://www.google.com')
.get('/foo/bar')
.once()
.reply(200, 'x'.repeat(101))
const { statusCode, body } = await got(`${baseUrl}/testing/123.json`, {
responseType: 'json',
})
expect(statusCode).to.equal(200)
expect(body).to.deep.equal({
name: 'testing',
value: 'Maximum response size exceeded',
label: 'testing',
message: 'Maximum response size exceeded',
color: 'lightgrey',
link: [],
})
})
afterEach(function () {
nock.cleanAll()
})
})
describe('caching', function () {
describe('standard query parameters', function () {
function register({ cacheHeaderConfig }) {

View File

@@ -11,14 +11,12 @@ function streamFromString(str) {
function sendSVG(res, askres, end) {
askres.setHeader('Content-Type', 'image/svg+xml;charset=utf-8')
askres.setHeader('Content-Length', Buffer.byteLength(res, 'utf8'))
end(null, { template: streamFromString(res) })
}
function sendJSON(res, askres, end) {
askres.setHeader('Content-Type', 'application/json')
askres.setHeader('Access-Control-Allow-Origin', '*')
askres.setHeader('Content-Length', Buffer.byteLength(res, 'utf8'))
end(null, { template: streamFromString(res) })
}

View File

@@ -1,67 +0,0 @@
/**
* @module
*/
import { InvalidResponse } from './errors.js'
import { fetch } from './got.js'
import checkErrorResponse from './check-error-response.js'
const oneDay = 24 * 3600 * 1000 // 1 day in milliseconds
// Map from URL to { timestamp: last fetch time, data: data }.
let resourceCache = Object.create(null)
/**
* Make a HTTP request using an in-memory cache
*
* @param {object} attrs Refer to individual attrs
* @param {string} attrs.url URL to request
* @param {number} attrs.ttl Number of milliseconds to keep cached value for
* @param {boolean} [attrs.json=true] True if we expect to parse the response as JSON
* @param {Function} [attrs.scraper=buffer => buffer] Function to extract value from the response
* @param {object} [attrs.options={}] Options to pass to got
* @param {Function} [attrs.requestFetcher=fetch] Custom fetch function
* @returns {*} Parsed response
*/
async function getCachedResource({
url,
ttl = oneDay,
json = true,
scraper = buffer => buffer,
options = {},
requestFetcher = fetch,
}) {
const timestamp = Date.now()
const cached = resourceCache[url]
if (cached != null && timestamp - cached.timestamp < ttl) {
return cached.data
}
const { buffer } = await checkErrorResponse({})(
await requestFetcher(url, options)
)
let reqData
if (json) {
try {
reqData = JSON.parse(buffer)
} catch (e) {
throw new InvalidResponse({
prettyMessage: 'unparseable intermediate json response',
underlyingError: e,
})
}
} else {
reqData = buffer
}
const data = scraper(reqData)
resourceCache[url] = { timestamp, data }
return data
}
function clearResourceCache() {
resourceCache = Object.create(null)
}
export { getCachedResource, clearResourceCache }

View File

@@ -1,47 +0,0 @@
import { expect } from 'chai'
import nock from 'nock'
import { getCachedResource, clearResourceCache } from './resource-cache.js'
describe('Resource Cache', function () {
beforeEach(function () {
clearResourceCache()
})
it('should use cached response if valid', async function () {
let resp
nock('https://www.foobar.com').get('/baz').reply(200, { value: 1 })
resp = await getCachedResource({ url: 'https://www.foobar.com/baz' })
expect(resp).to.deep.equal({ value: 1 })
expect(nock.isDone()).to.equal(true)
nock.cleanAll()
nock('https://www.foobar.com').get('/baz').reply(200, { value: 2 })
resp = await getCachedResource({ url: 'https://www.foobar.com/baz' })
expect(resp).to.deep.equal({ value: 1 })
expect(nock.isDone()).to.equal(false)
nock.cleanAll()
})
it('should not use cached response if expired', async function () {
let resp
nock('https://www.foobar.com').get('/baz').reply(200, { value: 1 })
resp = await getCachedResource({
url: 'https://www.foobar.com/baz',
ttl: 1,
})
expect(resp).to.deep.equal({ value: 1 })
expect(nock.isDone()).to.equal(true)
nock.cleanAll()
nock('https://www.foobar.com').get('/baz').reply(200, { value: 2 })
resp = await getCachedResource({
url: 'https://www.foobar.com/baz',
ttl: 1,
})
expect(resp).to.deep.equal({ value: 2 })
expect(nock.isDone()).to.equal(true)
nock.cleanAll()
})
})

View File

@@ -10,11 +10,15 @@ describe('validate', function () {
requiredString: Joi.string().required(),
}).required()
let sandbox
beforeEach(function () {
sinon.stub(trace, 'logTrace')
sandbox = sinon.createSandbox()
})
afterEach(function () {
sinon.restore()
sandbox.restore()
})
beforeEach(function () {
sandbox.stub(trace, 'logTrace')
})
const ErrorClass = InvalidParameter

View File

@@ -1,4 +1,4 @@
import got from 'got'
// https://github.com/nock/nock/issues/1523
export default got.extend({ retry: { limit: 0 } })
export default got.extend({ retry: 0 })

View File

@@ -0,0 +1,97 @@
import requestModule from 'request'
import { Inaccessible, InvalidResponse } from '../base-service/errors.js'
// Map from URL to { timestamp: last fetch time, data: data }.
let regularUpdateCache = Object.create(null)
// url: a string, scraper: a function that takes string data at that URL.
// interval: number in milliseconds.
// cb: a callback function that takes an error and data returned by the scraper.
//
// To use this from a service:
//
// import { promisify } from 'util'
// import { regularUpdate } from '../../core/legacy/regular-update.js'
//
// function getThing() {
// return promisify(regularUpdate)({
// url: ...,
// ...
// })
// }
//
// in handle():
//
// const thing = await getThing()
function regularUpdate(
{
url,
intervalMillis,
json = true,
scraper = buffer => buffer,
options = {},
request = requestModule,
},
cb
) {
const timestamp = Date.now()
const cached = regularUpdateCache[url]
if (cached != null && timestamp - cached.timestamp < intervalMillis) {
cb(null, cached.data)
return
}
request(url, options, (err, res, buffer) => {
if (err != null) {
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(
new InvalidResponse({
prettyMessage: 'unparseable intermediate json response',
underlyingError: e,
})
)
return
}
} else {
reqData = buffer
}
let data
try {
data = scraper(reqData)
} catch (e) {
cb(e)
return
}
regularUpdateCache[url] = { timestamp, data }
cb(null, data)
})
}
function clearRegularUpdateCache() {
regularUpdateCache = Object.create(null)
}
export { regularUpdate, clearRegularUpdateCache }

View File

@@ -16,7 +16,7 @@ export default class InfluxMetrics {
url: this._config.url,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: await this.metrics(),
timeout: { request: this._config.timeoutMillseconds },
timeout: this._config.timeoutMillseconds,
username: this._config.username,
password: this._config.password,
throwHttpErrors: false,

View File

@@ -5,7 +5,6 @@ import { expect } from 'chai'
import log from './log.js'
import InfluxMetrics from './influx-metrics.js'
import '../register-chai-plugins.spec.js'
describe('Influx metrics', function () {
const metricInstance = {
metrics() {

View File

@@ -0,0 +1,66 @@
<!doctype html><meta charset=utf-8>
<title> Shields.io Admin Monitoring Interface </title>
<style>
#monitorPlatform { display: none; }
</style>
<div id=passwordRequest>
<p> Please enter your admin secret here:
<input type=password id=secretInput>
</div>
<div id=monitorPlatform>
</div>
<script>
(function() {
let network;
const onLoad = function() {
const secretInput = document.getElementById('secretInput');
const onSecretChange = function() {
const secret = secretInput.value;
const authentication = `monitor:${secret}`;
const headers = new Headers({
Authorization: `Basic ${btoa(authentication)}`
})
fetch('/sys/network', {headers})
.then(res => res.json())
.then(networkData => {
network = networkData;
// Show monitor platform.
monitorPlatform.style.display = 'block';
passwordRequest.parentNode.removeChild(passwordRequest);
// Show logs for each server.
network.ips.forEach(ip => {
const logger = document.createElement('div');
const pre = document.createElement('pre');
logger.textContent = ip;
logger.appendChild(pre);
monitorPlatform.appendChild(logger);
// Set up the websocket.
const setUpWebsocket = () => {
const websocket = new WebSocket(
(window.location.protocol === 'http:' ? 'ws' : 'wss') + '://' +
ip + ':' + window.location.port + '/sys/logs');
websocket.addEventListener('message', event => {
pre.textContent += event.data + '\n';
});
websocket.addEventListener('close', () => {
setTimeout(setUpWebsocket, 100);
});
websocket.addEventListener('open', () => {
websocket.send(JSON.stringify({secret}));
});
};
setUpWebsocket();
});
})
.catch(alert)
};
secretInput.addEventListener('change', onSecretChange);
};
addEventListener('DOMContentLoaded', onLoad);
}());
</script>

View File

@@ -0,0 +1,18 @@
function constEq(a, b) {
if (a.length !== b.length) {
return false
}
let zero = 0
for (let i = 0; i < a.length; i++) {
zero |= a.charCodeAt(i) ^ b.charCodeAt(i)
}
return zero === 0
}
function makeSecretIsValid(shieldsSecret) {
return function secretIsValid(secret = '') {
return shieldsSecret && constEq(secret, shieldsSecret)
}
}
export { makeSecretIsValid }

View File

@@ -6,18 +6,18 @@ import path from 'path'
import url, { fileURLToPath } from 'url'
import { bootstrap } from 'global-agent'
import cloudflareMiddleware from 'cloudflare-middleware'
import bytes from 'bytes'
import Camp from '@shields_io/camp'
import originalJoi from 'joi'
import makeBadge from '../../badge-maker/lib/make-badge.js'
import GithubConstellation from '../../services/github/github-constellation.js'
import LibrariesIoConstellation from '../../services/librariesio/librariesio-constellation.js'
import { setRoutes } from '../../services/suggest.js'
import { loadServiceClasses } from '../base-service/loader.js'
import { makeSend } from '../base-service/legacy-result-sender.js'
import { handleRequest } from '../base-service/legacy-request-handler.js'
import { clearResourceCache } from '../base-service/resource-cache.js'
import { clearRegularUpdateCache } from '../legacy/regular-update.js'
import { rasterRedirectUrl } from '../badge-urls/make-badge-url.js'
import { fileSize, nonNegativeInteger } from '../../services/validators.js'
import { nonNegativeInteger } from '../../services/validators.js'
import log from './log.js'
import PrometheusMetrics from './prometheus-metrics.js'
import InfluxMetrics from './influx-metrics.js'
@@ -134,7 +134,6 @@ const publicConfigSchema = Joi.object({
}).default({ authorizedOrigins: [] }),
nexus: defaultService,
npm: defaultService,
obs: defaultService,
sonar: defaultService,
teamcity: defaultService,
weblate: defaultService,
@@ -142,8 +141,7 @@ const publicConfigSchema = Joi.object({
}).required(),
cacheHeaders: { defaultCacheLengthSeconds: nonNegativeInteger },
handleInternalErrors: Joi.boolean().required(),
fetchLimit: fileSize,
userAgentBase: Joi.string().required(),
fetchLimit: Joi.string().regex(/^[0-9]+(b|kb|mb|gb|tb)$/i),
requestTimeoutSeconds: nonNegativeInteger,
requestTimeoutMaxAgeSeconds: nonNegativeInteger,
documentRoot: Joi.string().default(
@@ -171,14 +169,12 @@ const privateConfigSchema = Joi.object({
jira_pass: Joi.string(),
bitbucket_server_username: Joi.string(),
bitbucket_server_password: Joi.string(),
librariesio_tokens: Joi.arrayFromString().items(Joi.string()),
nexus_user: Joi.string(),
nexus_pass: Joi.string(),
npm_token: Joi.string(),
obs_user: Joi.string(),
obs_pass: Joi.string(),
redis_url: Joi.string().uri({ scheme: ['redis', 'rediss'] }),
sentry_dsn: Joi.string(),
shields_secret: Joi.string(),
sl_insight_userUuid: Joi.string(),
sl_insight_apiToken: Joi.string(),
sonarqube_token: Joi.string(),
@@ -201,14 +197,6 @@ function addHandlerAtIndex(camp, index, handlerFn) {
camp.stack.splice(index, 0, handlerFn)
}
function isOnHeroku() {
return !!process.env.DYNO
}
function isOnFly() {
return !!process.env.FLY_APP_NAME
}
/**
* The Server is based on the web framework Scoutcamp. It creates
* an http server, sets up helpers for token persistence and monitoring.
@@ -251,10 +239,6 @@ class Server {
private: privateConfig,
})
this.librariesioConstellation = new LibrariesIoConstellation({
private: privateConfig,
})
if (publicConfig.metrics.prometheus.enabled) {
this.metricInstance = new PrometheusMetrics()
if (publicConfig.metrics.influx.enabled) {
@@ -309,21 +293,13 @@ class Server {
// Set `req.ip`, which is expected by `cloudflareMiddleware()`. This is set
// by Express but not Scoutcamp.
addHandlerAtIndex(this.camp, 0, function (req, res, next) {
if (isOnHeroku()) {
// On Heroku, `req.socket.remoteAddress` is the Heroku router. However,
// the router ensures that the last item in the `X-Forwarded-For` header
// is the real origin.
// https://stackoverflow.com/a/18517550/893113
req.ip = req.headers['x-forwarded-for'].split(', ').pop()
} else if (isOnFly()) {
// On Fly we can use the Fly-Client-IP header
// https://fly.io/docs/reference/runtime-environment/#request-headers
req.ip = req.headers['fly-client-ip']
? req.headers['fly-client-ip']
: req.socket.remoteAddress
} else {
req.ip = req.socket.remoteAddress
}
// On Heroku, `req.socket.remoteAddress` is the Heroku router. However,
// the router ensures that the last item in the `X-Forwarded-For` header
// is the real origin.
// https://stackoverflow.com/a/18517550/893113
req.ip = process.env.DYNO
? req.headers['x-forwarded-for'].split(', ').pop()
: req.socket.remoteAddress
next()
})
addHandlerAtIndex(this.camp, 1, cloudflareMiddleware())
@@ -435,20 +411,14 @@ class Server {
async registerServices() {
const { config, camp, metricInstance } = this
const { apiProvider: githubApiProvider } = this.githubConstellation
const { apiProvider: librariesIoApiProvider } =
this.librariesioConstellation
;(await loadServiceClasses()).forEach(serviceClass =>
serviceClass.register(
{
camp,
handleRequest,
githubApiProvider,
librariesIoApiProvider,
metricInstance,
},
{ camp, handleRequest, githubApiProvider, metricInstance },
{
handleInternalErrors: config.public.handleInternalErrors,
cacheHeaders: config.public.cacheHeaders,
fetchLimitBytes: bytes(config.public.fetchLimit),
rasterUrl: config.public.rasterUrl,
private: config.private,
public: config.public,
@@ -522,12 +492,6 @@ class Server {
const { apiProvider: githubApiProvider } = this.githubConstellation
setRoutes(allowedOrigin, githubApiProvider, camp)
// https://github.com/badges/shields/issues/3273
camp.handle((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*')
next()
})
this.registerErrorHandlers()
this.registerRedirects()
await this.registerServices()
@@ -553,7 +517,7 @@ class Server {
static resetGlobalState() {
// This state should be migrated to instance state. When possible, do not add new
// global state.
clearResourceCache()
clearRegularUpdateCache()
}
reset() {

View File

@@ -64,12 +64,6 @@ describe('The server', function () {
expect(headers['cache-control']).to.equal('max-age=3600, s-maxage=3600')
})
it('should return cors header for the request', async function () {
const { statusCode, headers } = await got(`${baseUrl}npm/v/express.svg`)
expect(statusCode).to.equal(200)
expect(headers['access-control-allow-origin']).to.equal('*')
})
it('should redirect colorscheme PNG badges as configured', async function () {
const { statusCode, headers } = await got(
`${baseUrl}:fruit-apple-green.png`,
@@ -93,28 +87,12 @@ describe('The server', function () {
)
})
it('should produce SVG badges with expected headers', async function () {
const { statusCode, headers } = await got(
`${baseUrl}:fruit-apple-green.svg`
)
expect(statusCode).to.equal(200)
expect(headers['content-type']).to.equal('image/svg+xml;charset=utf-8')
expect(headers['content-length']).to.equal('1130')
})
it('correctly calculates the content-length header for multi-byte unicode characters', async function () {
const { headers } = await got(`${baseUrl}:fruit-apple🍏-green.json`)
expect(headers['content-length']).to.equal('100')
})
it('should produce JSON badges with expected headers', async function () {
it('should produce json badges', async function () {
const { statusCode, body, headers } = await got(
`${baseUrl}:fruit-apple-green.json`
`${baseUrl}twitter/follow/_Pyves.json`
)
expect(statusCode).to.equal(200)
expect(headers['content-type']).to.equal('application/json')
expect(headers['access-control-allow-origin']).to.equal('*')
expect(headers['content-length']).to.equal('92')
expect(() => JSON.parse(body)).not.to.throw()
})
@@ -207,12 +185,6 @@ describe('The server', function () {
.and.to.include('410')
.and.to.include('jpg no longer available')
})
it('should return cors header for the request', async function () {
const { statusCode, headers } = await got(`${baseUrl}npm/v/express.svg`)
expect(statusCode).to.equal(200)
expect(headers['access-control-allow-origin']).to.equal('*')
})
})
context('`requireCloudflare` is enabled', function () {

View File

@@ -49,29 +49,14 @@ const factory = superclass =>
return this
}
expectBadge(badge) {
const expectedKeys = [
'label',
'message',
'logoWidth',
'labelColor',
'color',
'link',
]
for (const key of Object.keys(badge)) {
if (!expectedKeys.includes(key)) {
throw new Error(`Found unexpected object key '${key}'`)
}
}
expectBadge({ label, message, logoWidth, labelColor, color, link }) {
return this.afterJSON(json => {
this.constructor._expectField(json, 'label', badge.label)
this.constructor._expectField(json, 'message', badge.message)
this.constructor._expectField(json, 'logoWidth', badge.logoWidth)
this.constructor._expectField(json, 'labelColor', badge.labelColor)
this.constructor._expectField(json, 'color', badge.color)
this.constructor._expectField(json, 'link', badge.link)
this.constructor._expectField(json, 'label', label)
this.constructor._expectField(json, 'message', message)
this.constructor._expectField(json, 'logoWidth', logoWidth)
this.constructor._expectField(json, 'labelColor', labelColor)
this.constructor._expectField(json, 'color', color)
this.constructor._expectField(json, 'link', link)
})
}

View File

@@ -59,9 +59,7 @@ function _inferPullRequestFromTravisEnv(env) {
}
function _inferPullRequestFromCircleEnv(env) {
return parseGithubPullRequestUrl(
env.CI_PULL_REQUEST || env.CIRCLE_PULL_REQUEST
)
return parseGithubPullRequestUrl(env.CI_PULL_REQUEST)
}
/**

View File

@@ -80,10 +80,6 @@ class Token {
return this.usesRemaining <= 0 && !this.hasReset
}
get decrementedUsesRemaining() {
return this._usesRemaining - 1
}
/**
* Update the uses remaining and next reset time for a token.
*
@@ -192,6 +188,10 @@ class TokenPool {
this.priorityQueue = new PriorityQueue(this.constructor.compareTokens)
}
count() {
return this.tokenIds.size
}
/**
* compareTokens
*
@@ -332,6 +332,29 @@ class TokenPool {
this.fifoQueue.forEach(visit)
this.priorityQueue.forEach(visit)
}
allValidTokenIds() {
const result = []
this.forEach(({ id }) => result.push(id))
return result
}
serializeDebugInfo({ sanitize = true } = {}) {
const maybeSanitize = sanitize ? id => sanitizeToken(id) : id => id
const priorityQueue = []
this.priorityQueue.forEach(t =>
priorityQueue.push(t.getDebugInfo({ sanitize }))
)
return {
utcEpochSeconds: getUtcEpochSeconds(),
allValidTokenIds: this.allValidTokenIds().map(maybeSanitize),
fifoQueue: this.fifoQueue.map(t => t.getDebugInfo({ sanitize })),
priorityQueue,
sanitized: sanitize,
}
}
}
export { sanitizeToken, Token, TokenPool }

View File

@@ -19,6 +19,10 @@ describe('The token pool', function () {
ids.forEach(id => tokenPool.add(id))
})
it('allValidTokenIds() should return the full list', function () {
expect(tokenPool.allValidTokenIds()).to.deep.equal(ids)
})
it('should yield the expected tokens', function () {
ids.forEach(id =>
times(batchSize, () => expect(tokenPool.next().id).to.equal(id))
@@ -34,6 +38,67 @@ describe('The token pool', function () {
)
})
describe('serializeDebugInfo should initially return the expected', function () {
beforeEach(function () {
sinon.useFakeTimers({ now: 1544307744484 })
})
afterEach(function () {
sinon.restore()
})
context('sanitize is not specified', function () {
it('returns fully sanitized results', function () {
// This is `sha()` of '1', '2', '3', '4', '5'. These are written
// literally for avoidance of doubt as to whether sanitization is
// happening.
const sanitizedIds = [
'6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b',
'd4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35',
'4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce',
'4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a',
'ef2d127de37b942baad06145e54b0c619a1f22327b2ebbcfbec78f5564afe39d',
]
expect(tokenPool.serializeDebugInfo()).to.deep.equal({
allValidTokenIds: sanitizedIds,
priorityQueue: [],
fifoQueue: sanitizedIds.map(id => ({
data: '[redacted]',
id,
isFrozen: false,
isValid: true,
nextReset: Token.nextResetNever,
usesRemaining: batchSize,
})),
sanitized: true,
utcEpochSeconds: 1544307744,
})
})
})
context('with sanitize: false', function () {
it('returns unsanitized results', function () {
expect(tokenPool.serializeDebugInfo({ sanitize: false })).to.deep.equal(
{
allValidTokenIds: ids,
priorityQueue: [],
fifoQueue: ids.map(id => ({
data: undefined,
id,
isFrozen: false,
isValid: true,
nextReset: Token.nextResetNever,
usesRemaining: batchSize,
})),
sanitized: false,
utcEpochSeconds: 1544307744,
}
)
})
})
})
context('tokens are marked exhausted immediately', function () {
it('should be exhausted', function () {
ids.forEach(() => {

View File

@@ -1,13 +0,0 @@
import { defineConfig } from 'cypress'
export default defineConfig({
fixturesFolder: false,
env: {
backend_url: 'http://localhost:8080',
},
e2e: {
setupNodeEvents(on, config) {},
baseUrl: 'http://localhost:3000',
supportFile: false,
},
})

9
cypress.json Normal file
View File

@@ -0,0 +1,9 @@
{
"baseUrl": "http://localhost:3000",
"fixturesFolder": false,
"pluginsFile": false,
"supportFile": false,
"env": {
"backend_url": "http://localhost:8080"
}
}

View File

@@ -25,7 +25,7 @@ and learn about the [GitHub workflow](http://try.github.io/).
#### Node, NPM
Node >=16 and NPM >=8 is required. If you don't already have them,
Node >=14 and NPM >=7 is required. If you don't already have them,
install node and npm: https://nodejs.org/en/download/
### Setup a dev install
@@ -228,14 +228,14 @@ Description of the code:
9. Working our way upward, the `async fetch()` method is responsible for calling an API endpoint to get data. Extending `BaseJsonService` gives us the helper function `_requestJson()`. Note here that we pass the schema we defined in step 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 [got](https://github.com/sindresorhus/got) to perform the HTTP request. Options can be passed to got, including method, query string, and headers. If headers are provided they will override the ones automatically set by `_requestJson()`. There is no need to specify json, as the JSON parsing is handled by `_requestJson()`. See the `got` docs for [supported options](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md).
- `_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).
- Error messages corresponding to each status code can be returned by passing a dictionary of status codes -> messages in `errorMessages`.
- A more complex call to `_requestJson()` might look like this:
```js
return this._requestJson({
schema: mySchema,
url,
options: { searchParams: { branch: 'master' } },
options: { qs: { branch: 'master' } },
errorMessages: {
401: 'private application not supported',
404: 'application not found',

View File

@@ -1,22 +0,0 @@
# Adding New Config Values
The Badge Server supports a [variety of methods for defining configuration settings and secrets](./server-secrets.md), and provides a framework for loading those values during bootstrapping.
Any new configuration setting or secret must be correctly registered so that it will be loaded at startup along with the others.
This generally includes adding the corresponding information for your new setting(s)/secret(s) to the following locations:
- [core/server/server.js](https://github.com/badges/shields/blob/master/core/server/server.js) - Add the new values to the [schemas](https://github.com/badges/shields/blob/master/core/server/server.js#L118-L193). Secrets/tokens/etc. should go in the `privateConfigSchema` while non-secret configuration settings should go in the `publicConfigSchema`.
- [config/custom-environment-variables.yml](https://github.com/badges/shields/blob/master/config/custom-environment-variables.yml)
- [docs/server-secrets.md](https://github.com/badges/shields/blob/master/doc/server-secrets.md) (only applicable for secrets)
- [config/default.yml](https://github.com/badges/shields/blob/master/config/default.yml) (optional)
- Any other template config files (e.g. `config/local.template.yml`) (optional)
The exact values needed will depend on what type of secret/setting you are adding, but for reference a few commits are included below which added secrets and or settings:
- (secret) [8a9efb2fc99f97e78ab133c836ab1685803bf4df](https://github.com/badges/shields/commit/8a9efb2fc99f97e78ab133c836ab1685803bf4df)
- (secret) [bd6f4ee1465d14a8f188c37823748a21b6a46762](https://github.com/badges/shields/commit/bd6f4ee1465d14a8f188c37823748a21b6a46762)
- (secret) [0fd557d7bb623e3852c92cebac586d5f6d6d89d8](https://github.com/badges/shields/commit/0fd557d7bb623e3852c92cebac586d5f6d6d89d8)
- (configuration setting) [b1fc4925928c061234e9492f3794c0797467e123](https://github.com/badges/shields/commit/b1fc4925928c061234e9492f3794c0797467e123)
Don't hesitate to reach out if you're unsure of the exact values needed for your new secret/setting, or have any other questions. Feel free to post questions on your corresponding Issue/Pull Request, and/or ping us on the `contributing` channel on our Discord server.

View File

@@ -3,9 +3,6 @@
- The format of new badges should be of the form `/SERVICE/NOUN/PARAMETERS?QUERYSTRING` e.g:
`/github/issues/:user/:repo`. The service is github, the
badge is for issues, and the parameters are `:user/:repo`.
- The `NOUN` part of the route is:
- singular if the badge message represents a single entity, such as the current status of a build (e.g: `/build`), or a more abstract or aggregate representation of the thing (e.g.: `/coverage`, `/quality`)
- plural if there are (or may) be many of the thing (e.g: `/dependencies`, `/stars`)
- Parameters should always be part of the route if they are required to display a badge e.g: `:packageName`.
- Common optional params like, `:branch` or `:tag` should also be passed as part of the route.
- Query string parameters should be used when:

View File

@@ -58,7 +58,7 @@ The tests are also divided into several parts:
[redis-token-persistence.integration]: https://github.com/badges/shields/blob/master/core/token-pooling/redis-token-persistence.integration.js
[github-api-provider.integration]: https://github.com/badges/shields/blob/master/services/github/github-api-provider.integration.js
Our goal is to reach 100% coverage of the code in the
Our goal is for the core code is to reach 100% coverage of the code in the
frontend, core, and service helper functions when the unit and functional
tests are run.
@@ -95,8 +95,8 @@ test this kind of logic through unit tests (e.g. of `render()` and
callback with the four parameters `( queryParams, match, end, ask )` which
is created in a legacy helper function in
[`legacy-request-handler.js`][legacy-request-handler]. This callback
delegates to a callback in `BaseService.register` with three different
parameters `( queryParams, match, sendBadge )`, which
delegates to a callback in `BaseService.register` with four different
parameters `( queryParams, match, sendBadge, request )`, which
then runs `BaseService.invoke`. `BaseService.invoke` instantiates the
service and runs `BaseService#handle`.
@@ -129,12 +129,12 @@ test this kind of logic through unit tests (e.g. of `render()` and
handle unresponsive service code and the next callback is invoked: the
legacy handler function.
3. The legacy handler function receives
`( queryParams, match, sendBadge )`. Its job is to extract data
from the regex `match` and `queryParams`, and then invoke `sendBadge`
with the result.
`( queryParams, match, sendBadge, request )`. Its job is to extract data
from the regex `match` and `queryParams`, invoke `request` to fetch
whatever data it needs, and then invoke `sendBadge` with the result.
4. The implementation of this function is in `BaseService.register`. It
works by running `BaseService.invoke`, which instantiates the service,
injects more dependencies, and invokes `BaseService.handle` which is
injects more dependencies, and invokes `BaseService#handle` which is
implemented by the service subclass.
5. The job of `handle()`, which should be implemented by each service
subclass, is to return an object which partially describes a badge or

View File

@@ -94,8 +94,6 @@ Here is a listing of all deleted badges that were once part of the Shields.io se
- Cauditor
- CocoaPods Apps
- CocoaPods Downloads
- Codetally
- continuousphp
- Coverity
- Dockbit
- Dotnet Status

View File

@@ -40,7 +40,7 @@ If you are submitting a pull request for a custom logo, please:
- Install SVGO
- With npm: `npm install -g svgo`
- With Homebrew: `brew install svgo`
- Run the following command `svgo --precision=3 icon.svg -o icon.min.svg`
- Run the following command `svgo --precision=3 icon.svg icon.min.svg`
- Check if there is a loss of quality in the output, if so increase the precision.
- The [SVGOMG Online Tool][svgomg]
- Click "Open SVG" and select an SVG file.

View File

@@ -16,11 +16,14 @@ Production hosting is managed by the Shields ops team:
| Component | Subcomponent | People with access |
| ----------------------------- | ------------------------------- | --------------------------------------------------------------- |
| shields-io-production | Full access | @calebcartwright, @chris48s, @paulmelnikow |
| shields-io-production | Access management | @calebcartwright, @chris48s, @paulmelnikow |
| shields-production-us | Account owner | @paulmelnikow |
| shields-production-us | Full access | @calebcartwright, @chris48s, @paulmelnikow, @pyvesb |
| shields-production-us | Access management | @calebcartwright, @chris48s, @paulmelnikow, @pyvesb |
| Compose.io Redis | Account owner | @paulmelnikow |
| Compose.io Redis | Account access | @paulmelnikow |
| Compose.io Redis | Database connection credentials | @calebcartwright, @chris48s, @paulmelnikow, @pyvesb |
| Zeit Now | Team owner | @paulmelnikow |
| Zeit Now | Team members | @paulmelnikow, @chris48s, @calebcartwright, @platan |
| Raster server | Full access as team members | @paulmelnikow, @chris48s, @calebcartwright, @platan |
| shields-server.com redirector | Full access as team members | @paulmelnikow, @chris48s, @calebcartwright, @platan |
| Cloudflare (CDN) | Account owner | @espadrine |
@@ -46,11 +49,11 @@ Shields has mercifully little persistent state:
1. The GitHub tokens we collect are saved on each server in a cloud Redis
database. They can also be fetched from the [GitHub auth admin endpoint][]
for debugging.
2. The server keeps the [resource cache][] in memory. It is neither
2. The server keeps the [regular-update cache][] in memory. It is neither
persisted nor inspectable.
[github auth admin endpoint]: https://github.com/badges/shields/blob/master/services/github/auth/admin.js
[resource cache]: https://github.com/badges/shields/blob/master/core/base-service/resource-cache.js
[regular-update cache]: https://github.com/badges/shields/blob/master/core/legacy/regular-update.js
## Configuration
@@ -91,13 +94,19 @@ Cloudflare is configured to respect the servers' cache headers.
## Raster server
The raster server `raster.shields.io` (a.k.a. the rasterizing proxy) is
hosted on Heroku. It's managed in the
[squint](https://github.com/badges/squint/) repo.
hosted on [Zeit Now][]. It's managed in the
[svg-to-image-proxy repo][svg-to-image-proxy].
### Fly.io Deployment
[zeit now]: https://zeit.co/now
[svg-to-image-proxy]: https://github.com/badges/svg-to-image-proxy
Both the badge server and frontend are served from Fly.io. Deployments are
triggered using GitHub actions in a private repo.
### Heroku Deployment
Both the badge server and frontend are served from Heroku.
After merging a commit to master, heroku should create a staging deploy. Check this has deployed correctly in the `shields-staging` pipeline and review https://shields-staging.herokuapp.com/
If we're happy with it, "promote to production". This will deploy what's on staging to the `shields-production-eu` and `shields-production-us` pieplines.
## DNS
@@ -105,15 +114,19 @@ DNS is registered with [DNSimple][].
[dnsimple]: https://dnsimple.com/
## Logs
Logs can be retrieved [from heroku](https://devcenter.heroku.com/articles/logging#log-retrieval).
## Error reporting
[Error reporting][sentry] is one of the most useful tools we have for monitoring
the server. It's generously donated by [Sentry][sentry home]. We bundle
[`@sentry/node`][sentry-node] into the application, and the Sentry DSN is configured
via `local-shields-io-production.yml` (see [documentation][sentry configuration]).
[`raven`][raven] into the application, and the Sentry DSN is configured via
`local-shields-io-production.yml` (see [documentation][sentry configuration]).
[sentry]: https://sentry.io/shields/
[sentry-node]: https://www.npmjs.com/package/@sentry/node
[raven]: https://www.npmjs.com/package/raven
[sentry home]: https://sentry.io/shields/
[sentry configuration]: https://github.com/badges/shields/blob/master/doc/self-hosting.md#sentry

View File

@@ -2,7 +2,7 @@
Shields is a community project that is stewarded by a handful of core maintainers who contribute on a volunteer basis. We do our best to maintain the availability and reliability of the service, and enhance and improve the project overall. However, if you've spotted something wrong or would like to see a specific feature implemented, please consider helping us resolve it by submitting a pull request. All community contributions, even documentation improvements, are welcome!
https://github.com/badges/shields is a monorepo and hosts the Shields frontend and server code as well as the [badge-maker][npm package] NPM library (and the [badge design specification](https://github.com/badges/shields/tree/master/spec)). The packaging and release processes for these items are described in the respective sections below.
https://github.com/badges/shields is a monorepo and hosts the Shields frontend and server code as well as the [badge-maker][npm package] NPM library (and the [badge design specification](https://github.com/badges/shields/tree/master/spec)). The packaging and release processes for these items is described in the respective sections below.
## badge-maker package

View File

@@ -4,13 +4,13 @@ This document describes how to host your own shields server either from source o
## Installing from Source
You will need Node 16 or later, which you can install using a
You will need Node 14 or later, which you can install using a
[package manager][].
On Ubuntu / Debian:
```sh
curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash -; sudo apt-get install -y nodejs
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -; sudo apt-get install -y nodejs
```
```sh
@@ -94,18 +94,20 @@ Sending build context to Docker daemon 3.923 MB
Successfully built 4471b442c220
```
Optionally, alter the default values for configuration by setting them via [environment variables](https://docs.docker.com/engine/reference/commandline/run/#set-environment-variables--e---env---env-file).
See [server-secrets.md](server-secrets.md) and [config/custom-environment-variables.yml](/config/custom-environment-variables.yml) for possible values.
In [config/custom-environment-variables.yml](/config/custom-environment-variables.yml), environment variable names are specified as the quoted, uppercase key values (e.g. `GH_TOKEN`).
Optionally, create a file called `shields.env` that contains the needed
configuration. See [server-secrets.md](server-secrets.md) and [config/custom-environment-variables.yml](/config/custom-environment-variables.yml) for examples.
Then run the container, and be sure to specify the same mapped port as the one Shields is listening on :
Then run the container:
```console
$ docker run --rm -p 8080:8080 --env PORT=8080 --name shields shieldsio/shields:next
$ docker run --rm -p 8080:80 --name shields shields
# or if you have shields.env file, run the following instead
$ docker run --rm -p 8080:80 --env-file shields.env --name shields shields
Configuration:
...
0916211515 Server is starting up: http://0.0.0.0:8080/
> badge-maker@3.0.0 start /usr/src/app
> node server.js
http://[::1]/
```
Assuming Docker is running locally, you should be able to get to the
@@ -115,11 +117,15 @@ If you run Docker in a virtual machine (such as boot2docker or Docker Machine)
then you will need to replace `localhost` with the IP address of that virtual
machine.
[shields.example.env]: ../shields.example.env
## Raster server
If you want to host PNG badges, you can also self-host a [raster server][]
which points to your badge server. It's a docker container. We host it on
Fly.io but should be possible to host on a wide variety of platforms.
which points to your badge server. It's designed as a web function which is
tested on Zeit Now, though you may be able to run it on AWS Lambda. It's
built on the [micro][] framework, and comes with a `start` script that allows
it to run as a standalone Node service.
- In your raster instance, set `BASE_URL` to your Shields instance, e.g.
`https://shields.example.co`.
@@ -128,9 +134,11 @@ Fly.io but should be possible to host on a wide variety of platforms.
for the legacy raster URLs instead of 404's.
If anyone has set this up, more documentation on how to do this would be
welcome!
welcome! It would also be nice to ship a Docker image that includes a
preconfigured raster server.
[raster server]: https://github.com/badges/squint
[raster server]: https://github.com/badges/svg-to-image-proxy
[micro]: https://github.com/zeit/micro
## Server secrets

View File

@@ -174,24 +174,6 @@ access to a private Jenkins CI instance.
Provide a username and password to give your self-hosted Shields installation
access to a private JIRA instance.
### Libraries.io/Bower
- `LIBRARIESIO_TOKENS` (yml: `private.librariesio_tokens`)
Note that the Bower badges utilize the Libraries.io API, so use this secret for both Libraries.io badges and/or Bower badges.
Just like the `*_ORIGINS` type secrets, this value can accept a single token as a string, or a group of tokens provided as an array of strings. For example:
```yaml
private:
librariesio_tokens: my-token
## Or
private:
librariesio_tokens: [my-token some-other-token]
```
When using the environment variable with multiple tokens, be sure to use a space to separate the tokens, e.g. `LIBRARIESIO_TOKENS="my-token some-other-token"`
### Nexus
- `NEXUS_ORIGINS` (yml: `public.services.nexus.authorizedOrigins`)
@@ -211,21 +193,6 @@ installation access to private npm packages
[npm token]: https://docs.npmjs.com/getting-started/working_with_tokens
## Open Build Service
- `OBS_USER` (yml: `private.obs_user`)
- `OBS_PASS` (yml: `private.obs_user`)
Only authenticated users are allowed to access the Open Build Service API.
Authentication is done by sending a Basic HTTP Authorisation header. A user
account for the [reference instance](https://build.opensuse.org) is a SUSE
IdP account, which can be created [here](https://idp-portal.suse.com/univention/self-service/#page=createaccount).
While OBS supports [API tokens](https://openbuildservice.org/help/manuals/obs-user-guide/cha.obs.authorization.token.html#id-1.5.10.16.4),
they can only be scoped to execute specific actions on a POST request. This
means however, that an actual account is required to read the build status
of a package.
### SymfonyInsight (formerly Sensiolabs)
- `SL_INSIGHT_USER_UUID` (yml: `private.sl_insight_userUuid`)

View File

@@ -67,7 +67,7 @@ t.create('Build status')
- All badges on shields can be requested in a number of formats. As well as calling https://img.shields.io/wercker/build/wercker/go-wercker-api.svg to generate ![](https://img.shields.io/wercker/build/wercker/go-wercker-api.svg) we can also call https://img.shields.io/wercker/build/wercker/go-wercker-api.json to request the same content as JSON. When writing service tests, we request the badge in JSON format so it is easier to make assertions about the content.
- We don't need to explicitly call `/wercker/build/wercker/go-wercker-api.json` here, only `/build/wercker/go-wercker-api.json`. When we create a tester object with `createServiceTester()` the URL base defined in our service class (in this case `/wercker`) is used as the base URL for any requests made by the tester object.
3. `expectBadge()` is a helper function which accepts either a string literal, a [RegExp][] or a [Joi][] schema for the different fields.
Joi is a validation library that is built into IcedFrisby which you can use to
Joi is a validation library that is build into IcedFrisby which you can use to
match based on a set of allowed strings, regexes, or specific values. You can
refer to their [API reference][joi api].
4. We expect `label` to be a string literal `"build"`.
@@ -254,7 +254,7 @@ By checking code coverage, we can make sure we've covered all our bases.
We can generate a coverage report and open it:
```
npm run coverage:test:services -- -- --only=wercker
npm run coverage:test:services -- --only=wercker
npm run coverage:report:open
```

View File

@@ -43,12 +43,9 @@ function Example({
exampleData: RenderableExample
isBadgeSuggestion: boolean
}): JSX.Element {
const handleClick = React.useCallback(
function (): void {
onClick(exampleData, isBadgeSuggestion)
},
[exampleData, isBadgeSuggestion, onClick]
)
function handleClick(): void {
onClick(exampleData, isBadgeSuggestion)
}
let exampleUrl, previewUrl
if (isBadgeSuggestion) {

View File

@@ -50,16 +50,13 @@ function _CopiedContentIndicator(
},
}))
const handlePoseComplete = React.useCallback(
function (): void {
if (pose === 'effectStart') {
setPose('effectEnd')
} else {
setPose('hidden')
}
},
[pose, setPose]
)
function handlePoseComplete(): void {
if (pose === 'effectStart') {
setPose('effectEnd')
} else {
setPose('hidden')
}
}
return (
<ContentAnchor>

View File

@@ -40,13 +40,10 @@ export default function Customizer({
const [markup, setMarkup] = useState<string>()
const [message, setMessage] = useState<string>()
const generateBuiltBadgeUrl = React.useCallback(
function (): string {
const suffix = queryString ? `?${queryString}` : ''
return `${baseUrl}${path}${suffix}`
},
[baseUrl, path, queryString]
)
function generateBuiltBadgeUrl(): string {
const suffix = queryString ? `?${queryString}` : ''
return `${baseUrl}${path}${suffix}`
}
function renderLivePreview(): JSX.Element {
// There are some usability issues here. It would be better if the message
@@ -70,31 +67,28 @@ export default function Customizer({
)
}
const copyMarkup = React.useCallback(
async function (markupFormat: MarkupFormat): Promise<void> {
const builtBadgeUrl = generateBuiltBadgeUrl()
const markup = generateMarkup({
badgeUrl: builtBadgeUrl,
link,
title,
markupFormat,
})
try {
await clipboardCopy(markup)
} catch (e) {
setMessage('Copy failed')
setMarkup(markup)
return
}
async function copyMarkup(markupFormat: MarkupFormat): Promise<void> {
const builtBadgeUrl = generateBuiltBadgeUrl()
const markup = generateMarkup({
badgeUrl: builtBadgeUrl,
link,
title,
markupFormat,
})
try {
await clipboardCopy(markup)
} catch (e) {
setMessage('Copy failed')
setMarkup(markup)
if (indicatorRef.current) {
indicatorRef.current.trigger()
}
},
[generateBuiltBadgeUrl, link, title, setMessage, setMarkup]
)
return
}
setMarkup(markup)
if (indicatorRef.current) {
indicatorRef.current.trigger()
}
}
function renderMarkupAndLivePreview(): JSX.Element {
return (
@@ -116,32 +110,26 @@ export default function Customizer({
)
}
const handlePathChange = React.useCallback(
function ({
path,
isComplete,
}: {
path: string
isComplete: boolean
}): void {
setPath(path)
setPathIsComplete(isComplete)
},
[setPath, setPathIsComplete]
)
function handlePathChange({
path,
isComplete,
}: {
path: string
isComplete: boolean
}): void {
setPath(path)
setPathIsComplete(isComplete)
}
const handleQueryStringChange = React.useCallback(
function ({
queryString,
isComplete,
}: {
queryString: string
isComplete: boolean
}): void {
setQueryString(queryString)
},
[setQueryString]
)
function handleQueryStringChange({
queryString,
isComplete,
}: {
queryString: string
isComplete: boolean
}): void {
setQueryString(queryString)
}
return (
<form action="">

View File

@@ -149,17 +149,14 @@ export default function PathBuilder({
}
}, [tokens, namedParams, onChange])
const handleTokenChange = React.useCallback(
function ({
target: { name, value },
}: ChangeEvent<HTMLInputElement | HTMLSelectElement>): void {
setNamedParams({
...namedParams,
[name]: value,
})
},
[setNamedParams, namedParams]
)
function handleTokenChange({
target: { name, value },
}: ChangeEvent<HTMLInputElement | HTMLSelectElement>): void {
setNamedParams({
...namedParams,
[name]: value,
})
}
function renderLiteral(
literal: string,

View File

@@ -270,24 +270,18 @@ export default function QueryStringBuilder({
}, {} as Record<BadgeOptionName, string>)
)
const handleServiceQueryParamChange = React.useCallback(
function ({
target: { name, type: targetType, checked, value },
}: ChangeEvent<HTMLInputElement>): void {
const outValue = targetType === 'checkbox' ? checked : value
setQueryParams({ ...queryParams, [name]: outValue })
},
[setQueryParams, queryParams]
)
function handleServiceQueryParamChange({
target: { name, type: targetType, checked, value },
}: ChangeEvent<HTMLInputElement>): void {
const outValue = targetType === 'checkbox' ? checked : value
setQueryParams({ ...queryParams, [name]: outValue })
}
const handleBadgeOptionChange = React.useCallback(
function ({
target: { name, value },
}: ChangeEvent<HTMLInputElement>): void {
setBadgeOptions({ ...badgeOptions, [name]: value })
},
[setBadgeOptions, badgeOptions]
)
function handleBadgeOptionChange({
target: { name, value },
}: ChangeEvent<HTMLInputElement>): void {
setBadgeOptions({ ...badgeOptions, [name]: value })
}
useEffect(() => {
if (onChange) {

View File

@@ -86,30 +86,24 @@ export default function GetMarkupButton({
Select<Option>
>
const onControlMouseDown = React.useCallback(
async function (event: MouseEvent): Promise<void> {
if (onMarkupRequested) {
await onMarkupRequested('link')
}
if (selectRef.current) {
selectRef.current.blur()
}
},
[onMarkupRequested, selectRef]
)
async function onControlMouseDown(event: MouseEvent): Promise<void> {
if (onMarkupRequested) {
await onMarkupRequested('link')
}
if (selectRef.current) {
selectRef.current.blur()
}
}
const onOptionClick = React.useCallback(
async function onOptionClick(
// Eeesh.
value: Option | readonly Option[] | null | undefined
): Promise<void> {
const { value: markupFormat } = value as Option
if (onMarkupRequested) {
await onMarkupRequested(markupFormat)
}
},
[onMarkupRequested]
)
async function onOptionClick(
// Eeesh.
value: Option | readonly Option[] | null | undefined
): Promise<void> {
const { value: markupFormat } = value as Option
if (onMarkupRequested) {
await onMarkupRequested(markupFormat)
}
}
return (
// TODO It doesn't seem to be possible to check the types and wrap with

View File

@@ -44,39 +44,33 @@ export default function DynamicBadgeMaker({
const isValid =
values.datatype && values.label && values.dataUrl && values.query
const onChange = React.useCallback(
function ({
target: { name, value },
}: ChangeEvent<HTMLInputElement | HTMLSelectElement>): void {
setValues({
...values,
[name]: value,
})
},
[values]
)
function onChange({
target: { name, value },
}: ChangeEvent<HTMLInputElement | HTMLSelectElement>): void {
setValues({
...values,
[name]: value,
})
}
const onSubmit = React.useCallback(
function onSubmit(e: React.FormEvent): void {
e.preventDefault()
function onSubmit(e: React.FormEvent): void {
e.preventDefault()
const { datatype, label, dataUrl, query, color, prefix, suffix } = values
window.open(
dynamicBadgeUrl({
baseUrl,
datatype,
label,
dataUrl,
query,
color,
prefix,
suffix,
}),
'_blank'
)
},
[baseUrl, values]
)
const { datatype, label, dataUrl, query, color, prefix, suffix } = values
window.open(
dynamicBadgeUrl({
baseUrl,
datatype,
label,
dataUrl,
query,
color,
prefix,
suffix,
}),
'_blank'
)
}
return (
<form onSubmit={onSubmit}>

View File

@@ -54,28 +54,24 @@ export default function Main({
const searchTimeout = useRef(0)
const baseUrl = getBaseUrl()
const performSearch = React.useCallback(
function (query: string): void {
setSearchIsInProgress(false)
function performSearch(query: string): void {
setSearchIsInProgress(false)
setQueryIsTooShort(query.length === 1)
setQueryIsTooShort(query.length === 1)
if (query.length >= 2) {
const flat = ServiceDefinitionSetHelper.create(services)
.notDeprecated()
.search(query)
.toArray()
setSearchResults(groupBy(flat, 'category'))
} else {
setSearchResults(undefined)
}
},
[setSearchIsInProgress, setQueryIsTooShort, setSearchResults]
)
if (query.length >= 2) {
const flat = ServiceDefinitionSetHelper.create(services)
.notDeprecated()
.search(query)
.toArray()
setSearchResults(groupBy(flat, 'category'))
} else {
setSearchResults(undefined)
}
}
const searchQueryChanged = React.useCallback(
function (query: string): void {
/*
function searchQueryChanged(query: string): void {
/*
Add a small delay before showing search results
so that we wait until the user has stopped typing
before we start loading stuff.
@@ -85,27 +81,22 @@ export default function Main({
b) stops the page from 'flashing' as the user types, like this:
https://user-images.githubusercontent.com/7288322/42600206-9b278470-85b5-11e8-9f63-eb4a0c31cb4a.gif
*/
setSearchIsInProgress(true)
window.clearTimeout(searchTimeout.current)
searchTimeout.current = window.setTimeout(() => performSearch(query), 500)
},
[setSearchIsInProgress, performSearch]
)
setSearchIsInProgress(true)
window.clearTimeout(searchTimeout.current)
searchTimeout.current = window.setTimeout(() => performSearch(query), 500)
}
const exampleClicked = React.useCallback(
function (example: RenderableExample, isSuggestion: boolean): void {
setSelectedExample(example)
setSelectedExampleIsSuggestion(isSuggestion)
},
[setSelectedExample, setSelectedExampleIsSuggestion]
)
function exampleClicked(
example: RenderableExample,
isSuggestion: boolean
): void {
setSelectedExample(example)
setSelectedExampleIsSuggestion(isSuggestion)
}
const dismissMarkupModal = React.useCallback(
function (): void {
setSelectedExample(undefined)
},
[setSelectedExample]
)
function dismissMarkupModal(): void {
setSelectedExample(undefined)
}
function Category({
category,

View File

@@ -18,27 +18,21 @@ export default function StaticBadgeMaker({
const isValid = values.message && values.color
const onChange = React.useCallback(
function onChange({
target: { name, value },
}: ChangeEvent<HTMLInputElement | HTMLSelectElement>): void {
setValues({
...values,
[name]: value,
})
},
[setValues, values]
)
function onChange({
target: { name, value },
}: ChangeEvent<HTMLInputElement | HTMLSelectElement>): void {
setValues({
...values,
[name]: value,
})
}
const onSubmit = React.useCallback(
function (e: React.FormEvent): void {
e.preventDefault()
function onSubmit(e: React.FormEvent): void {
e.preventDefault()
const { label, message, color } = values
window.open(staticBadgeUrl({ baseUrl, label, message, color }), '_blank')
},
[baseUrl, values]
)
const { label, message, color } = values
window.open(staticBadgeUrl({ baseUrl, label, message, color }), '_blank')
}
return (
<form onSubmit={onSubmit}>

View File

@@ -41,47 +41,41 @@ export default function SuggestionAndSearch({
const [projectUrl, setProjectUrl] = useState<string>()
const [suggestions, setSuggestions] = useState<SuggestionItem[]>([])
const onQueryChanged = React.useCallback(
function ({
target: { value: query },
}: ChangeEvent<HTMLInputElement>): void {
const isUrl = query.startsWith('https://') || query.startsWith('http://')
setIsUrl(isUrl)
setProjectUrl(isUrl ? query : undefined)
function onQueryChanged({
target: { value: query },
}: ChangeEvent<HTMLInputElement>): void {
const isUrl = query.startsWith('https://') || query.startsWith('http://')
setIsUrl(isUrl)
setProjectUrl(isUrl ? query : undefined)
queryChangedDebounced.current(query)
},
[setIsUrl, setProjectUrl, queryChangedDebounced]
)
queryChangedDebounced.current(query)
}
const getSuggestions = React.useCallback(
async function (): Promise<void> {
if (!projectUrl) {
setSuggestions([])
return
}
async function getSuggestions(): Promise<void> {
if (!projectUrl) {
setSuggestions([])
return
}
setInProgress(true)
setInProgress(true)
const fetch = window.fetch || fetchPonyfill
const res = await fetch(
`${baseUrl}/$suggest/v1?url=${encodeURIComponent(projectUrl)}`
)
let suggestions = [] as SuggestionItem[]
try {
const json = (await res.json()) as SuggestionResponse
// This doesn't validate the response. The default value here prevents
// a crash if the server returns {"err":"Disallowed"}.
suggestions = json.suggestions || []
} catch (e) {
suggestions = []
}
const fetch = window.fetch || fetchPonyfill
const res = await fetch(
`${baseUrl}/$suggest/v1?url=${encodeURIComponent(projectUrl)}`
)
let suggestions = [] as SuggestionItem[]
try {
const json = (await res.json()) as SuggestionResponse
// This doesn't validate the response. The default value here prevents
// a crash if the server returns {"err":"Disallowed"}.
suggestions = json.suggestions || []
} catch (e) {
suggestions = []
}
setInProgress(false)
setSuggestions(suggestions)
},
[setSuggestions, setInProgress, baseUrl, projectUrl]
)
setInProgress(false)
setSuggestions(suggestions)
}
function renderSuggestions(): JSX.Element | null {
if (suggestions.length === 0) {
@@ -111,8 +105,6 @@ export default function SuggestionAndSearch({
)
}
// TODO: Warning: A future version of React will block javascript: URLs as a security precaution
// how else to do this?
return (
<section>
<form action="javascript:void 0" autoComplete="off">

View File

@@ -21,14 +21,11 @@ export function getBaseUrl(): string {
https://img.shields.io/
*/
try {
const { protocol, hostname, port } = window.location
const { protocol, hostname } = window.location
if (['shields.io', 'www.shields.io'].includes(hostname)) {
return 'https://img.shields.io'
}
if (!port) {
return `${protocol}//${hostname}`
}
return `${protocol}//${hostname}:${port}`
return `${protocol}//${hostname}`
} catch (e) {
// server-side rendering
return ''

View File

@@ -5,6 +5,7 @@ import Meta from '../components/meta'
import Header from '../components/header'
import Footer from '../components/footer'
import { BaseFont, GlobalStyle, H3 } from '../components/common'
import Heroku from '../../static/images/heroku-logotype-horizontal-purple.svg'
import NodePing from '../../static/images/nodeping.svg'
import Sentry from '../../static/images/sentry-logo-black.svg'
const MainContainer = styled(BaseFont)`
@@ -42,6 +43,11 @@ export default function SponsorsPage(): JSX.Element {
These companies help us by donating their services to shields:
<ul style={{ listStyleType: 'none' }}>
<SponsorItems>
<li>
<a href="https://www.heroku.com/">
<Heroku alt="heroku_logo" height={120} />
</a>
</li>
<li>
<a href="https://nodeping.com/">
<NodePing alt="nodeping_logo" height={60} />

View File

@@ -134,7 +134,7 @@ export default function EndpointPage(): JSX.Element {
</p>
<p>
The endpoint badge is a better alternative than redirecting to the
static badge endpoint or generating SVG on your server:
static badge enpoint or generating SVG on your server:
</p>
<ol>
<li>
@@ -142,7 +142,7 @@ export default function EndpointPage(): JSX.Element {
Content and presentation are separate.
</a>{' '}
The service provider authors the badge, and Shields takes input from
the user to format it. As a service provider, you author the badge
the user to format it. As a service provider you author the badge
but don't have to concern yourself with styling. You don't even have
to pass the formatting options through to Shields.
</li>
@@ -152,12 +152,12 @@ export default function EndpointPage(): JSX.Element {
</li>
<li>
A JSON response is easy to implement; easier than an HTTP redirect.
It is trivial in almost any framework and is more compatible with
It is trivial in almost any framework, and is more compatible with
hosting environments such as{' '}
<a href="https://runkit.com/docs/endpoint">RunKit endpoints</a>.
</li>
<li>
As a service provider, you can rely on the Shields CDN. There's no
As a service provider you can rely on the Shields CDN. There's no
need to study the HTTP headers. Adjusting cache behavior is as
simple as setting a property in the JSON response.
</li>
@@ -197,7 +197,7 @@ export default function EndpointPage(): JSX.Element {
<dd>
Default: <code>false</code>. <code>true</code> to treat this as an
error badge. This prevents the user from overriding the color. In the
future, it may affect cache behavior.
future it may affect cache behavior.
</dd>
<dt>namedLogo</dt>
<dd>

View File

@@ -1,4 +1,4 @@
import * as originalSimpleIcons from 'simple-icons/icons'
import originalSimpleIcons from 'simple-icons'
import { svg2base64 } from './svg-helpers.js'
function loadSimpleIcons() {
@@ -14,10 +14,10 @@ function loadSimpleIcons() {
// https://github.com/badges/shields/issues/4273
Object.keys(originalSimpleIcons).forEach(key => {
const icon = originalSimpleIcons[key]
const { title, slug, hex } = icon
const title = icon.title.toLowerCase()
const legacyTitle = title.replace(/ /g, '-')
icon.base64 = {
default: svg2base64(icon.svg.replace('<svg', `<svg fill="#${hex}"`)),
default: svg2base64(icon.svg.replace('<svg', `<svg fill="#${icon.hex}"`)),
light: svg2base64(icon.svg.replace('<svg', `<svg fill="whitesmoke"`)),
dark: svg2base64(icon.svg.replace('<svg', `<svg fill="#333"`)),
}
@@ -26,17 +26,14 @@ function loadSimpleIcons() {
// (e.g. 'Hive'). If a by-title reference we generate for
// backwards compatibility collides with a proper slug from Simple Icons
// then do nothing, so that the proper slug will always map to the correct icon.
// Starting in v7, the exported object with the full icon set has updated the keys
// to include a lowercase `si` prefix, and utilizes proper case naming conventions.
if (!(`si${title}` in originalSimpleIcons)) {
simpleIcons[title.toLowerCase()] = icon
if (!(title in originalSimpleIcons)) {
simpleIcons[title] = icon
}
const legacyTitle = title.replace(/ /g, '-')
if (!(`si${legacyTitle}` in originalSimpleIcons)) {
simpleIcons[legacyTitle.toLowerCase()] = icon
if (!(legacyTitle in originalSimpleIcons)) {
simpleIcons[legacyTitle] = icon
}
simpleIcons[slug] = icon
simpleIcons[key] = icon
})
return simpleIcons
}

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="93 93 194 194"><defs><style>.b{fill:#fc6d26}</style></defs><path style="fill:#e24329" d="m282.83 170.73-.27-.69-26.14-68.22a6.81 6.81 0 0 0-2.69-3.24 7 7 0 0 0-8 .43 7 7 0 0 0-2.32 3.52l-17.65 54h-71.47l-17.65-54a6.86 6.86 0 0 0-2.32-3.53 7 7 0 0 0-8-.43 6.87 6.87 0 0 0-2.69 3.24L97.44 170l-.26.69a48.54 48.54 0 0 0 16.1 56.1l.09.07.24.17 39.82 29.82 19.7 14.91 12 9.06a8.07 8.07 0 0 0 9.76 0l12-9.06 19.7-14.91 40.06-30 .1-.08a48.56 48.56 0 0 0 16.08-56.04Z"/><path class="b" d="m282.83 170.73-.27-.69a88.3 88.3 0 0 0-35.15 15.8L190 229.25c19.55 14.79 36.57 27.64 36.57 27.64l40.06-30 .1-.08a48.56 48.56 0 0 0 16.1-56.08Z"/><path style="fill:#fca326" d="m153.43 256.89 19.7 14.91 12 9.06a8.07 8.07 0 0 0 9.76 0l12-9.06 19.7-14.91S209.55 244 190 229.25c-19.55 14.75-36.57 27.64-36.57 27.64Z"/><path class="b" d="M132.58 185.84A88.19 88.19 0 0 0 97.44 170l-.26.69a48.54 48.54 0 0 0 16.1 56.1l.09.07.24.17 39.82 29.82L190 229.21Z"/></svg>
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M23.956 13.587l-1.344-4.133a4549.814 4549.814 0 0 0-2.663-8.189.456.456 0 0 0-.87 0l-2.658 8.189H7.585L4.92 1.265a.456.456 0 0 0-.87 0A4549.814 4549.814 0 0 0 .044 13.587a.908.908 0 0 0 .336 1.02L12 23.054l11.62-8.447a.908.908 0 0 0 .336-1.02" fill="#fc6d26"/><path d="M12 23.054l4.421-13.6H7.58z" fill="#e24329"/><path d="M7.579 9.454H1.388L12 23.054z" fill="#fc6d26"/><path d="M1.388 9.454L.044 13.587a.908.908 0 0 0 .336 1.02L12 23.054z" fill="#fca326"/><path d="M7.579 9.454L4.92 1.265a.456.456 0 0 0-.87 0L1.388 9.454z" fill="#e24329"/><path d="M16.421 9.454h6.191L12 23.054z" fill="#fc6d26"/><path d="M22.612 9.454l1.344 4.133a.908.908 0 0 1-.336 1.02L12 23.054z" fill="#fca326"/><path d="M16.421 9.454l2.658-8.189a.456.456 0 0 1 .87 0l2.663 8.189z" fill="#e24329"/></svg>

Before

Width:  |  Height:  |  Size: 986 B

After

Width:  |  Height:  |  Size: 847 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 24c6.627 0 12-5.373 12-12S18.627 0 12 0 0 5.373 0 12s5.373 12 12 12Z" fill="url(#a)"/><path fill-rule="evenodd" clip-rule="evenodd" d="M5.425 11.871a796.414 796.414 0 0 1 6.994-3.018c3.328-1.388 4.027-1.628 4.477-1.638.1 0 .32.02.47.14.12.1.15.23.17.33.02.1.04.31.02.47-.18 1.898-.96 6.504-1.36 8.622-.17.9-.5 1.199-.819 1.229-.7.06-1.229-.46-1.898-.9-1.06-.689-1.649-1.119-2.678-1.798-1.19-.78-.42-1.209.26-1.908.18-.18 3.247-2.978 3.307-3.228.01-.03.01-.15-.06-.21-.07-.06-.17-.04-.25-.02-.11.02-1.788 1.14-5.056 3.348-.48.33-.909.49-1.299.48-.43-.01-1.248-.24-1.868-.44-.75-.24-1.349-.37-1.299-.79.03-.22.33-.44.89-.669Z" fill="#fff"/><defs><linearGradient id="a" x1="11.99" y1="0" x2="11.99" y2="23.81" gradientUnits="userSpaceOnUse"><stop stop-color="#2AABEE"/><stop offset="1" stop-color="#229ED9"/></linearGradient></defs></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="12" cy="12" r="12" fill="#2ca5e0"/><path d="M9.8 17.5c-.389 0-.323-.147-.457-.517L8.2 13.221 17 8" fill="#a9c9dd"/><path d="M9.8 17.5c.3 0 .433-.137.6-.3l1.6-1.556-1.996-1.203" fill="#c8daea"/><path d="M10.004 14.441l4.836 3.573c.552.304.95.147 1.088-.512l1.968-9.277c.202-.808-.308-1.174-.836-.935L5.501 11.748c-.789.316-.784.756-.144.952l2.967.926 6.867-4.332c.324-.197.622-.091.377.125" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 909 B

After

Width:  |  Height:  |  Size: 481 B

24138
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,8 @@
],
"homepage": "https://shields.io",
"bugs": {
"url": "https://github.com/badges/shields/issues"
"url": "https://github.com/badges/shields/issues",
"email": "thaddee.tyl@gmail.com"
},
"license": "CC0-1.0",
"author": "Thaddée Tyl <thaddee.tyl@gmail.com>",
@@ -21,48 +22,48 @@
"url": "https://github.com/badges/shields"
},
"dependencies": {
"@fontsource/lato": "^4.5.8",
"@fontsource/lekton": "^4.5.9",
"@renovate/pep440": "^1.0.0",
"@sentry/node": "^7.8.0",
"@fontsource/lato": "^4.5.0",
"@fontsource/lekton": "^4.5.0",
"@sentry/node": "^6.12.0",
"@shields_io/camp": "^18.1.1",
"badge-maker": "file:badge-maker",
"bytes": "^3.1.2",
"camelcase": "^7.0.0",
"chalk": "^5.0.1",
"check-node-version": "^4.2.1",
"bytes": "^3.1.0",
"camelcase": "^6.2.0",
"chalk": "^4.1.2",
"check-node-version": "^4.1.0",
"cloudflare-middleware": "^1.0.4",
"config": "^3.3.7",
"config": "^3.3.6",
"cross-env": "^7.0.3",
"dayjs": "^1.11.4",
"decamelize": "^3.2.0",
"emojic": "^1.1.17",
"decamelize": "^5.0.0",
"emojic": "^1.1.16",
"escape-string-regexp": "^4.0.0",
"fast-xml-parser": "^4.0.9",
"glob": "^8.0.3",
"fast-xml-parser": "^3.20.0",
"glob": "^7.1.7",
"global-agent": "^3.0.0",
"got": "^12.3.0",
"graphql": "^15.6.1",
"graphql-tag": "^2.12.6",
"ioredis": "5.2.2",
"joi": "17.6.0",
"got": "11.8.2",
"graphql": "^15.5.3",
"graphql-tag": "^2.12.5",
"ioredis": "4.27.9",
"joi": "17.4.2",
"joi-extension-semver": "5.0.0",
"js-yaml": "^4.1.0",
"jsonpath": "~1.1.1",
"lodash.countby": "^4.6.0",
"lodash.groupby": "^4.6.0",
"lodash.times": "^4.3.2",
"moment": "^2.29.1",
"node-env-flag": "^0.1.0",
"parse-link-header": "^2.0.0",
"path-to-regexp": "^6.2.1",
"pretty-bytes": "^6.0.0",
"parse-link-header": "^1.0.1",
"path-to-regexp": "^6.2.0",
"pretty-bytes": "^5.6.0",
"priorityqueuejs": "^2.0.0",
"prom-client": "^14.0.1",
"qs": "^6.11.0",
"query-string": "^7.1.1",
"semver": "~7.3.7",
"simple-icons": "7.5.0",
"webextension-store-meta": "^1.0.5",
"prom-client": "^13.2.0",
"qs": "^6.10.1",
"query-string": "^7.0.1",
"request": "~2.88.2",
"semver": "~7.3.5",
"simple-icons": "5.14.0",
"webextension-store-meta": "^1.0.4",
"xmldom": "~0.6.0",
"xpath": "~0.0.32"
},
@@ -98,7 +99,7 @@
"test": "run-s --silent --continue-on-error lint test:frontend test:package test:core test:entrypoint check-types:package check-types:frontend prettier:check",
"check-types:package": "tsd badge-maker",
"check-types:frontend": "tsc --noEmit --project .",
"depcheck": "check-node-version --node \">= 16.0\"",
"depcheck": "check-node-version --node \">= 14.0\"",
"prebuild": "run-s --silent depcheck",
"features": "node scripts/export-supported-features-cli.js > ./frontend/supported-features.json",
"defs": "node scripts/export-service-definitions-cli.js > ./frontend/service-definitions.yml",
@@ -141,110 +142,118 @@
]
},
"devDependencies": {
"@babel/core": "^7.18.9",
"@babel/core": "^7.15.5",
"@babel/polyfill": "^7.12.1",
"@babel/register": "7.18.9",
"@istanbuljs/schema": "^0.1.3",
"@babel/register": "7.15.3",
"@mapbox/react-click-to-select": "^2.2.1",
"@types/chai": "^4.3.1",
"@types/lodash.debounce": "^4.0.7",
"@types/lodash.groupby": "^4.6.7",
"@types/mocha": "^9.1.1",
"@types/chai": "^4.2.21",
"@types/lodash.debounce": "^4.0.6",
"@types/lodash.groupby": "^4.6.6",
"@types/mocha": "^9.0.0",
"@types/node": "^16.7.10",
"@types/react-helmet": "^6.1.5",
"@types/react-modal": "^3.13.1",
"@types/react-helmet": "^6.1.2",
"@types/react-modal": "^3.12.1",
"@types/react-select": "^4.0.17",
"@types/styled-components": "5.1.25",
"@typescript-eslint/eslint-plugin": "^5.31.0",
"@typescript-eslint/parser": "^5.30.7",
"@types/styled-components": "5.1.14",
"@typescript-eslint/eslint-plugin": "^4.31.0",
"@typescript-eslint/parser": "^4.30.0",
"babel-plugin-inline-react-svg": "^2.0.1",
"babel-preset-gatsby": "^2.19.0",
"c8": "^7.12.0",
"caller": "^1.1.0",
"chai": "^4.3.6",
"babel-plugin-istanbul": "^6.0.0",
"babel-preset-gatsby": "^1.13.0",
"c8": "^7.9.0",
"caller": "^1.0.1",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"chai-datetime": "^1.8.0",
"chai-string": "^1.4.0",
"child-process-promise": "^2.2.1",
"clipboard-copy": "^4.0.1",
"concurrently": "^7.3.0",
"cypress": "^10.3.1",
"danger": "^11.1.1",
"concurrently": "^6.2.1",
"cypress": "^8.4.0",
"danger": "^10.6.6",
"danger-plugin-no-test-shortcuts": "^2.0.0",
"deepmerge": "^4.2.2",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.5.0",
"eslint-config-prettier": "^8.3.0",
"eslint-config-standard": "^16.0.3",
"eslint-config-standard-jsx": "^10.0.0",
"eslint-config-standard-react": "^11.0.1",
"eslint-plugin-chai-friendly": "^0.7.2",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsdoc": "^39.3.3",
"eslint-plugin-mocha": "^10.1.0",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-jsdoc": "^36.1.0",
"eslint-plugin-mocha": "^9.0.0",
"eslint-plugin-no-extension-in-require": "^0.2.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.2.0",
"eslint-plugin-react": "^7.30.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-sort-class-members": "^1.14.1",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.2.0",
"eslint-plugin-sort-class-members": "^1.11.0",
"fetch-ponyfill": "^7.1.0",
"form-data": "^4.0.0",
"gatsby": "4.6.2",
"gatsby-plugin-catch-links": "^4.19.0",
"gatsby-plugin-page-creator": "^4.7.0",
"gatsby-plugin-react-helmet": "^5.10.0",
"gatsby-plugin-remove-trailing-slashes": "^4.9.0",
"gatsby-plugin-styled-components": "^5.19.0",
"gatsby-plugin-typescript": "^4.11.1",
"gatsby": "3.13.1",
"gatsby-plugin-catch-links": "^3.13.0",
"gatsby-plugin-page-creator": "^3.13.0",
"gatsby-plugin-react-helmet": "^4.13.0",
"gatsby-plugin-remove-trailing-slashes": "^3.13.0",
"gatsby-plugin-styled-components": "^4.13.0",
"gatsby-plugin-typescript": "^3.2.0",
"humanize-string": "^2.1.0",
"icedfrisby": "4.0.0",
"icedfrisby-nock": "^2.1.0",
"is-svg": "^4.3.2",
"is-svg": "^4.3.1",
"js-yaml-loader": "^1.2.2",
"jsdoc": "^3.6.11",
"lint-staged": "^13.0.3",
"jsdoc": "^3.6.7",
"lint-staged": "^11.1.2",
"lodash.debounce": "^4.0.8",
"lodash.difference": "^4.5.0",
"minimist": "^1.2.6",
"mocha": "^9.2.2",
"minimist": "^1.2.5",
"mocha": "^9.1.1",
"mocha-env-reporter": "^4.0.0",
"mocha-junit-reporter": "^2.0.2",
"mocha-junit-reporter": "^2.0.0",
"mocha-yaml-loader": "^1.0.3",
"nock": "13.2.9",
"node-mocks-http": "^1.11.0",
"nodemon": "^2.0.19",
"nock": "13.1.3",
"node-mocks-http": "^1.10.1",
"nodemon": "^2.0.12",
"npm-run-all": "^4.1.5",
"open-cli": "^7.0.1",
"portfinder": "^1.0.28",
"prettier": "2.7.1",
"prettier": "2.4.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-error-overlay": "^6.0.11",
"react-error-overlay": "^6.0.9",
"react-helmet": "^6.1.0",
"react-modal": "^3.15.1",
"react-modal": "^3.14.3",
"react-pose": "^4.0.10",
"react-select": "^4.3.1",
"read-all-stdin-sync": "^1.0.5",
"redis-server": "^1.2.2",
"rimraf": "^3.0.2",
"sazerac": "^2.0.0",
"simple-git-hooks": "^2.8.0",
"sinon": "^14.0.0",
"simple-git-hooks": "^2.6.1",
"sinon": "^11.1.2",
"sinon-chai": "^3.7.0",
"snap-shot-it": "^7.9.6",
"start-server-and-test": "1.14.0",
"styled-components": "^5.3.5",
"ts-mocha": "^10.0.0",
"tsd": "^0.22.0",
"typescript": "^4.7.4",
"url": "^0.11.0"
"styled-components": "^5.3.1",
"ts-mocha": "^8.0.0",
"tsd": "^0.17.0",
"typescript": "^4.4.3"
},
"engines": {
"node": "^16.13.0",
"npm": ">=8.0.0"
"node": "^14.17.1",
"npm": ">=7.0.0"
},
"type": "module",
"babel": {
"env": {
"test": {
"plugins": [
"istanbul"
]
}
}
},
"collective": {
"type": "opencollective",
"url": "https://opencollective.com/shields",

View File

@@ -0,0 +1,19 @@
#!/usr/bin/env fish
#
# Back up the GitHub tokens from each production server.
#
if test (count $argv) -lt 1
echo Usage: (basename (status -f)) shields_secret
end
set shields_secret $argv[1]
function do_backup
set server $argv[1]
curl --insecure -u ":$shields_secret" "https://$server.servers.shields.io/\$github-auth/tokens" > "$server""_tokens.json"
end
for server in s0 s1 s2
do_backup $server
end

View File

@@ -21,7 +21,7 @@ class BaseAmoService extends BaseJsonService {
async fetch({ addonId }) {
return this._requestJson({
schema,
url: `https://addons.mozilla.org/api/v4/addons/addon/${addonId}/`,
url: `https://addons.mozilla.org/api/v3/addons/addon/${addonId}`,
})
}
}

View File

@@ -1,4 +1,5 @@
import { renderDownloadsBadge } from '../downloads.js'
import { metric } from '../text-formatters.js'
import { downloadCount } from '../color-formatters.js'
import { redirector } from '../index.js'
import { BaseAmoService, keywords } from './amo-base.js'
@@ -24,12 +25,13 @@ class AmoWeeklyDownloads extends BaseAmoService {
},
]
static _cacheLength = 21600
static defaultBadgeData = { label: 'downloads' }
static render({ downloads }) {
return renderDownloadsBadge({ downloads, interval: 'week' })
return {
message: `${metric(downloads)}/week`,
color: downloadCount(downloads),
}
}
async handle({ addonId }) {

View File

@@ -23,8 +23,6 @@ export default class AmoRating extends BaseAmoService {
},
]
static _cacheLength = 7200
static render({ format, rating }) {
rating = Math.round(rating)
return {

View File

@@ -1,4 +1,4 @@
import { renderDownloadsBadge } from '../downloads.js'
import { metric } from '../text-formatters.js'
import { BaseAmoService, keywords } from './amo-base.js'
export default class AmoUsers extends BaseAmoService {
@@ -14,12 +14,13 @@ export default class AmoUsers extends BaseAmoService {
},
]
static _cacheLength = 21600
static defaultBadgeData = { label: 'users' }
static render({ users: downloads }) {
return renderDownloadsBadge({ downloads, colorOverride: 'blue' })
static render({ users }) {
return {
message: metric(users),
color: 'blue',
}
}
async handle({ addonId }) {

View File

@@ -1,5 +1,6 @@
import Joi from 'joi'
import { renderDownloadsBadge } from '../downloads.js'
import { downloadCount } from '../color-formatters.js'
import { metric } from '../text-formatters.js'
import { nonNegativeInteger } from '../validators.js'
import { BaseJsonService } from '../index.js'
@@ -31,15 +32,22 @@ class AnsibleGalaxyRoleDownloads extends AnsibleGalaxyRole {
{
title: 'Ansible Role',
namedParams: { roleId: '3078' },
staticPreview: renderDownloadsBadge({ downloads: 76 }),
staticPreview: this.render({ downloads: 76 }),
},
]
static defaultBadgeData = { label: 'role downloads' }
static render({ downloads }) {
return {
message: metric(downloads),
color: downloadCount(downloads),
}
}
async handle({ roleId }) {
const json = await this.fetch({ roleId })
return renderDownloadsBadge({ downloads: json.download_count })
return this.constructor.render({ downloads: json.download_count })
}
}

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