Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
046856c056 | ||
|
|
851a30be39 | ||
|
|
785ee090a9 | ||
|
|
0002d6749e | ||
|
|
fb379c0556 |
@@ -143,7 +143,7 @@ package_steps: &package_steps
|
||||
jobs:
|
||||
main:
|
||||
docker:
|
||||
- image: cimg/node:16.14
|
||||
- image: circleci/node:16
|
||||
environment:
|
||||
NPM_CONFIG_ENGINE_STRICT: 'true'
|
||||
NPM_CONFIG_STRICT_PEER_DEPS: 'true'
|
||||
@@ -152,13 +152,13 @@ jobs:
|
||||
|
||||
main@node-17:
|
||||
docker:
|
||||
- image: cimg/node:17.7
|
||||
- image: circleci/node:17
|
||||
|
||||
<<: *main_steps
|
||||
|
||||
integration:
|
||||
docker:
|
||||
- image: cimg/node:16.14
|
||||
- image: circleci/node:16
|
||||
environment:
|
||||
NPM_CONFIG_ENGINE_STRICT: 'true'
|
||||
NPM_CONFIG_STRICT_PEER_DEPS: 'true'
|
||||
@@ -168,14 +168,14 @@ jobs:
|
||||
|
||||
integration@node-17:
|
||||
docker:
|
||||
- image: cimg/node:17.7
|
||||
- image: circleci/node:17
|
||||
- image: redis
|
||||
|
||||
<<: *integration_steps
|
||||
|
||||
danger:
|
||||
docker:
|
||||
- image: cimg/node:16.14
|
||||
- image: circleci/node:16
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
@@ -195,7 +195,7 @@ jobs:
|
||||
|
||||
frontend:
|
||||
docker:
|
||||
- image: cimg/node:16.14
|
||||
- image: circleci/node:16
|
||||
environment:
|
||||
NPM_CONFIG_ENGINE_STRICT: 'true'
|
||||
NPM_CONFIG_STRICT_PEER_DEPS: 'true'
|
||||
@@ -235,14 +235,13 @@ jobs:
|
||||
command: npm run build
|
||||
|
||||
package:
|
||||
machine:
|
||||
image: 'ubuntu-2004:202111-02'
|
||||
machine: true
|
||||
|
||||
<<: *package_steps
|
||||
|
||||
services:
|
||||
docker:
|
||||
- image: cimg/node:16.14
|
||||
- image: circleci/node:16
|
||||
environment:
|
||||
NPM_CONFIG_ENGINE_STRICT: 'true'
|
||||
NPM_CONFIG_STRICT_PEER_DEPS: 'true'
|
||||
@@ -251,7 +250,7 @@ jobs:
|
||||
|
||||
services@node-17:
|
||||
docker:
|
||||
- image: cimg/node:17.7
|
||||
- image: circleci/node:17
|
||||
|
||||
<<: *services_steps
|
||||
|
||||
|
||||
190
.github/actions/close-bot/package-lock.json
generated
vendored
190
.github/actions/close-bot/package-lock.json
generated
vendored
@@ -10,7 +10,7 @@
|
||||
"license": "CC0",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.6.0",
|
||||
"@actions/github": "^5.0.1"
|
||||
"@actions/github": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/core": {
|
||||
@@ -22,14 +22,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/github": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@actions/github/-/github-5.0.1.tgz",
|
||||
"integrity": "sha512-JZGyPM9ektb8NVTTI/2gfJ9DL7Rk98tQ7OVyTlgTuaQroariRBsOnzjy0I2EarX4xUZpK88YyO503fhmjFdyAg==",
|
||||
"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": "^1.0.11",
|
||||
"@octokit/core": "^3.6.0",
|
||||
"@octokit/plugin-paginate-rest": "^2.17.0",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^5.13.0"
|
||||
"@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": {
|
||||
@@ -41,21 +41,21 @@
|
||||
}
|
||||
},
|
||||
"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 +63,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 +73,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 +111,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 +134,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",
|
||||
@@ -234,14 +234,14 @@
|
||||
}
|
||||
},
|
||||
"@actions/github": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@actions/github/-/github-5.0.1.tgz",
|
||||
"integrity": "sha512-JZGyPM9ektb8NVTTI/2gfJ9DL7Rk98tQ7OVyTlgTuaQroariRBsOnzjy0I2EarX4xUZpK88YyO503fhmjFdyAg==",
|
||||
"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": "^1.0.11",
|
||||
"@octokit/core": "^3.6.0",
|
||||
"@octokit/plugin-paginate-rest": "^2.17.0",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^5.13.0"
|
||||
"@octokit/core": "^3.4.0",
|
||||
"@octokit/plugin-paginate-rest": "^2.13.3",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^5.1.1"
|
||||
}
|
||||
},
|
||||
"@actions/http-client": {
|
||||
@@ -253,21 +253,21 @@
|
||||
}
|
||||
},
|
||||
"@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 +275,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 +285,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 +340,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",
|
||||
|
||||
2
.github/actions/close-bot/package.json
vendored
2
.github/actions/close-bot/package.json
vendored
@@ -11,6 +11,6 @@
|
||||
"license": "CC0",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.6.0",
|
||||
"@actions/github": "^5.0.1"
|
||||
"@actions/github": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
12
.github/actions/tester/action.yml
vendored
Normal file
12
.github/actions/tester/action.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
name: 'Tester'
|
||||
description: 'Just for debugging purposes'
|
||||
branding:
|
||||
icon: 'check-circle'
|
||||
color: 'green'
|
||||
inputs:
|
||||
build-args:
|
||||
description: 'List of build-time variables'
|
||||
required: true
|
||||
runs:
|
||||
using: 'node12'
|
||||
main: 'index.js'
|
||||
10
.github/actions/tester/index.js
vendored
Normal file
10
.github/actions/tester/index.js
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
'use strict'
|
||||
|
||||
const core = require('@actions/core')
|
||||
|
||||
async function run() {
|
||||
const buildArgs = await core.getInput('build-args', true)
|
||||
console.log(buildArgs)
|
||||
}
|
||||
|
||||
run()
|
||||
416
.github/actions/tester/package-lock.json
generated
vendored
Normal file
416
.github/actions/tester/package-lock.json
generated
vendored
Normal file
@@ -0,0 +1,416 @@
|
||||
{
|
||||
"name": "close-bot",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "close-bot",
|
||||
"version": "0.0.0",
|
||||
"license": "CC0",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.6.0",
|
||||
"@actions/github": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/core": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.6.0.tgz",
|
||||
"integrity": "sha512-NB1UAZomZlCV/LmJqkLhNTqtKfFXJZAUPcfl/zqG7EfsQdeUJtaWO98SGbuQ3pydJ3fHl2CvI/51OKYlCYYcaw==",
|
||||
"dependencies": {
|
||||
"@actions/http-client": "^1.0.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/github": {
|
||||
"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": "^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": "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"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/auth-token": {
|
||||
"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.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.4.12",
|
||||
"@octokit/request-error": "^2.0.5",
|
||||
"@octokit/types": "^6.0.3",
|
||||
"before-after-hook": "^2.2.0",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/endpoint": {
|
||||
"version": "6.0.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",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/graphql": {
|
||||
"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.3.0",
|
||||
"@octokit/types": "^6.0.3",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/openapi-types": {
|
||||
"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.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.11.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@octokit/core": ">=2"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/plugin-rest-endpoint-methods": {
|
||||
"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.14.1",
|
||||
"deprecation": "^2.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@octokit/core": ">=3"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/request": {
|
||||
"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.0.0",
|
||||
"@octokit/types": "^6.7.1",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/request-error": {
|
||||
"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",
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/types": {
|
||||
"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": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/before-after-hook": {
|
||||
"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",
|
||||
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
|
||||
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
|
||||
},
|
||||
"node_modules/is-plain-object": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
||||
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
|
||||
},
|
||||
"node_modules/tunnel": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
|
||||
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
|
||||
"engines": {
|
||||
"node": ">=0.6.11 <=0.7.0 || >=0.7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/universal-user-agent": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
|
||||
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.6.0.tgz",
|
||||
"integrity": "sha512-NB1UAZomZlCV/LmJqkLhNTqtKfFXJZAUPcfl/zqG7EfsQdeUJtaWO98SGbuQ3pydJ3fHl2CvI/51OKYlCYYcaw==",
|
||||
"requires": {
|
||||
"@actions/http-client": "^1.0.11"
|
||||
}
|
||||
},
|
||||
"@actions/github": {
|
||||
"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": "^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": "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"
|
||||
}
|
||||
},
|
||||
"@octokit/auth-token": {
|
||||
"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.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.4.12",
|
||||
"@octokit/request-error": "^2.0.5",
|
||||
"@octokit/types": "^6.0.3",
|
||||
"before-after-hook": "^2.2.0",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"@octokit/endpoint": {
|
||||
"version": "6.0.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",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"@octokit/graphql": {
|
||||
"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.3.0",
|
||||
"@octokit/types": "^6.0.3",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"@octokit/openapi-types": {
|
||||
"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.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.11.0"
|
||||
}
|
||||
},
|
||||
"@octokit/plugin-rest-endpoint-methods": {
|
||||
"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.14.1",
|
||||
"deprecation": "^2.3.1"
|
||||
}
|
||||
},
|
||||
"@octokit/request": {
|
||||
"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.0.0",
|
||||
"@octokit/types": "^6.7.1",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"@octokit/request-error": {
|
||||
"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",
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"@octokit/types": {
|
||||
"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": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"before-after-hook": {
|
||||
"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",
|
||||
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
|
||||
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
|
||||
},
|
||||
"is-plain-object": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
||||
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
|
||||
"requires": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
|
||||
},
|
||||
"tunnel": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
|
||||
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
|
||||
},
|
||||
"universal-user-agent": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
|
||||
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
|
||||
},
|
||||
"webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
|
||||
},
|
||||
"whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
|
||||
"requires": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
}
|
||||
}
|
||||
}
|
||||
16
.github/actions/tester/package.json
vendored
Normal file
16
.github/actions/tester/package.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "close-bot",
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "chris48s",
|
||||
"license": "CC0",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.6.0",
|
||||
"@actions/github": "^5.0.0"
|
||||
}
|
||||
}
|
||||
5
.github/workflows/build-docker-image.yml
vendored
5
.github/workflows/build-docker-image.yml
vendored
@@ -12,9 +12,6 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Set Git Short SHA
|
||||
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
@@ -22,4 +19,4 @@ jobs:
|
||||
push: false
|
||||
tags: shieldsio/shields:pr-validation
|
||||
build-args: |
|
||||
version=${{ env.SHORT_SHA }}
|
||||
version=${GITHUB_SHA::7}
|
||||
|
||||
14
.github/workflows/enforce-dependency-review.yml
vendored
14
.github/workflows/enforce-dependency-review.yml
vendored
@@ -1,14 +0,0 @@
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v1
|
||||
5
.github/workflows/publish-docker-next.yml
vendored
5
.github/workflows/publish-docker-next.yml
vendored
@@ -20,9 +20,6 @@ jobs:
|
||||
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@v2
|
||||
with:
|
||||
@@ -30,4 +27,4 @@ jobs:
|
||||
push: true
|
||||
tags: shieldsio/shields:next
|
||||
build-args: |
|
||||
version=${{ env.SHORT_SHA }}
|
||||
version=${GITHUB_SHA::7}
|
||||
|
||||
20
.github/workflows/test-build-args.yml
vendored
Normal file
20
.github/workflows/test-build-args.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: Tester
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install action dependencies
|
||||
run: cd .github/actions/tester && npm ci
|
||||
|
||||
- name: Set Git Short SHA
|
||||
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
|
||||
- uses: ./.github/actions/tester
|
||||
with:
|
||||
build-args: |
|
||||
version=${{ env.SHORT_SHA }}
|
||||
47
CHANGELOG.md
47
CHANGELOG.md
@@ -4,53 +4,6 @@ Note: this changelog is for the shields.io server. The changelog for the badge-m
|
||||
|
||||
---
|
||||
|
||||
## 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)
|
||||
|
||||
@@ -23,8 +23,6 @@ FROM node:16-alpine
|
||||
|
||||
ARG version=dev
|
||||
ENV DOCKER_SHIELDS_VERSION=$version
|
||||
LABEL version=$version
|
||||
LABEL fly.version=$version
|
||||
|
||||
# Run the server using production configs.
|
||||
ENV NODE_ENV production
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ Please follow this guidance when reporting security issues affecting:
|
||||
- The [squint](https://github.com/badges/squint) raster proxy
|
||||
- The [badge-maker](https://www.npmjs.com/package/badge-maker) NPM package
|
||||
|
||||
The [gh-badges](https://www.npmjs.com/package/gh-badges) and [svg-to-image-proxy](https://www.npmjs.com/package/svg-to-image-proxy) NPM packages are now deprecated and will no longer receive fixes for bugs or security issues.
|
||||
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
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict'
|
||||
|
||||
const { toSvgColor } = require('./color')
|
||||
const { normalizeColor, toSvgColor } = require('./color')
|
||||
const badgeRenderers = require('./badge-renderers')
|
||||
const { stripXmlWhitespace } = require('./xml')
|
||||
|
||||
@@ -9,6 +9,7 @@ note: makeBadge() is fairly thinly wrapped so if we are making changes here
|
||||
it is likely this will impact on the package's public interface in index.js
|
||||
*/
|
||||
module.exports = function makeBadge({
|
||||
format,
|
||||
style = 'flat',
|
||||
label,
|
||||
message,
|
||||
@@ -23,6 +24,22 @@ module.exports = function makeBadge({
|
||||
label = `${label}`.trim()
|
||||
message = `${message}`.trim()
|
||||
|
||||
// This ought to be the responsibility of the server, not `makeBadge`.
|
||||
if (format === 'json') {
|
||||
return JSON.stringify({
|
||||
label,
|
||||
message,
|
||||
logoWidth,
|
||||
// Only call normalizeColor for the JSON case: this is handled
|
||||
// internally by toSvgColor in the SVG case.
|
||||
color: normalizeColor(color),
|
||||
labelColor: normalizeColor(labelColor),
|
||||
link: links,
|
||||
name: label,
|
||||
value: message,
|
||||
})
|
||||
}
|
||||
|
||||
const render = badgeRenderers[style]
|
||||
if (!render) {
|
||||
throw new Error(`Unknown badge style: '${style}'`)
|
||||
|
||||
@@ -1,48 +1,143 @@
|
||||
'use strict'
|
||||
|
||||
const { test, given, forCases } = require('sazerac')
|
||||
const { expect } = require('chai')
|
||||
const snapshot = require('snap-shot-it')
|
||||
const isSvg = require('is-svg')
|
||||
const prettier = require('prettier')
|
||||
const makeBadge = require('./make-badge')
|
||||
|
||||
function expectBadgeToMatchSnapshot(badgeData) {
|
||||
snapshot(prettier.format(makeBadge(badgeData), { parser: 'html' }))
|
||||
function expectBadgeToMatchSnapshot(format) {
|
||||
snapshot(prettier.format(makeBadge(format), { parser: 'html' }))
|
||||
}
|
||||
|
||||
function testColor(color = '', colorAttr = 'color') {
|
||||
return JSON.parse(
|
||||
makeBadge({
|
||||
label: 'name',
|
||||
message: 'Bob',
|
||||
[colorAttr]: color,
|
||||
format: 'json',
|
||||
})
|
||||
).color
|
||||
}
|
||||
|
||||
describe('The badge generator', function () {
|
||||
describe('color test', function () {
|
||||
test(testColor, () => {
|
||||
// valid hex
|
||||
forCases([
|
||||
given('#4c1'),
|
||||
given('#4C1'),
|
||||
given('4C1'),
|
||||
given('4c1'),
|
||||
]).expect('#4c1')
|
||||
forCases([
|
||||
given('#abc123'),
|
||||
given('#ABC123'),
|
||||
given('abc123'),
|
||||
given('ABC123'),
|
||||
]).expect('#abc123')
|
||||
// valid rgb(a)
|
||||
given('rgb(0,128,255)').expect('rgb(0,128,255)')
|
||||
given('rgb(220,128,255,0.5)').expect('rgb(220,128,255,0.5)')
|
||||
given('rgba(0,0,255)').expect('rgba(0,0,255)')
|
||||
given('rgba(0,128,255,0)').expect('rgba(0,128,255,0)')
|
||||
// valid hsl(a)
|
||||
given('hsl(100, 56%, 10%)').expect('hsl(100, 56%, 10%)')
|
||||
given('hsl(360,50%,50%,0.5)').expect('hsl(360,50%,50%,0.5)')
|
||||
given('hsla(25,20%,0%,0.1)').expect('hsla(25,20%,0%,0.1)')
|
||||
given('hsla(0,50%,101%)').expect('hsla(0,50%,101%)')
|
||||
// CSS named color.
|
||||
given('papayawhip').expect('papayawhip')
|
||||
// Shields named color.
|
||||
given('red').expect('red')
|
||||
given('green').expect('green')
|
||||
given('blue').expect('blue')
|
||||
given('yellow').expect('yellow')
|
||||
// Semantic color alias
|
||||
given('success').expect('brightgreen')
|
||||
given('informational').expect('blue')
|
||||
|
||||
forCases(
|
||||
// invalid hex
|
||||
given('#123red'), // contains letter above F
|
||||
given('#red'), // contains letter above F
|
||||
// neither a css named color nor colorscheme
|
||||
given('notacolor'),
|
||||
given('bluish'),
|
||||
given('almostred'),
|
||||
given('brightmaroon'),
|
||||
given('cactus')
|
||||
).expect(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe('color aliases', function () {
|
||||
test(testColor, () => {
|
||||
forCases([given('#4c1', 'color')]).expect('#4c1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('SVG', function () {
|
||||
it('should produce SVG', function () {
|
||||
expect(makeBadge({ label: 'cactus', message: 'grown' }))
|
||||
expect(makeBadge({ label: 'cactus', message: 'grown', format: 'svg' }))
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('cactus')
|
||||
.and.to.include('grown')
|
||||
})
|
||||
|
||||
it('should match snapshot', function () {
|
||||
expectBadgeToMatchSnapshot({ label: 'cactus', message: 'grown' })
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('JSON', function () {
|
||||
it('should produce the expected JSON', function () {
|
||||
const json = makeBadge({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'json',
|
||||
links: ['https://example.com/', 'https://other.example.com/'],
|
||||
})
|
||||
expect(JSON.parse(json)).to.deep.equal({
|
||||
name: 'cactus',
|
||||
label: 'cactus',
|
||||
value: 'grown',
|
||||
message: 'grown',
|
||||
link: ['https://example.com/', 'https://other.example.com/'],
|
||||
})
|
||||
})
|
||||
|
||||
it('should replace undefined svg badge style with "flat"', function () {
|
||||
expect(
|
||||
makeBadge({
|
||||
label: 'name',
|
||||
message: 'Bob',
|
||||
})
|
||||
)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.equal(
|
||||
makeBadge({
|
||||
label: 'name',
|
||||
message: 'Bob',
|
||||
style: 'flat',
|
||||
})
|
||||
)
|
||||
const jsonBadgeWithUnknownStyle = makeBadge({
|
||||
label: 'name',
|
||||
message: 'Bob',
|
||||
format: 'svg',
|
||||
})
|
||||
const jsonBadgeWithDefaultStyle = makeBadge({
|
||||
label: 'name',
|
||||
message: 'Bob',
|
||||
format: 'svg',
|
||||
style: 'flat',
|
||||
})
|
||||
expect(jsonBadgeWithUnknownStyle)
|
||||
.to.equal(jsonBadgeWithDefaultStyle)
|
||||
.and.to.satisfy(isSvg)
|
||||
})
|
||||
|
||||
it('should fail with unknown svg badge style', function () {
|
||||
expect(() =>
|
||||
makeBadge({ label: 'name', message: 'Bob', style: 'unknown_style' })
|
||||
makeBadge({
|
||||
label: 'name',
|
||||
message: 'Bob',
|
||||
format: 'svg',
|
||||
style: 'unknown_style',
|
||||
})
|
||||
).to.throw(Error, "Unknown badge style: 'unknown_style'")
|
||||
})
|
||||
})
|
||||
@@ -52,6 +147,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -62,6 +158,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -73,6 +170,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat',
|
||||
color: '#b3e',
|
||||
})
|
||||
@@ -82,6 +180,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat',
|
||||
color: '#b3e',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
@@ -92,6 +191,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -103,6 +203,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -114,6 +215,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat',
|
||||
color: '#000',
|
||||
labelColor: '#f3f3f3',
|
||||
@@ -124,6 +226,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat',
|
||||
color: '#e2ffe1',
|
||||
labelColor: '#000',
|
||||
@@ -136,6 +239,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat-square',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -146,6 +250,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat-square',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -157,6 +262,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat-square',
|
||||
color: '#b3e',
|
||||
})
|
||||
@@ -166,6 +272,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat-square',
|
||||
color: '#b3e',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
@@ -176,6 +283,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat-square',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -187,6 +295,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat-square',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -198,6 +307,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat-square',
|
||||
color: '#000',
|
||||
labelColor: '#f3f3f3',
|
||||
@@ -208,6 +318,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat-square',
|
||||
color: '#e2ffe1',
|
||||
labelColor: '#000',
|
||||
@@ -220,6 +331,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'plastic',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -230,6 +342,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'plastic',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -241,6 +354,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'plastic',
|
||||
color: '#b3e',
|
||||
})
|
||||
@@ -250,6 +364,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'plastic',
|
||||
color: '#b3e',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
@@ -260,6 +375,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'plastic',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -271,6 +387,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'plastic',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -282,6 +399,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'plastic',
|
||||
color: '#000',
|
||||
labelColor: '#f3f3f3',
|
||||
@@ -292,6 +410,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'plastic',
|
||||
color: '#e2ffe1',
|
||||
labelColor: '#000',
|
||||
@@ -306,6 +425,7 @@ describe('The badge generator', function () {
|
||||
makeBadge({
|
||||
label: 1998,
|
||||
message: 1999,
|
||||
format: 'svg',
|
||||
style: 'for-the-badge',
|
||||
})
|
||||
)
|
||||
@@ -318,6 +438,7 @@ describe('The badge generator', function () {
|
||||
makeBadge({
|
||||
label: 'Label',
|
||||
message: '1 string',
|
||||
format: 'svg',
|
||||
style: 'for-the-badge',
|
||||
})
|
||||
)
|
||||
@@ -329,6 +450,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'for-the-badge',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -339,6 +461,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'for-the-badge',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -350,6 +473,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'for-the-badge',
|
||||
color: '#b3e',
|
||||
})
|
||||
@@ -359,6 +483,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'for-the-badge',
|
||||
color: '#b3e',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
@@ -369,6 +494,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'for-the-badge',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -380,6 +506,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'for-the-badge',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -391,6 +518,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'for-the-badge',
|
||||
color: '#000',
|
||||
labelColor: '#f3f3f3',
|
||||
@@ -401,6 +529,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'for-the-badge',
|
||||
color: '#e2ffe1',
|
||||
labelColor: '#000',
|
||||
@@ -414,6 +543,7 @@ describe('The badge generator', function () {
|
||||
makeBadge({
|
||||
label: 'some-key',
|
||||
message: 'some-value',
|
||||
format: 'svg',
|
||||
style: 'social',
|
||||
})
|
||||
)
|
||||
@@ -427,10 +557,11 @@ describe('The badge generator', function () {
|
||||
makeBadge({
|
||||
label: '',
|
||||
message: 'some-value',
|
||||
format: 'json',
|
||||
style: 'social',
|
||||
})
|
||||
)
|
||||
.to.include('></text>')
|
||||
.to.include('""')
|
||||
.and.to.include('some-value')
|
||||
})
|
||||
|
||||
@@ -438,6 +569,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'social',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -448,6 +580,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'social',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -459,6 +592,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'social',
|
||||
color: '#b3e',
|
||||
})
|
||||
@@ -468,6 +602,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'social',
|
||||
color: '#b3e',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
@@ -478,6 +613,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'social',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -489,6 +625,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'social',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
@@ -502,6 +639,7 @@ describe('The badge generator', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
label: 'label',
|
||||
message: 'message',
|
||||
format: 'svg',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,29 +1,58 @@
|
||||
import makeBadge from '../../badge-maker/lib/make-badge.js'
|
||||
import BaseService from './base.js'
|
||||
import {
|
||||
serverHasBeenUpSinceResourceCached,
|
||||
setCacheHeadersForStaticResource,
|
||||
} from './cache-headers.js'
|
||||
import { prepareRoute } from './route.js'
|
||||
import { makeSend } from './legacy-result-sender.js'
|
||||
import { MetricHelper } from './metric-helper.js'
|
||||
import coalesceBadge from './coalesce-badge.js'
|
||||
import { prepareRoute, namedParamsForMatch } from './route.js'
|
||||
|
||||
export default class BaseStaticService extends BaseService {
|
||||
static _applyCacheHeaders({ res }) {
|
||||
setCacheHeadersForStaticResource(res)
|
||||
}
|
||||
static register({ camp, metricInstance }, serviceConfig) {
|
||||
const { regex, captureNames } = prepareRoute(this.route)
|
||||
|
||||
static register({ app, ...serviceContext }, serviceConfig) {
|
||||
const { regex } = prepareRoute(this.route)
|
||||
app.get(
|
||||
regex,
|
||||
(req, res, next) => {
|
||||
if (serverHasBeenUpSinceResourceCached(req)) {
|
||||
// Send Not Modified.
|
||||
res.status(304)
|
||||
res.end()
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
},
|
||||
this.makeExpressHandler(serviceContext, serviceConfig)
|
||||
)
|
||||
const metricHelper = MetricHelper.create({
|
||||
metricInstance,
|
||||
ServiceClass: this,
|
||||
})
|
||||
|
||||
camp.route(regex, async (queryParams, match, end, ask) => {
|
||||
if (serverHasBeenUpSinceResourceCached(ask.req)) {
|
||||
// Send Not Modified.
|
||||
ask.res.statusCode = 304
|
||||
ask.res.end()
|
||||
return
|
||||
}
|
||||
|
||||
const metricHandle = metricHelper.startRequest()
|
||||
|
||||
const namedParams = namedParamsForMatch(captureNames, match, this)
|
||||
const serviceData = await this.invoke(
|
||||
{},
|
||||
serviceConfig,
|
||||
namedParams,
|
||||
queryParams
|
||||
)
|
||||
|
||||
const badgeData = coalesceBadge(
|
||||
queryParams,
|
||||
serviceData,
|
||||
this.defaultBadgeData,
|
||||
this
|
||||
)
|
||||
|
||||
// The final capture group is the extension.
|
||||
const format = (match.slice(-1)[0] || '.svg').replace(/^\./, '')
|
||||
badgeData.format = format
|
||||
|
||||
setCacheHeadersForStaticResource(ask.res)
|
||||
|
||||
const svg = makeBadge(badgeData)
|
||||
makeSend(format, ask.res, end)(svg)
|
||||
|
||||
metricHandle.noteResponseSent()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,8 @@
|
||||
import emojic from 'emojic'
|
||||
import Joi from 'joi'
|
||||
import log from '../server/log.js'
|
||||
import makeBadge from '../../badge-maker/lib/make-badge.js'
|
||||
import { AuthHelper } from './auth-helper.js'
|
||||
import { MetricHelper, MetricNames } from './metric-helper.js'
|
||||
import {
|
||||
coalesceCacheLength,
|
||||
setHeadersForCacheLength,
|
||||
} from './cache-headers.js'
|
||||
import { assertValidCategory } from './categories.js'
|
||||
import checkErrorResponse from './check-error-response.js'
|
||||
import coalesceBadge from './coalesce-badge.js'
|
||||
@@ -26,12 +21,11 @@ import {
|
||||
} from './errors.js'
|
||||
import { validateExample, transformExample } from './examples.js'
|
||||
import { fetch } from './got.js'
|
||||
import { makeJsonBadge } from './make-json-badge.js'
|
||||
import {
|
||||
makeFullUrl,
|
||||
assertValidRoute,
|
||||
paramsForReq,
|
||||
prepareRoute,
|
||||
namedParamsForMatch,
|
||||
getQueryParamNames,
|
||||
} from './route.js'
|
||||
import { assertValidServiceDefinition } from './service-definitions.js'
|
||||
@@ -153,7 +147,6 @@ class BaseService {
|
||||
version: 300,
|
||||
debug: 60,
|
||||
downloads: 900,
|
||||
rating: 900,
|
||||
social: 900,
|
||||
}
|
||||
return cacheLengths[this.category]
|
||||
@@ -429,90 +422,60 @@ class BaseService {
|
||||
return serviceData
|
||||
}
|
||||
|
||||
// `defaultCacheLengthSeconds` can be overridden by
|
||||
// `serviceDefaultCacheLengthSeconds` (either by category or on a badge-
|
||||
// by-badge basis). Then in turn that can be overridden by
|
||||
// `serviceOverrideCacheLengthSeconds` (which we expect to be used only in
|
||||
// the dynamic badge) but only if `serviceOverrideCacheLengthSeconds` is
|
||||
// longer than `serviceDefaultCacheLengthSeconds` and then the `cacheSeconds`
|
||||
// query param can also override both of those but again only if `cacheSeconds`
|
||||
// is longer.
|
||||
//
|
||||
// Ref: https://github.com/badges/shields/pull/2755
|
||||
static _applyCacheHeaders({
|
||||
cacheHeaderConfig,
|
||||
req,
|
||||
res,
|
||||
serviceOverrideCacheLengthSeconds,
|
||||
}) {
|
||||
const cacheLengthSeconds = coalesceCacheLength({
|
||||
cacheHeaderConfig,
|
||||
serviceDefaultCacheLengthSeconds: this._cacheLength,
|
||||
serviceOverrideCacheLengthSeconds,
|
||||
queryParams: req.query,
|
||||
})
|
||||
setHeadersForCacheLength(res, cacheLengthSeconds)
|
||||
}
|
||||
|
||||
static makeExpressHandler(
|
||||
{ githubApiProvider, librariesIoApiProvider, metricInstance },
|
||||
static register(
|
||||
{
|
||||
camp,
|
||||
handleRequest,
|
||||
githubApiProvider,
|
||||
librariesIoApiProvider,
|
||||
metricInstance,
|
||||
},
|
||||
serviceConfig
|
||||
) {
|
||||
const { cacheHeaders: cacheHeaderConfig } = serviceConfig
|
||||
const { regex, captureNames } = prepareRoute(this.route)
|
||||
const queryParams = getQueryParamNames(this.route)
|
||||
|
||||
const metricHelper = MetricHelper.create({
|
||||
metricInstance,
|
||||
ServiceClass: this,
|
||||
})
|
||||
const { captureNames } = prepareRoute(this.route)
|
||||
const { cacheHeaders: cacheHeaderConfig } = serviceConfig
|
||||
|
||||
return async (req, res) => {
|
||||
const metricHandle = metricHelper.startRequest()
|
||||
camp.route(
|
||||
regex,
|
||||
handleRequest(cacheHeaderConfig, {
|
||||
queryParams,
|
||||
handler: async (queryParams, match, sendBadge) => {
|
||||
const metricHandle = metricHelper.startRequest()
|
||||
|
||||
const { namedParams, format } = paramsForReq(captureNames, req, this)
|
||||
const serviceData = await this.invoke(
|
||||
{
|
||||
requestFetcher: fetch,
|
||||
githubApiProvider,
|
||||
librariesIoApiProvider,
|
||||
metricHelper,
|
||||
const namedParams = namedParamsForMatch(captureNames, match, this)
|
||||
const serviceData = await this.invoke(
|
||||
{
|
||||
requestFetcher: fetch,
|
||||
githubApiProvider,
|
||||
librariesIoApiProvider,
|
||||
metricHelper,
|
||||
},
|
||||
serviceConfig,
|
||||
namedParams,
|
||||
queryParams
|
||||
)
|
||||
|
||||
const badgeData = coalesceBadge(
|
||||
queryParams,
|
||||
serviceData,
|
||||
this.defaultBadgeData,
|
||||
this
|
||||
)
|
||||
// The final capture group is the extension.
|
||||
const format = (match.slice(-1)[0] || '.svg').replace(/^\./, '')
|
||||
sendBadge(format, badgeData)
|
||||
|
||||
metricHandle.noteResponseSent()
|
||||
},
|
||||
serviceConfig,
|
||||
namedParams,
|
||||
req.query
|
||||
)
|
||||
|
||||
const badgeData = coalesceBadge(
|
||||
req.query,
|
||||
serviceData,
|
||||
this.defaultBadgeData,
|
||||
this
|
||||
)
|
||||
|
||||
this._applyCacheHeaders({
|
||||
cacheHeaderConfig,
|
||||
req,
|
||||
res,
|
||||
serviceOverrideCacheLengthSeconds: badgeData.cacheLengthSeconds,
|
||||
cacheLength: this._cacheLength,
|
||||
})
|
||||
|
||||
if (format === 'svg') {
|
||||
res.setHeader('Content-Type', 'image/svg+xml')
|
||||
res.send(makeBadge(badgeData))
|
||||
} else if (format === 'json') {
|
||||
res.json(makeJsonBadge(badgeData))
|
||||
} else {
|
||||
throw Error(`Unrecognized format: ${format}`)
|
||||
}
|
||||
|
||||
res.end()
|
||||
|
||||
metricHandle.noteResponseSent()
|
||||
}
|
||||
}
|
||||
|
||||
static register({ app, ...serviceContext }, serviceConfig) {
|
||||
const { regex } = prepareRoute(this.route)
|
||||
app.get(regex, this.makeExpressHandler(serviceContext, serviceConfig))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import Joi from 'joi'
|
||||
import chai from 'chai'
|
||||
import isSvg from 'is-svg'
|
||||
import sinon from 'sinon'
|
||||
import prometheus from 'prom-client'
|
||||
import chaiAsPromised from 'chai-as-promised'
|
||||
import PrometheusMetrics from '../server/prometheus-metrics.js'
|
||||
import { ExpressTestHarness } from '../express-test-harness.js'
|
||||
import trace from './trace.js'
|
||||
import {
|
||||
NotFound,
|
||||
@@ -17,7 +15,6 @@ import {
|
||||
import BaseService from './base.js'
|
||||
import { MetricHelper, MetricNames } from './metric-helper.js'
|
||||
import '../register-chai-plugins.spec.js'
|
||||
|
||||
const { expect } = chai
|
||||
chai.use(chaiAsPromised)
|
||||
|
||||
@@ -62,12 +59,9 @@ class DummyServiceWithServiceResponseSizeMetricEnabled extends DummyService {
|
||||
|
||||
describe('BaseService', function () {
|
||||
const defaultConfig = {
|
||||
handleInternalErrors: false,
|
||||
cacheHeaders: { defaultCacheLengthSeconds: 120 },
|
||||
public: {
|
||||
handleInternalErrors: false,
|
||||
services: {},
|
||||
cacheHeaders: { defaultCacheLengthSeconds: 120 },
|
||||
},
|
||||
private: {},
|
||||
}
|
||||
@@ -327,45 +321,62 @@ describe('BaseService', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Express integration', function () {
|
||||
let harness
|
||||
beforeEach(async function () {
|
||||
harness = new ExpressTestHarness()
|
||||
DummyService.register({ app: harness.app }, defaultConfig)
|
||||
await harness.start()
|
||||
})
|
||||
describe('ScoutCamp integration', function () {
|
||||
// TODO Strangly, without the useless escape the regexes do not match in Node 12.
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const expectedRouteRegex = /^\/foo(?:\/([^\/#\?]+?))(|\.svg|\.json)$/
|
||||
|
||||
afterEach(async function () {
|
||||
await harness.stop()
|
||||
})
|
||||
let mockCamp
|
||||
let mockHandleRequest
|
||||
|
||||
it('fulfills the request for an SVG badge', async function () {
|
||||
const { headers, body } = await harness.get(
|
||||
'/foo/bar.svg?queryParamA=%3F'
|
||||
beforeEach(function () {
|
||||
mockCamp = {
|
||||
route: sinon.spy(),
|
||||
}
|
||||
mockHandleRequest = sinon.spy()
|
||||
DummyService.register(
|
||||
{ camp: mockCamp, handleRequest: mockHandleRequest },
|
||||
defaultConfig
|
||||
)
|
||||
|
||||
expect(headers).to.include({
|
||||
'content-type': 'image/svg+xml; charset=utf-8',
|
||||
})
|
||||
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('cat: Hello namedParamA: bar with queryParamA: ?')
|
||||
})
|
||||
|
||||
it('fulfills the request for a JSON badge', async function () {
|
||||
const { headers, body } = await harness.get(
|
||||
'/foo/bar.json?queryParamA=%3F',
|
||||
{ responseType: 'json' }
|
||||
)
|
||||
it('registers the service', function () {
|
||||
expect(mockCamp.route).to.have.been.calledOnce
|
||||
expect(mockCamp.route).to.have.been.calledWith(expectedRouteRegex)
|
||||
})
|
||||
|
||||
expect(headers).to.include({
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
})
|
||||
it('handles the request', async function () {
|
||||
expect(mockHandleRequest).to.have.been.calledOnce
|
||||
|
||||
expect(body).to.include({
|
||||
const { queryParams: serviceQueryParams, handler: requestHandler } =
|
||||
mockHandleRequest.getCall(0).args[1]
|
||||
expect(serviceQueryParams).to.deep.equal([
|
||||
'queryParamA',
|
||||
'legacyQueryParamA',
|
||||
])
|
||||
|
||||
const mockSendBadge = sinon.spy()
|
||||
const mockRequest = {
|
||||
asPromise: sinon.spy(),
|
||||
}
|
||||
const queryParams = { queryParamA: '?' }
|
||||
const match = '/foo/bar.svg'.match(expectedRouteRegex)
|
||||
await requestHandler(queryParams, match, mockSendBadge, mockRequest)
|
||||
|
||||
const expectedFormat = 'svg'
|
||||
expect(mockSendBadge).to.have.been.calledOnce
|
||||
expect(mockSendBadge).to.have.been.calledWith(expectedFormat, {
|
||||
label: 'cat',
|
||||
message: 'Hello namedParamA: bar with queryParamA: ?',
|
||||
color: 'lightgrey',
|
||||
style: 'flat',
|
||||
namedLogo: undefined,
|
||||
logo: undefined,
|
||||
logoWidth: undefined,
|
||||
logoPosition: undefined,
|
||||
links: [],
|
||||
labelColor: undefined,
|
||||
cacheLengthSeconds: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -563,7 +574,9 @@ describe('BaseService', function () {
|
||||
},
|
||||
private: {},
|
||||
},
|
||||
{ namedParamA: 'bar.bar.bar' }
|
||||
{
|
||||
namedParamA: 'bar.bar.bar',
|
||||
}
|
||||
)
|
||||
).to.deep.equal({
|
||||
color: 'lightgray',
|
||||
|
||||
143
core/base-service/legacy-request-handler.js
Normal file
143
core/base-service/legacy-request-handler.js
Normal file
@@ -0,0 +1,143 @@
|
||||
import makeBadge from '../../badge-maker/lib/make-badge.js'
|
||||
import { setCacheHeaders } from './cache-headers.js'
|
||||
import { makeSend } from './legacy-result-sender.js'
|
||||
import coalesceBadge from './coalesce-badge.js'
|
||||
|
||||
// These query parameters are available to any badge. They are handled by
|
||||
// `coalesceBadge`.
|
||||
const globalQueryParams = new Set([
|
||||
'label',
|
||||
'style',
|
||||
'link',
|
||||
'logo',
|
||||
'logoColor',
|
||||
'logoPosition',
|
||||
'logoWidth',
|
||||
'link',
|
||||
'colorA',
|
||||
'colorB',
|
||||
'color',
|
||||
'labelColor',
|
||||
])
|
||||
|
||||
function flattenQueryParams(queryParams) {
|
||||
const union = new Set(globalQueryParams)
|
||||
;(queryParams || []).forEach(name => {
|
||||
union.add(name)
|
||||
})
|
||||
return Array.from(union).sort()
|
||||
}
|
||||
|
||||
// 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
|
||||
//
|
||||
// For safety, the service must declare the query parameters it wants to use.
|
||||
// Only the declared parameters (and the global parameters) are provided to
|
||||
// the service. Consequently, failure to declare a parameter results in the
|
||||
// parameter not working at all (which is undesirable, but easy to debug)
|
||||
// rather than indeterminate behavior that depends on the cache state
|
||||
// (undesirable and hard to debug).
|
||||
//
|
||||
// Pass just the handler function as shorthand.
|
||||
function handleRequest(cacheHeaderConfig, handlerOptions) {
|
||||
if (!cacheHeaderConfig) {
|
||||
throw Error('cacheHeaderConfig is required')
|
||||
}
|
||||
|
||||
if (typeof handlerOptions === 'function') {
|
||||
handlerOptions = { handler: handlerOptions }
|
||||
}
|
||||
|
||||
const allowedKeys = flattenQueryParams(handlerOptions.queryParams)
|
||||
const { cacheLength: serviceDefaultCacheLengthSeconds } = handlerOptions
|
||||
|
||||
return (queryParams, match, end, ask) => {
|
||||
/*
|
||||
This is here for legacy reasons. The badge server and frontend used to live
|
||||
on two different servers. When we merged them there was a conflict so we
|
||||
did this to avoid moving the endpoint docs to another URL.
|
||||
|
||||
Never ever do this again.
|
||||
*/
|
||||
if (match[0] === '/endpoint' && Object.keys(queryParams).length === 0) {
|
||||
ask.res.statusCode = 301
|
||||
ask.res.setHeader('Location', '/endpoint/')
|
||||
ask.res.end()
|
||||
return
|
||||
}
|
||||
|
||||
// `defaultCacheLengthSeconds` can be overridden by
|
||||
// `serviceDefaultCacheLengthSeconds` (either by category or on a badge-
|
||||
// by-badge basis). Then in turn that can be overridden by
|
||||
// `serviceOverrideCacheLengthSeconds` (which we expect to be used only in
|
||||
// the dynamic badge) but only if `serviceOverrideCacheLengthSeconds` is
|
||||
// longer than `serviceDefaultCacheLengthSeconds` and then the `cacheSeconds`
|
||||
// query param can also override both of those but again only if `cacheSeconds`
|
||||
// is longer.
|
||||
//
|
||||
// When the legacy services have been rewritten, all the code in here
|
||||
// will go away, which should achieve this goal in a simpler way.
|
||||
//
|
||||
// Ref: https://github.com/badges/shields/pull/2755
|
||||
function setCacheHeadersOnResponse(res, serviceOverrideCacheLengthSeconds) {
|
||||
setCacheHeaders({
|
||||
cacheHeaderConfig,
|
||||
serviceDefaultCacheLengthSeconds,
|
||||
serviceOverrideCacheLengthSeconds,
|
||||
queryParams,
|
||||
res,
|
||||
})
|
||||
}
|
||||
|
||||
const filteredQueryParams = {}
|
||||
allowedKeys.forEach(key => {
|
||||
filteredQueryParams[key] = queryParams[key]
|
||||
})
|
||||
|
||||
// In case our vendor servers are unresponsive.
|
||||
let serverUnresponsive = false
|
||||
const serverResponsive = setTimeout(() => {
|
||||
serverUnresponsive = true
|
||||
ask.res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||
const badgeData = coalesceBadge(
|
||||
filteredQueryParams,
|
||||
{ label: 'vendor', message: 'unresponsive' },
|
||||
{}
|
||||
)
|
||||
const svg = makeBadge(badgeData)
|
||||
const extension = (match.slice(-1)[0] || '.svg').replace(/^\./, '')
|
||||
setCacheHeadersOnResponse(ask.res)
|
||||
makeSend(extension, ask.res, end)(svg)
|
||||
}, 25000)
|
||||
|
||||
const result = handlerOptions.handler(
|
||||
filteredQueryParams,
|
||||
match,
|
||||
// eslint-disable-next-line mocha/prefer-arrow-callback
|
||||
function sendBadge(format, badgeData) {
|
||||
if (serverUnresponsive) {
|
||||
return
|
||||
}
|
||||
clearTimeout(serverResponsive)
|
||||
// Add format to badge data.
|
||||
badgeData.format = format
|
||||
const svg = makeBadge(badgeData)
|
||||
setCacheHeadersOnResponse(ask.res, badgeData.cacheLengthSeconds)
|
||||
makeSend(format, ask.res, end)(svg)
|
||||
}
|
||||
)
|
||||
// eslint-disable-next-line promise/prefer-await-to-then
|
||||
if (result && result.catch) {
|
||||
// eslint-disable-next-line promise/prefer-await-to-then
|
||||
result.catch(err => {
|
||||
throw err
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { handleRequest }
|
||||
251
core/base-service/legacy-request-handler.spec.js
Normal file
251
core/base-service/legacy-request-handler.spec.js
Normal file
@@ -0,0 +1,251 @@
|
||||
import { expect } from 'chai'
|
||||
import portfinder from 'portfinder'
|
||||
import Camp from '@shields_io/camp'
|
||||
import got from '../got-test-client.js'
|
||||
import coalesceBadge from './coalesce-badge.js'
|
||||
import { handleRequest } from './legacy-request-handler.js'
|
||||
|
||||
async function performTwoRequests(baseUrl, first, second) {
|
||||
expect((await got(`${baseUrl}${first}`)).statusCode).to.equal(200)
|
||||
expect((await got(`${baseUrl}${second}`)).statusCode).to.equal(200)
|
||||
}
|
||||
|
||||
function fakeHandler(queryParams, match, sendBadge, request) {
|
||||
const [, someValue, format] = match
|
||||
const badgeData = coalesceBadge(
|
||||
queryParams,
|
||||
{
|
||||
label: 'testing',
|
||||
message: someValue,
|
||||
},
|
||||
{}
|
||||
)
|
||||
sendBadge(format, badgeData)
|
||||
}
|
||||
|
||||
function createFakeHandlerWithCacheLength(cacheLengthSeconds) {
|
||||
return function fakeHandler(queryParams, match, sendBadge, request) {
|
||||
const [, someValue, format] = match
|
||||
const badgeData = coalesceBadge(
|
||||
queryParams,
|
||||
{
|
||||
label: 'testing',
|
||||
message: someValue,
|
||||
},
|
||||
{},
|
||||
{
|
||||
_cacheLength: cacheLengthSeconds,
|
||||
}
|
||||
)
|
||||
sendBadge(format, badgeData)
|
||||
}
|
||||
}
|
||||
|
||||
describe('The request handler', function () {
|
||||
let port, baseUrl
|
||||
beforeEach(async function () {
|
||||
port = await portfinder.getPortPromise()
|
||||
baseUrl = `http://127.0.0.1:${port}`
|
||||
})
|
||||
|
||||
let camp
|
||||
beforeEach(function (done) {
|
||||
camp = Camp.start({ port, hostname: '::' })
|
||||
camp.on('listening', () => done())
|
||||
})
|
||||
afterEach(function (done) {
|
||||
if (camp) {
|
||||
camp.close(() => done())
|
||||
camp = null
|
||||
}
|
||||
})
|
||||
|
||||
const standardCacheHeaders = { defaultCacheLengthSeconds: 120 }
|
||||
|
||||
describe('the options object calling style', function () {
|
||||
beforeEach(function () {
|
||||
camp.route(
|
||||
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
|
||||
handleRequest(standardCacheHeaders, { handler: fakeHandler })
|
||||
)
|
||||
})
|
||||
|
||||
it('should return the expected response', async function () {
|
||||
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: [],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('the function shorthand calling style', function () {
|
||||
beforeEach(function () {
|
||||
camp.route(
|
||||
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
|
||||
handleRequest(standardCacheHeaders, fakeHandler)
|
||||
)
|
||||
})
|
||||
|
||||
it('should return the expected response', async function () {
|
||||
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: [],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('caching', function () {
|
||||
describe('standard query parameters', function () {
|
||||
function register({ cacheHeaderConfig }) {
|
||||
camp.route(
|
||||
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
|
||||
handleRequest(
|
||||
cacheHeaderConfig,
|
||||
(queryParams, match, sendBadge, request) => {
|
||||
fakeHandler(queryParams, match, sendBadge, request)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
it('should set the expires header to current time + defaultCacheLengthSeconds', async function () {
|
||||
register({ cacheHeaderConfig: { defaultCacheLengthSeconds: 900 } })
|
||||
const { headers } = await got(`${baseUrl}/testing/123.json`)
|
||||
const expectedExpiry = new Date(
|
||||
+new Date(headers.date) + 900000
|
||||
).toGMTString()
|
||||
expect(headers.expires).to.equal(expectedExpiry)
|
||||
expect(headers['cache-control']).to.equal('max-age=900, s-maxage=900')
|
||||
})
|
||||
|
||||
it('should set the expected cache headers on cached responses', async function () {
|
||||
register({ cacheHeaderConfig: { defaultCacheLengthSeconds: 900 } })
|
||||
|
||||
// Make first request.
|
||||
await got(`${baseUrl}/testing/123.json`)
|
||||
|
||||
const { headers } = await got(`${baseUrl}/testing/123.json`)
|
||||
const expectedExpiry = new Date(
|
||||
+new Date(headers.date) + 900000
|
||||
).toGMTString()
|
||||
expect(headers.expires).to.equal(expectedExpiry)
|
||||
expect(headers['cache-control']).to.equal('max-age=900, s-maxage=900')
|
||||
})
|
||||
|
||||
it('should let live service data override the default cache headers with longer value', async function () {
|
||||
camp.route(
|
||||
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
|
||||
handleRequest(
|
||||
{ defaultCacheLengthSeconds: 300 },
|
||||
(queryParams, match, sendBadge, request) => {
|
||||
createFakeHandlerWithCacheLength(400)(
|
||||
queryParams,
|
||||
match,
|
||||
sendBadge,
|
||||
request
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
const { headers } = await got(`${baseUrl}/testing/123.json`)
|
||||
expect(headers['cache-control']).to.equal('max-age=400, s-maxage=400')
|
||||
})
|
||||
|
||||
it('should not let live service data override the default cache headers with shorter value', async function () {
|
||||
camp.route(
|
||||
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
|
||||
handleRequest(
|
||||
{ defaultCacheLengthSeconds: 300 },
|
||||
(queryParams, match, sendBadge, request) => {
|
||||
createFakeHandlerWithCacheLength(200)(
|
||||
queryParams,
|
||||
match,
|
||||
sendBadge,
|
||||
request
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
const { headers } = await got(`${baseUrl}/testing/123.json`)
|
||||
expect(headers['cache-control']).to.equal('max-age=300, s-maxage=300')
|
||||
})
|
||||
|
||||
it('should set the expires header to current time + cacheSeconds', async function () {
|
||||
register({ cacheHeaderConfig: { defaultCacheLengthSeconds: 0 } })
|
||||
const { headers } = await got(
|
||||
`${baseUrl}/testing/123.json?cacheSeconds=3600`
|
||||
)
|
||||
const expectedExpiry = new Date(
|
||||
+new Date(headers.date) + 3600000
|
||||
).toGMTString()
|
||||
expect(headers.expires).to.equal(expectedExpiry)
|
||||
expect(headers['cache-control']).to.equal('max-age=3600, s-maxage=3600')
|
||||
})
|
||||
|
||||
it('should ignore cacheSeconds when shorter than defaultCacheLengthSeconds', async function () {
|
||||
register({ cacheHeaderConfig: { defaultCacheLengthSeconds: 600 } })
|
||||
const { headers } = await got(
|
||||
`${baseUrl}/testing/123.json?cacheSeconds=300`
|
||||
)
|
||||
const expectedExpiry = new Date(
|
||||
+new Date(headers.date) + 600000
|
||||
).toGMTString()
|
||||
expect(headers.expires).to.equal(expectedExpiry)
|
||||
expect(headers['cache-control']).to.equal('max-age=600, s-maxage=600')
|
||||
})
|
||||
|
||||
it('should set Cache-Control: no-cache, no-store, must-revalidate if cache seconds is 0', async function () {
|
||||
register({ cacheHeaderConfig: { defaultCacheLengthSeconds: 0 } })
|
||||
const { headers } = await got(`${baseUrl}/testing/123.json`)
|
||||
expect(headers.expires).to.equal(headers.date)
|
||||
expect(headers['cache-control']).to.equal(
|
||||
'no-cache, no-store, must-revalidate'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('custom query parameters', function () {
|
||||
let handlerCallCount
|
||||
beforeEach(function () {
|
||||
handlerCallCount = 0
|
||||
camp.route(
|
||||
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
|
||||
handleRequest(standardCacheHeaders, {
|
||||
queryParams: ['foo'],
|
||||
handler: (queryParams, match, sendBadge, request) => {
|
||||
++handlerCallCount
|
||||
fakeHandler(queryParams, match, sendBadge, request)
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should differentiate them', async function () {
|
||||
await performTwoRequests(
|
||||
baseUrl,
|
||||
'/testing/123.svg?foo=1',
|
||||
'/testing/123.svg?foo=2'
|
||||
)
|
||||
expect(handlerCallCount).to.equal(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
35
core/base-service/legacy-result-sender.js
Normal file
35
core/base-service/legacy-result-sender.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import stream from 'stream'
|
||||
|
||||
function streamFromString(str) {
|
||||
const newStream = new stream.Readable()
|
||||
newStream._read = () => {
|
||||
newStream.push(str)
|
||||
newStream.push(null)
|
||||
}
|
||||
return newStream
|
||||
}
|
||||
|
||||
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) })
|
||||
}
|
||||
|
||||
function makeSend(format, askres, end) {
|
||||
if (format === 'svg') {
|
||||
return res => sendSVG(res, askres, end)
|
||||
} else if (format === 'json') {
|
||||
return res => sendJSON(res, askres, end)
|
||||
} else {
|
||||
throw Error(`Unrecognized format: ${format}`)
|
||||
}
|
||||
}
|
||||
|
||||
export { makeSend }
|
||||
@@ -1,16 +0,0 @@
|
||||
import { normalizeColor } from 'badge-maker/lib/color.js'
|
||||
|
||||
export function makeJsonBadge(badgeData) {
|
||||
const { label, message, logoWidth, color, labelColor, links } = badgeData
|
||||
|
||||
return {
|
||||
label,
|
||||
message,
|
||||
logoWidth,
|
||||
color: normalizeColor(color),
|
||||
labelColor: normalizeColor(labelColor),
|
||||
link: links,
|
||||
name: label,
|
||||
value: message,
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { expect } from 'chai'
|
||||
import { makeJsonBadge } from './make-json-badge.js'
|
||||
|
||||
describe('makeJsonBadge()', function () {
|
||||
it('should produce the expected JSON', function () {
|
||||
expect(
|
||||
makeJsonBadge({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
links: ['https://example.com/', 'https://other.example.com/'],
|
||||
})
|
||||
).to.deep.equal({
|
||||
name: 'cactus',
|
||||
label: 'cactus',
|
||||
value: 'grown',
|
||||
message: 'grown',
|
||||
link: ['https://example.com/', 'https://other.example.com/'],
|
||||
color: undefined,
|
||||
labelColor: undefined,
|
||||
logoWidth: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,3 @@
|
||||
import url from 'url'
|
||||
import camelcase from 'camelcase'
|
||||
import emojic from 'emojic'
|
||||
import Joi from 'joi'
|
||||
@@ -10,7 +9,7 @@ import {
|
||||
} from './cache-headers.js'
|
||||
import { isValidCategory } from './categories.js'
|
||||
import { MetricHelper } from './metric-helper.js'
|
||||
import { isValidRoute, prepareRoute, paramsForReq } from './route.js'
|
||||
import { isValidRoute, prepareRoute, namedParamsForMatch } from './route.js'
|
||||
import trace from './trace.js'
|
||||
|
||||
const attrSchema = Joi.object({
|
||||
@@ -55,7 +54,7 @@ export default function redirector(attrs) {
|
||||
static route = route
|
||||
static examples = examples
|
||||
|
||||
static register({ app, metricInstance }, { rasterUrl }) {
|
||||
static register({ camp, metricInstance }, { rasterUrl }) {
|
||||
const { regex, captureNames } = prepareRoute({
|
||||
...this.route,
|
||||
withPng: Boolean(rasterUrl),
|
||||
@@ -66,17 +65,17 @@ export default function redirector(attrs) {
|
||||
ServiceClass: this,
|
||||
})
|
||||
|
||||
app.get(regex, async (req, res) => {
|
||||
if (serverHasBeenUpSinceResourceCached(req)) {
|
||||
camp.route(regex, async (queryParams, match, end, ask) => {
|
||||
if (serverHasBeenUpSinceResourceCached(ask.req)) {
|
||||
// Send Not Modified.
|
||||
res.status(304)
|
||||
res.end()
|
||||
ask.res.statusCode = 304
|
||||
ask.res.end()
|
||||
return
|
||||
}
|
||||
|
||||
const metricHandle = metricHelper.startRequest()
|
||||
|
||||
const { namedParams, format } = paramsForReq(captureNames, req, this)
|
||||
const namedParams = namedParamsForMatch(captureNames, match, this)
|
||||
trace.logTrace(
|
||||
'inbound',
|
||||
emojic.arrowHeadingUp,
|
||||
@@ -84,12 +83,12 @@ export default function redirector(attrs) {
|
||||
route.base
|
||||
)
|
||||
trace.logTrace('inbound', emojic.ticket, 'Named params', namedParams)
|
||||
trace.logTrace('inbound', emojic.crayon, 'Query params', req.query)
|
||||
trace.logTrace('inbound', emojic.crayon, 'Query params', queryParams)
|
||||
|
||||
const targetPath = encodeURI(transformPath(namedParams))
|
||||
trace.logTrace('validate', emojic.dart, 'Target', targetPath)
|
||||
|
||||
let urlSuffix = url.parse(req.url).search ?? '' // eslint-disable-line node/no-deprecated-api
|
||||
let urlSuffix = ask.uri.search || ''
|
||||
|
||||
if (transformQueryParams) {
|
||||
const specifiedParams = queryString.parse(urlSuffix)
|
||||
@@ -101,18 +100,21 @@ export default function redirector(attrs) {
|
||||
urlSuffix = `?${outQueryString}`
|
||||
}
|
||||
|
||||
const baseUrl = format === 'png' ? rasterUrl : ''
|
||||
const redirectUrl = `${baseUrl}${targetPath}.${format}${urlSuffix}`
|
||||
// The final capture group is the extension.
|
||||
const format = (match.slice(-1)[0] || '.svg').replace(/^\./, '')
|
||||
const redirectUrl = `${
|
||||
format === 'png' ? rasterUrl : ''
|
||||
}${targetPath}.${format}${urlSuffix}`
|
||||
trace.logTrace('outbound', emojic.shield, 'Redirect URL', redirectUrl)
|
||||
|
||||
res.status(301)
|
||||
res.setHeader('Location', redirectUrl)
|
||||
ask.res.statusCode = 301
|
||||
ask.res.setHeader('Location', redirectUrl)
|
||||
|
||||
// To avoid caching mistakes for a long time, and to make this simpler
|
||||
// to reason about, use the same cache semantics as the static badge.
|
||||
setCacheHeadersForStaticResource(res)
|
||||
setCacheHeadersForStaticResource(ask.res)
|
||||
|
||||
res.end()
|
||||
ask.res.end()
|
||||
|
||||
metricHandle.noteResponseSent()
|
||||
})
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import Camp from '@shields_io/camp'
|
||||
import portfinder from 'portfinder'
|
||||
import { expect } from 'chai'
|
||||
import { ExpressTestHarness } from '../express-test-harness.js'
|
||||
import got from '../got-test-client.js'
|
||||
import redirector from './redirector.js'
|
||||
|
||||
describe('Redirector', function () {
|
||||
@@ -61,12 +63,28 @@ describe('Redirector', function () {
|
||||
expect(redirector({ ...attrs, examples }).examples).to.equal(examples)
|
||||
})
|
||||
|
||||
describe('Express integration', function () {
|
||||
describe('ScoutCamp integration', function () {
|
||||
let port, baseUrl
|
||||
beforeEach(async function () {
|
||||
port = await portfinder.getPortPromise()
|
||||
baseUrl = `http://127.0.0.1:${port}`
|
||||
})
|
||||
|
||||
let camp
|
||||
beforeEach(async function () {
|
||||
camp = Camp.start({ port, hostname: '::' })
|
||||
await new Promise(resolve => camp.on('listening', () => resolve()))
|
||||
})
|
||||
afterEach(async function () {
|
||||
if (camp) {
|
||||
await new Promise(resolve => camp.close(resolve))
|
||||
camp = undefined
|
||||
}
|
||||
})
|
||||
|
||||
const transformPath = ({ namedParamA }) => `/new/service/${namedParamA}`
|
||||
|
||||
let harness
|
||||
beforeEach(async function () {
|
||||
harness = new ExpressTestHarness()
|
||||
beforeEach(function () {
|
||||
const ServiceClass = redirector({
|
||||
category,
|
||||
route,
|
||||
@@ -74,20 +92,17 @@ describe('Redirector', function () {
|
||||
dateAdded,
|
||||
})
|
||||
ServiceClass.register(
|
||||
{ app: harness.app },
|
||||
{ camp },
|
||||
{ rasterUrl: 'http://raster.example.test' }
|
||||
)
|
||||
await harness.start()
|
||||
})
|
||||
|
||||
afterEach(async function () {
|
||||
await harness.stop()
|
||||
})
|
||||
|
||||
it('should redirect as configured', async function () {
|
||||
const { statusCode, headers } = await harness.get(
|
||||
'/very/old/service/hello-world.svg',
|
||||
{ followRedirect: false }
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}/very/old/service/hello-world.svg`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
)
|
||||
|
||||
expect(statusCode).to.equal(301)
|
||||
@@ -95,9 +110,11 @@ describe('Redirector', function () {
|
||||
})
|
||||
|
||||
it('should redirect raster extensions to the canonical path as configured', async function () {
|
||||
const { statusCode, headers } = await harness.get(
|
||||
'/very/old/service/hello-world.png',
|
||||
{ followRedirect: false }
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}/very/old/service/hello-world.png`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
)
|
||||
|
||||
expect(statusCode).to.equal(301)
|
||||
@@ -107,9 +124,11 @@ describe('Redirector', function () {
|
||||
})
|
||||
|
||||
it('should forward the query params', async function () {
|
||||
const { statusCode, headers } = await harness.get(
|
||||
'/very/old/service/hello-world.svg?color=123&style=flat-square',
|
||||
{ followRedirect: false }
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}/very/old/service/hello-world.svg?color=123&style=flat-square`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
)
|
||||
|
||||
expect(statusCode).to.equal(301)
|
||||
@@ -119,9 +138,11 @@ describe('Redirector', function () {
|
||||
})
|
||||
|
||||
it('should correctly encode the redirect URL', async function () {
|
||||
const { statusCode, headers } = await harness.get(
|
||||
'/very/old/service/hello%0Dworld.svg?foobar=a%0Db',
|
||||
{ followRedirect: false }
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}/very/old/service/hello%0Dworld.svg?foobar=a%0Db`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
)
|
||||
|
||||
expect(statusCode).to.equal(301)
|
||||
@@ -145,13 +166,15 @@ describe('Redirector', function () {
|
||||
transformQueryParams,
|
||||
dateAdded,
|
||||
})
|
||||
ServiceClass.register({ app: harness.app }, {})
|
||||
ServiceClass.register({ camp }, {})
|
||||
})
|
||||
|
||||
it('should forward the transformed query params', async function () {
|
||||
const { statusCode, headers } = await harness.get(
|
||||
'/another/old/service/token/abc123/hello-world.svg',
|
||||
{ followRedirect: false }
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}/another/old/service/token/abc123/hello-world.svg`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
)
|
||||
|
||||
expect(statusCode).to.equal(301)
|
||||
@@ -161,9 +184,11 @@ describe('Redirector', function () {
|
||||
})
|
||||
|
||||
it('should forward the specified and transformed query params', async function () {
|
||||
const { statusCode, headers } = await harness.get(
|
||||
'/another/old/service/token/abc123/hello-world.svg?color=123&style=flat-square',
|
||||
{ followRedirect: false }
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}/another/old/service/token/abc123/hello-world.svg?color=123&style=flat-square`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
)
|
||||
|
||||
expect(statusCode).to.equal(301)
|
||||
@@ -173,9 +198,11 @@ describe('Redirector', function () {
|
||||
})
|
||||
|
||||
it('should use transformed query params on param conflicts by default', async function () {
|
||||
const { statusCode, headers } = await harness.get(
|
||||
'/another/old/service/token/abc123/hello-world.svg?color=123&style=flat-square&token=def456',
|
||||
{ followRedirect: false }
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}/another/old/service/token/abc123/hello-world.svg?color=123&style=flat-square&token=def456`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
)
|
||||
|
||||
expect(statusCode).to.equal(301)
|
||||
@@ -197,10 +224,12 @@ describe('Redirector', function () {
|
||||
overrideTransformedQueryParams: true,
|
||||
dateAdded,
|
||||
})
|
||||
ServiceClass.register({ app: harness.app }, {})
|
||||
const { statusCode, headers } = await harness.get(
|
||||
'/override/service/token/abc123/hello-world.svg?style=flat-square&token=def456',
|
||||
{ followRedirect: false }
|
||||
ServiceClass.register({ camp }, {})
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}/override/service/token/abc123/hello-world.svg?style=flat-square&token=def456`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
)
|
||||
|
||||
expect(statusCode).to.equal(301)
|
||||
|
||||
@@ -44,29 +44,23 @@ function prepareRoute({ base, pattern, format, capture, withPng }) {
|
||||
return { regex, captureNames }
|
||||
}
|
||||
|
||||
function paramsForReq(captureNames = [], req, ServiceClass) {
|
||||
// In addition to the parameters declared by the service, we have one match
|
||||
// for the format.
|
||||
const expectedNamedParamCount = Object.keys(req.params).length - 1
|
||||
if (captureNames.length !== expectedNamedParamCount) {
|
||||
function namedParamsForMatch(captureNames = [], match, ServiceClass) {
|
||||
// Assume the last match is the format, and drop match[0], which is the
|
||||
// entire match.
|
||||
const captures = match.slice(1, -1)
|
||||
|
||||
if (captureNames.length !== captures.length) {
|
||||
throw new Error(
|
||||
`Service ${ServiceClass.name} declares incorrect number of named params ` +
|
||||
`(expected ${expectedNamedParamCount}, got ${captureNames.length})`
|
||||
`(expected ${captures.length}, got ${captureNames.length})`
|
||||
)
|
||||
}
|
||||
|
||||
const namedParams = {}
|
||||
const result = {}
|
||||
captureNames.forEach((name, index) => {
|
||||
namedParams[name] = req.params[index]
|
||||
result[name] = captures[index]
|
||||
})
|
||||
|
||||
// The final capture group is the extension.
|
||||
const format = (req.params[expectedNamedParamCount] || '.svg').replace(
|
||||
/^\./,
|
||||
''
|
||||
)
|
||||
|
||||
return { namedParams, format }
|
||||
return result
|
||||
}
|
||||
|
||||
function getQueryParamNames({ queryParamSchema }) {
|
||||
@@ -83,6 +77,6 @@ export {
|
||||
isValidRoute,
|
||||
assertValidRoute,
|
||||
prepareRoute,
|
||||
paramsForReq,
|
||||
namedParamsForMatch,
|
||||
getQueryParamNames,
|
||||
}
|
||||
|
||||
@@ -1,25 +1,13 @@
|
||||
import { expect } from 'chai'
|
||||
import Joi from 'joi'
|
||||
import { test, given } from 'sazerac'
|
||||
import { prepareRoute, paramsForReq, getQueryParamNames } from './route.js'
|
||||
|
||||
function paramsForPath({ regex, captureNames, ServiceClass }, path) {
|
||||
// Prepare a mock express `req` object.
|
||||
const params = {}
|
||||
regex.exec(path).forEach((param, i) => {
|
||||
// regex.exec(path)[0] contains the entire path. We want [1] ... [n].
|
||||
if (i > 0) {
|
||||
params[i - 1] = param
|
||||
}
|
||||
})
|
||||
const req = { params }
|
||||
|
||||
return paramsForReq(captureNames, req, ServiceClass)
|
||||
}
|
||||
import { test, given, forCases } from 'sazerac'
|
||||
import {
|
||||
prepareRoute,
|
||||
namedParamsForMatch,
|
||||
getQueryParamNames,
|
||||
} from './route.js'
|
||||
|
||||
describe('Route helpers', function () {
|
||||
const ServiceClass = { name: 'MyService' }
|
||||
|
||||
context('A `pattern` with a named param is declared', function () {
|
||||
const { regex, captureNames } = prepareRoute({
|
||||
base: 'foo',
|
||||
@@ -27,31 +15,22 @@ describe('Route helpers', function () {
|
||||
queryParamSchema: Joi.object({ queryParamA: Joi.string() }).required(),
|
||||
})
|
||||
|
||||
const regexExec = path => regex.exec(path)
|
||||
const regexExec = str => regex.exec(str)
|
||||
test(regexExec, () => {
|
||||
given('/foo/bar/bar.svg').expect(null)
|
||||
})
|
||||
|
||||
const params = path =>
|
||||
paramsForPath({ regex, captureNames, ServiceClass }, path)
|
||||
test(params, () => {
|
||||
given('/foo/bar.bar.bar.svg').expect({
|
||||
namedParams: { namedParamA: 'bar.bar.bar' },
|
||||
format: 'svg',
|
||||
})
|
||||
given('/foo/bar.bar.bar.json').expect({
|
||||
namedParams: { namedParamA: 'bar.bar.bar' },
|
||||
format: 'json',
|
||||
})
|
||||
const namedParams = str =>
|
||||
namedParamsForMatch(captureNames, regex.exec(str))
|
||||
test(namedParams, () => {
|
||||
forCases([
|
||||
given('/foo/bar.bar.bar.svg'),
|
||||
given('/foo/bar.bar.bar.json'),
|
||||
]).expect({ namedParamA: 'bar.bar.bar' })
|
||||
|
||||
// This pattern catches bugs related to escaping the extension separator.
|
||||
given('/foo/bar.bar.bar_svg').expect({
|
||||
namedParams: { namedParamA: 'bar.bar.bar_svg' },
|
||||
format: 'svg',
|
||||
})
|
||||
given('/foo/bar.bar.bar.zip').expect({
|
||||
namedParams: { namedParamA: 'bar.bar.bar.zip' },
|
||||
format: 'svg',
|
||||
})
|
||||
given('/foo/bar.bar.bar_svg').expect({ namedParamA: 'bar.bar.bar_svg' })
|
||||
given('/foo/bar.bar.bar.zip').expect({ namedParamA: 'bar.bar.bar.zip' })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -67,41 +46,33 @@ describe('Route helpers', function () {
|
||||
given('/foo/bar/bar.svg').expect(null)
|
||||
})
|
||||
|
||||
const params = path =>
|
||||
paramsForPath({ regex, captureNames, ServiceClass }, path)
|
||||
test(params, () => {
|
||||
given('/foo/bar.bar.bar.svg').expect({
|
||||
namedParams: { namedParamA: 'bar.bar.bar' },
|
||||
format: 'svg',
|
||||
})
|
||||
given('/foo/bar.bar.bar.json').expect({
|
||||
namedParams: { namedParamA: 'bar.bar.bar' },
|
||||
format: 'json',
|
||||
})
|
||||
const namedParams = str =>
|
||||
namedParamsForMatch(captureNames, regex.exec(str))
|
||||
test(namedParams, () => {
|
||||
forCases([
|
||||
given('/foo/bar.bar.bar.svg'),
|
||||
given('/foo/bar.bar.bar.json'),
|
||||
]).expect({ namedParamA: 'bar.bar.bar' })
|
||||
|
||||
// This pattern catches bugs related to escaping the extension separator.
|
||||
given('/foo/bar.bar.bar_svg').expect({
|
||||
namedParams: { namedParamA: 'bar.bar.bar_svg' },
|
||||
format: 'svg',
|
||||
})
|
||||
given('/foo/bar.bar.bar.zip').expect({
|
||||
namedParams: { namedParamA: 'bar.bar.bar.zip' },
|
||||
format: 'svg',
|
||||
})
|
||||
given('/foo/bar.bar.bar_svg').expect({ namedParamA: 'bar.bar.bar_svg' })
|
||||
given('/foo/bar.bar.bar.zip').expect({ namedParamA: 'bar.bar.bar.zip' })
|
||||
})
|
||||
})
|
||||
|
||||
context('No named params are declared', function () {
|
||||
const { regex, captureNames } = prepareRoute({
|
||||
base: 'foo',
|
||||
format: '(?:[^/]+?)',
|
||||
format: '(?:[^/]+)',
|
||||
})
|
||||
|
||||
const params = path =>
|
||||
paramsForPath({ regex, captureNames, ServiceClass }, path)
|
||||
test(params, () => {
|
||||
given('/foo/bar.bar.bar.svg').expect({ namedParams: {}, format: 'svg' })
|
||||
given('/foo/bar.bar.bar.json').expect({ namedParams: {}, format: 'json' })
|
||||
const namedParams = str =>
|
||||
namedParamsForMatch(captureNames, regex.exec(str))
|
||||
test(namedParams, () => {
|
||||
forCases([
|
||||
given('/foo/bar.bar.bar.svg'),
|
||||
given('/foo/bar.bar.bar.json'),
|
||||
]).expect({})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -112,13 +83,13 @@ describe('Route helpers', function () {
|
||||
capture: ['namedParamA'],
|
||||
})
|
||||
|
||||
it('Throws the expected error', function () {
|
||||
expect(() =>
|
||||
paramsForPath({ regex, captureNames, ServiceClass }, '/foo/bar/baz.svg')
|
||||
).to.throw(
|
||||
'Service MyService declares incorrect number of named params (expected 2, got 1)'
|
||||
)
|
||||
})
|
||||
expect(() =>
|
||||
namedParamsForMatch(captureNames, regex.exec('/foo/bar/baz.svg'), {
|
||||
name: 'MyService',
|
||||
})
|
||||
).to.throw(
|
||||
'Service MyService declares incorrect number of named params (expected 2, got 1)'
|
||||
)
|
||||
})
|
||||
|
||||
it('getQueryParamNames', function () {
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import express from 'express'
|
||||
import portfinder from 'portfinder'
|
||||
import got from './got-test-client.js'
|
||||
|
||||
export class ExpressTestHarness {
|
||||
constructor() {
|
||||
this.app = express()
|
||||
}
|
||||
|
||||
async start() {
|
||||
const port = (this.port = await portfinder.getPortPromise())
|
||||
this.baseUrl = `http://127.0.0.1:${port}`
|
||||
await new Promise(resolve => {
|
||||
this.server = this.app.listen({ host: '::', port }, () => resolve())
|
||||
})
|
||||
}
|
||||
|
||||
async stop() {
|
||||
await new Promise(resolve => this.server.close(resolve))
|
||||
}
|
||||
|
||||
ensureStarted() {
|
||||
if (!this.server) {
|
||||
throw Error('Server has not been started')
|
||||
}
|
||||
}
|
||||
|
||||
async get(url, options) {
|
||||
this.ensureStarted()
|
||||
return got.get(`${this.baseUrl}${url}`, options)
|
||||
}
|
||||
|
||||
async post(url, options) {
|
||||
this.ensureStarted()
|
||||
return got.post(`${this.baseUrl}${url}`, options)
|
||||
}
|
||||
}
|
||||
@@ -37,13 +37,12 @@ export default class PrometheusMetrics {
|
||||
})
|
||||
}
|
||||
|
||||
async registerMetricsEndpoint(app) {
|
||||
async registerMetricsEndpoint(server) {
|
||||
const { register } = this
|
||||
|
||||
app.get('/metrics', async (req, res) => {
|
||||
res.setHeader('Content-Type', register.contentType)
|
||||
res.send(await register.metrics())
|
||||
res.end()
|
||||
server.route(/^\/metrics$/, async (data, match, end, ask) => {
|
||||
ask.res.setHeader('Content-Type', register.contentType)
|
||||
ask.res.end(await register.metrics())
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,32 @@
|
||||
import { expect } from 'chai'
|
||||
import { ExpressTestHarness } from '../express-test-harness.js'
|
||||
import Camp from '@shields_io/camp'
|
||||
import portfinder from 'portfinder'
|
||||
import got from '../got-test-client.js'
|
||||
import Metrics from './prometheus-metrics.js'
|
||||
|
||||
describe('Prometheus metrics route', function () {
|
||||
let harness, metrics
|
||||
let port, baseUrl, camp, metrics
|
||||
beforeEach(async function () {
|
||||
harness = new ExpressTestHarness()
|
||||
|
||||
metrics = new Metrics()
|
||||
metrics.registerMetricsEndpoint(harness.app)
|
||||
|
||||
await harness.start()
|
||||
port = await portfinder.getPortPromise()
|
||||
baseUrl = `http://127.0.0.1:${port}`
|
||||
camp = Camp.start({ port, hostname: '::' })
|
||||
await new Promise(resolve => camp.on('listening', () => resolve()))
|
||||
})
|
||||
|
||||
afterEach(async function () {
|
||||
await harness.stop()
|
||||
if (metrics) {
|
||||
metrics.stop()
|
||||
}
|
||||
if (camp) {
|
||||
await new Promise(resolve => camp.close(resolve))
|
||||
camp = undefined
|
||||
}
|
||||
})
|
||||
|
||||
it('returns default metrics', async function () {
|
||||
const { statusCode, body } = await harness.get('/metrics')
|
||||
metrics = new Metrics()
|
||||
metrics.registerMetricsEndpoint(camp)
|
||||
|
||||
const { statusCode, body } = await got(`${baseUrl}/metrics`)
|
||||
|
||||
expect(statusCode).to.be.equal(200)
|
||||
expect(body).to.contain('nodejs_version_info')
|
||||
|
||||
66
core/server/public/monitor.html
Normal file
66
core/server/public/monitor.html
Normal 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>
|
||||
@@ -2,20 +2,19 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import http from 'http'
|
||||
import https from 'https'
|
||||
import path from 'path'
|
||||
import url, { fileURLToPath } from 'url'
|
||||
import express from 'express'
|
||||
import { bootstrap } from 'global-agent'
|
||||
import cloudflareMiddleware from 'cloudflare-middleware'
|
||||
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 as setSuggestRoutes } from '../../services/suggest.js'
|
||||
import { setRoutes } from '../../services/suggest.js'
|
||||
import { loadServiceClasses } from '../base-service/loader.js'
|
||||
import { makeJsonBadge } from '../base-service/make-json-badge.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 { rasterRedirectUrl } from '../badge-urls/make-badge-url.js'
|
||||
import { fileSize, nonNegativeInteger } from '../../services/validators.js'
|
||||
@@ -141,9 +140,7 @@ const publicConfigSchema = Joi.object({
|
||||
weblate: defaultService,
|
||||
trace: Joi.boolean().required(),
|
||||
}).required(),
|
||||
cacheHeaders: Joi.object({
|
||||
defaultCacheLengthSeconds: nonNegativeInteger,
|
||||
}).required(),
|
||||
cacheHeaders: { defaultCacheLengthSeconds: nonNegativeInteger },
|
||||
handleInternalErrors: Joi.boolean().required(),
|
||||
fetchLimit: fileSize,
|
||||
userAgentBase: Joi.string().required(),
|
||||
@@ -200,11 +197,15 @@ const privateMetricsInfluxConfigSchema = privateConfigSchema.append({
|
||||
influx_password: Joi.string().required(),
|
||||
})
|
||||
|
||||
function addHandlerAtIndex(camp, index, handlerFn) {
|
||||
camp.stack.splice(index, 0, handlerFn)
|
||||
}
|
||||
|
||||
/**
|
||||
* The Server is based on Express. It creates an http server and sets up helpers
|
||||
* for token persistence and monitoring. Then it loads all the services,
|
||||
* injecting dependencies, as it asks each one to register its route with
|
||||
* Express.
|
||||
* The Server is based on the web framework Scoutcamp. It creates
|
||||
* an http server, sets up helpers for token persistence and monitoring.
|
||||
* Then it loads all the services, injecting dependencies as it
|
||||
* asks each one to register its route with Scoutcamp.
|
||||
*/
|
||||
class Server {
|
||||
/**
|
||||
@@ -297,25 +298,37 @@ class Server {
|
||||
|
||||
// See https://www.viget.com/articles/heroku-cloudflare-the-right-way/
|
||||
requireCloudflare() {
|
||||
const { app } = this
|
||||
app.use(cloudflareMiddleware())
|
||||
// Set `req.ip`, which is expected by `cloudflareMiddleware()`. This is set
|
||||
// by Express but not Scoutcamp.
|
||||
addHandlerAtIndex(this.camp, 0, function (req, res, next) {
|
||||
// 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())
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up Express routes for 404/not found responses.
|
||||
* Set up Scoutcamp routes for 404/not found responses
|
||||
*/
|
||||
registerErrorHandlers() {
|
||||
const { app, config } = this
|
||||
const { camp, config } = this
|
||||
const {
|
||||
public: { rasterUrl },
|
||||
} = config
|
||||
|
||||
app.get(/\.(gif|jpg)$/, (req, res) => {
|
||||
res.status(410)
|
||||
res.setHeader('Content-Type', 'image/svg+xml;charset=utf-8')
|
||||
|
||||
const format = req.params[0]
|
||||
res.send(
|
||||
camp.route(/\.(gif|jpg)$/, (query, match, end, request) => {
|
||||
const [, format] = match
|
||||
makeSend(
|
||||
'svg',
|
||||
request.res,
|
||||
end
|
||||
)(
|
||||
makeBadge({
|
||||
label: '410',
|
||||
message: `${format} no longer available`,
|
||||
@@ -323,53 +336,41 @@ class Server {
|
||||
format: 'svg',
|
||||
})
|
||||
)
|
||||
|
||||
res.end()
|
||||
})
|
||||
|
||||
if (!rasterUrl) {
|
||||
app.get(/\.png$/, (req, res) => {
|
||||
res.status(404)
|
||||
res.setHeader('Content-Type', 'image/svg+xml;charset=utf-8')
|
||||
res.send(
|
||||
camp.route(/\.png$/, (query, match, end, request) => {
|
||||
makeSend(
|
||||
'svg',
|
||||
request.res,
|
||||
end
|
||||
)(
|
||||
makeBadge({
|
||||
label: '404',
|
||||
message: 'raster badges not available',
|
||||
color: 'lightgray',
|
||||
format: 'svg',
|
||||
})
|
||||
)
|
||||
res.end()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
registerNotFoundHandlers() {
|
||||
const { app } = this
|
||||
camp.notfound(/(\.svg|\.json|)$/, (query, match, end, request) => {
|
||||
const [, extension] = match
|
||||
const format = (extension || '.svg').replace(/^\./, '')
|
||||
|
||||
app.get(/\.json$/, (req, res) => {
|
||||
res.status(404)
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.json(
|
||||
makeJsonBadge({
|
||||
label: '404',
|
||||
message: 'badge not found',
|
||||
color: 'red',
|
||||
})
|
||||
)
|
||||
res.end()
|
||||
})
|
||||
|
||||
app.get(/(?:\.svg|)$/, (req, res) => {
|
||||
res.status(404)
|
||||
res.setHeader('Content-Type', 'image/svg+xml;charset=utf-8')
|
||||
res.send(
|
||||
makeSend(
|
||||
format,
|
||||
request.res,
|
||||
end
|
||||
)(
|
||||
makeBadge({
|
||||
label: '404',
|
||||
message: 'badge not found',
|
||||
color: 'red',
|
||||
format,
|
||||
})
|
||||
)
|
||||
res.end()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -381,62 +382,54 @@ class Server {
|
||||
* to {@link https://shields.io/} )
|
||||
*/
|
||||
registerRedirects() {
|
||||
const { config, app } = this
|
||||
const { config, camp } = this
|
||||
const {
|
||||
public: { rasterUrl, redirectUrl },
|
||||
} = config
|
||||
|
||||
if (rasterUrl) {
|
||||
// Redirect to the raster server for raster versions of modern badges.
|
||||
app.get(/\.png$/, (req, res) => {
|
||||
res.status(301)
|
||||
res.setHeader('Location', rasterRedirectUrl({ rasterUrl }, req.url))
|
||||
camp.route(/\.png$/, (queryParams, match, end, ask) => {
|
||||
ask.res.statusCode = 301
|
||||
ask.res.setHeader(
|
||||
'Location',
|
||||
rasterRedirectUrl({ rasterUrl }, ask.req.url)
|
||||
)
|
||||
|
||||
const cacheDuration = (30 * 24 * 3600) | 0 // 30 days.
|
||||
res.setHeader('Cache-Control', `max-age=${cacheDuration}`)
|
||||
ask.res.setHeader('Cache-Control', `max-age=${cacheDuration}`)
|
||||
|
||||
res.end()
|
||||
ask.res.end()
|
||||
})
|
||||
}
|
||||
|
||||
if (redirectUrl) {
|
||||
app.get('/', (req, res) => {
|
||||
res.status(302)
|
||||
res.setHeader('Location', redirectUrl)
|
||||
res.end()
|
||||
camp.route(/^\/$/, (data, match, end, ask) => {
|
||||
ask.res.statusCode = 302
|
||||
ask.res.setHeader('Location', redirectUrl)
|
||||
ask.res.end()
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
This is here for legacy reasons. The badge server and frontend used to live
|
||||
on two different servers. When we merged them there was a conflict so we did
|
||||
this to avoid moving the endpoint docs to another URL.
|
||||
|
||||
Never ever do this again.
|
||||
*/
|
||||
app.use('/endpoint', (req, res, next) => {
|
||||
if (Object.keys(req.query).length === 0) {
|
||||
res.status(301)
|
||||
res.setHeader('Location', '/endpoint/')
|
||||
res.end()
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate all the service classes defined in /services,
|
||||
* load each service and register an Express route for each service.
|
||||
* load each service and register a Scoutcamp route for each service.
|
||||
*/
|
||||
async registerServices() {
|
||||
const { app, config, metricInstance } = this
|
||||
const { config, camp, metricInstance } = this
|
||||
const { apiProvider: githubApiProvider } = this.githubConstellation
|
||||
const { apiProvider: librariesIoApiProvider } =
|
||||
this.librariesioConstellation
|
||||
;(await loadServiceClasses()).forEach(serviceClass =>
|
||||
serviceClass.register(
|
||||
{ app, githubApiProvider, librariesIoApiProvider, metricInstance },
|
||||
{
|
||||
camp,
|
||||
handleRequest,
|
||||
githubApiProvider,
|
||||
librariesIoApiProvider,
|
||||
metricInstance,
|
||||
},
|
||||
{
|
||||
handleInternalErrors: config.public.handleInternalErrors,
|
||||
cacheHeaders: config.public.cacheHeaders,
|
||||
@@ -469,14 +462,11 @@ class Server {
|
||||
|
||||
/**
|
||||
* Start the HTTP server:
|
||||
* Bootstrap Express,
|
||||
* Bootstrap Scoutcamp,
|
||||
* Register handlers,
|
||||
* Start listening for requests on this.baseUrl()
|
||||
*
|
||||
* @param {Function} registerExtras Optional function to register additional
|
||||
* routes, used for testing.
|
||||
*/
|
||||
async start(registerExtras) {
|
||||
async start() {
|
||||
const {
|
||||
bind: { port, address: hostname },
|
||||
ssl: { isSecure: secure, cert, key },
|
||||
@@ -488,17 +478,25 @@ class Server {
|
||||
|
||||
log.log(`Server is starting up: ${this.baseUrl}`)
|
||||
|
||||
const app = (this.app = express())
|
||||
const camp = (this.camp = Camp.create({
|
||||
documentRoot: this.config.public.documentRoot,
|
||||
port,
|
||||
hostname,
|
||||
secure,
|
||||
staticMaxAge: 300,
|
||||
cert,
|
||||
key,
|
||||
}))
|
||||
|
||||
if (requireCloudflare) {
|
||||
this.requireCloudflare()
|
||||
}
|
||||
|
||||
const { githubConstellation, metricInstance } = this
|
||||
await githubConstellation.initialize(app)
|
||||
await githubConstellation.initialize(camp)
|
||||
if (metricInstance) {
|
||||
if (this.config.public.metrics.prometheus.endpointEnabled) {
|
||||
metricInstance.registerMetricsEndpoint(app)
|
||||
metricInstance.registerMetricsEndpoint(camp)
|
||||
}
|
||||
if (this.influxMetrics) {
|
||||
this.influxMetrics.startPushingMetrics()
|
||||
@@ -506,47 +504,39 @@ class Server {
|
||||
}
|
||||
|
||||
const { apiProvider: githubApiProvider } = this.githubConstellation
|
||||
setSuggestRoutes(allowedOrigin, githubApiProvider, app)
|
||||
setRoutes(allowedOrigin, githubApiProvider, camp)
|
||||
|
||||
// https://github.com/badges/shields/issues/3273
|
||||
app.use((req, res, next) => {
|
||||
camp.handle((req, res, next) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*')
|
||||
next()
|
||||
})
|
||||
|
||||
this.registerErrorHandlers()
|
||||
this.registerRedirects()
|
||||
app.use(
|
||||
express.static(this.config.public.documentRoot, {
|
||||
// Since express's `maxAge` parameter sets `Cache-Control: public`, set
|
||||
// the headers manually insetad.
|
||||
cacheControl: false,
|
||||
setHeaders: res =>
|
||||
res.setHeader('Cache-Control', 'max-age=300, s-maxage=300'),
|
||||
})
|
||||
)
|
||||
await this.registerServices()
|
||||
if (registerExtras) {
|
||||
registerExtras(app)
|
||||
|
||||
camp.timeout = this.config.public.requestTimeoutSeconds * 1000
|
||||
if (this.config.public.requestTimeoutSeconds > 0) {
|
||||
camp.on('timeout', socket => {
|
||||
const maxAge = this.config.public.requestTimeoutMaxAgeSeconds
|
||||
socket.write('HTTP/1.1 408 Request Timeout\r\n')
|
||||
socket.write('Content-Type: text/html; charset=UTF-8\r\n')
|
||||
socket.write('Content-Encoding: UTF-8\r\n')
|
||||
socket.write(`Cache-Control: max-age=${maxAge}, s-maxage=${maxAge}\r\n`)
|
||||
socket.write('Connection: close\r\n\r\n')
|
||||
socket.write('Request Timeout')
|
||||
socket.end()
|
||||
})
|
||||
}
|
||||
this.registerNotFoundHandlers()
|
||||
camp.listenAsConfigured()
|
||||
|
||||
if (secure) {
|
||||
this.server = https.createServer({ hostname, cert, key }, app)
|
||||
} else {
|
||||
this.server = http.createServer({ hostname }, app)
|
||||
}
|
||||
|
||||
this.server.setTimeout(this.config.public.requestTimeoutSeconds * 1000)
|
||||
|
||||
await new Promise(resolve =>
|
||||
this.server.listen({ host: hostname, port }, () => resolve())
|
||||
)
|
||||
await new Promise(resolve => camp.on('listening', () => resolve()))
|
||||
}
|
||||
|
||||
static resetGlobalState() {
|
||||
// TODO: This state should be migrated to instance state. When possible, do
|
||||
// not add new global state.
|
||||
// This state should be migrated to instance state. When possible, do not add new
|
||||
// global state.
|
||||
clearResourceCache()
|
||||
}
|
||||
|
||||
@@ -558,11 +548,10 @@ class Server {
|
||||
* Stop the HTTP server and clean up helpers
|
||||
*/
|
||||
async stop() {
|
||||
if (this.server) {
|
||||
await new Promise(resolve => this.server.close(() => resolve()))
|
||||
this.server = undefined
|
||||
if (this.camp) {
|
||||
await new Promise(resolve => this.camp.close(resolve))
|
||||
this.camp = undefined
|
||||
}
|
||||
this.app = undefined
|
||||
|
||||
if (this.cleanupMonitor) {
|
||||
this.cleanupMonitor()
|
||||
|
||||
@@ -73,7 +73,9 @@ describe('The server', function () {
|
||||
it('should redirect colorscheme PNG badges as configured', async function () {
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}:fruit-apple-green.png`,
|
||||
{ followRedirect: false }
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
)
|
||||
expect(statusCode).to.equal(301)
|
||||
expect(headers.location).to.equal(
|
||||
@@ -96,7 +98,7 @@ describe('The server', function () {
|
||||
`${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-type']).to.equal('image/svg+xml;charset=utf-8')
|
||||
expect(headers['content-length']).to.equal('1130')
|
||||
})
|
||||
|
||||
@@ -110,9 +112,7 @@ describe('The server', function () {
|
||||
`${baseUrl}:fruit-apple-green.json`
|
||||
)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(headers['content-type']).to.equal(
|
||||
'application/json; charset=utf-8'
|
||||
)
|
||||
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()
|
||||
@@ -200,12 +200,19 @@ describe('The server', function () {
|
||||
const { statusCode, body } = await got(`${baseUrl}npm/v/express.jpg`, {
|
||||
throwHttpErrors: false,
|
||||
})
|
||||
expect(statusCode).to.equal(410)
|
||||
// TODO It would be nice if this were 404 or 410.
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.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 () {
|
||||
@@ -238,12 +245,22 @@ describe('The server', function () {
|
||||
|
||||
// configure server to time out requests that take >2 seconds
|
||||
server = await createTestServer({ public: { requestTimeoutSeconds: 2 } })
|
||||
await server.start(app => {
|
||||
// /fast returns a 200 OK after a 1 second delay
|
||||
app.get('/fast', (req, res) => setTimeout(() => res.end(), 1000))
|
||||
await server.start()
|
||||
|
||||
// /slow returns a 200 OK after a 3 second delay
|
||||
app.get('/slow', (req, res) => setTimeout(() => res.end(), 3000))
|
||||
// /fast returns a 200 OK after a 1 second delay
|
||||
server.camp.route(/^\/fast$/, (data, match, end, ask) => {
|
||||
setTimeout(() => {
|
||||
ask.res.statusCode = 200
|
||||
ask.res.end()
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
// /slow returns a 200 OK after a 3 second delay
|
||||
server.camp.route(/^\/slow$/, (data, match, end, ask) => {
|
||||
setTimeout(() => {
|
||||
ask.res.statusCode = 200
|
||||
ask.res.end()
|
||||
}, 3000)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -256,9 +273,11 @@ describe('The server', function () {
|
||||
|
||||
it('should time out slow requests', async function () {
|
||||
this.timeout(10000)
|
||||
await expect(got(`${server.baseUrl}slow`)).to.be.rejectedWith(
|
||||
'socket hang up'
|
||||
)
|
||||
const { statusCode, body } = await got(`${server.baseUrl}slow`, {
|
||||
throwHttpErrors: false,
|
||||
})
|
||||
expect(statusCode).to.be.equal(408)
|
||||
expect(body).to.equal('Request Timeout')
|
||||
})
|
||||
|
||||
it('should not time out fast requests', async function () {
|
||||
|
||||
@@ -80,22 +80,29 @@ test this kind of logic through unit tests (e.g. of `render()` and
|
||||
reporting, loads config, and creates an instance of the server.
|
||||
|
||||
2. The Server, which is defined in
|
||||
[`core/server/server.js`][core/server/server], is based on [Express][].
|
||||
It creates an http server, sets up helpers for token persistence and
|
||||
monitoring. Then it loads all the services, injecting dependencies as it
|
||||
asks each one to register its route with the Express app.
|
||||
[`core/server/server.js`][core/server/server], is based on the web
|
||||
framework [Scoutcamp][]. It creates an http server, sets up helpers for
|
||||
token persistence and monitoring. Then it loads all the services,
|
||||
injecting dependencies as it asks each one to register its route
|
||||
with Scoutcamp.
|
||||
|
||||
3. The service registration continues in `BaseService.register`. From its
|
||||
`route` property, it derives a regular expression to match the route
|
||||
path, and invokes `app.get` with this value.
|
||||
path, and invokes `camp.route` with this value.
|
||||
|
||||
4. TODO: Explain what happens here (i.e. now that we've migrated from Scoutcamp
|
||||
to Express). `BaseService.invoke` instantiates the service and runs
|
||||
`BaseService#handle`.
|
||||
4. At this point the situation gets gnarly and hard to follow. For the
|
||||
purpose of initialization, suffice it to say that `camp.route` invokes a
|
||||
callback with the four parameters `( queryParams, match, end, ask )` which
|
||||
is created in a legacy helper function in
|
||||
[`legacy-request-handler.js`][legacy-request-handler]. This callback
|
||||
delegates to a callback in `BaseService.register` with four different
|
||||
parameters `( queryParams, match, sendBadge )`, which
|
||||
then runs `BaseService.invoke`. `BaseService.invoke` instantiates the
|
||||
service and runs `BaseService#handle`.
|
||||
|
||||
[entrypoint]: https://github.com/badges/shields/blob/master/server.js
|
||||
[core/server/server]: https://github.com/badges/shields/blob/master/core/server/server.js
|
||||
[express]: https://expressjs.com/
|
||||
[scoutcamp]: https://github.com/espadrine/sc
|
||||
[legacy-request-handler]: https://github.com/badges/shields/blob/master/core/base-service/legacy-request-handler.js
|
||||
|
||||
## Downstream caching
|
||||
@@ -112,15 +119,24 @@ test this kind of logic through unit tests (e.g. of `render()` and
|
||||
|
||||
## How the server makes a badge
|
||||
|
||||
1. An HTTPS request arrives. Express inspects the URL path and matches it
|
||||
against all the registered routes until it finds one that matches. (See
|
||||
*Initialization* above for an explanation of how routes are
|
||||
1. An HTTPS request arrives. Scoutcamp inspects the URL path and matches it
|
||||
against the regexes for all the registered routes until it finds one that
|
||||
matches. (See *Initialization* above for an explanation of how routes are
|
||||
registered.)
|
||||
2. Invoke the request handler function, defined in `BaseService.register`,
|
||||
which handles the request. It runs `BaseService.invoke`, which instantiates
|
||||
the service, injects more dependencies, and invokes `BaseService.handle`
|
||||
which is implemented by the service subclass.
|
||||
3. The job of `handle()`, which should be implemented by each service
|
||||
2. Scoutcamp invokes a callback with the four parameters:
|
||||
`( queryParams, match, end, ask )`. This callback is defined in
|
||||
[`legacy-request-handler`][legacy-request-handler]. A timeout is set to
|
||||
handle unresponsive service code and the next callback is invoked: the
|
||||
legacy handler function.
|
||||
3. The legacy handler function receives
|
||||
`( queryParams, match, sendBadge )`. Its job is to extract data
|
||||
from the regex `match` and `queryParams`, and then invoke `sendBadge`
|
||||
with the result.
|
||||
4. The implementation of this function is in `BaseService.register`. It
|
||||
works by running `BaseService.invoke`, which instantiates the service,
|
||||
injects more dependencies, and invokes `BaseService.handle` which is
|
||||
implemented by the service subclass.
|
||||
5. The job of `handle()`, which should be implemented by each service
|
||||
subclass, is to return an object which partially describes a badge or
|
||||
throw one of the handled error classes. "Partially rendered" most
|
||||
commonly means a non-empty message and an optional color. In the case
|
||||
@@ -130,7 +146,7 @@ test this kind of logic through unit tests (e.g. of `render()` and
|
||||
Throwing any other error is a programmer error which will be
|
||||
[reported][error reporting] and described to the user as a **shields
|
||||
internal error**.
|
||||
4. A typical `handle()` function delegates to one or more helpers to
|
||||
6. A typical `handle()` function delegates to one or more helpers to
|
||||
handle stages of the request:
|
||||
1. **fetch**: load the needed data from the upstream service and
|
||||
validate it
|
||||
@@ -138,13 +154,13 @@ test this kind of logic through unit tests (e.g. of `render()` and
|
||||
into a few properties which will be displayed on the badge
|
||||
3. **render**: given a few properties, return a message, optional
|
||||
color, and optional label.
|
||||
5. When an error is thrown, BaseService steps in and converts the error
|
||||
7. When an error is thrown, BaseService steps in and converts the error
|
||||
object to renderable properties: `{ isError, message, color }`.
|
||||
6. The service invokes [`coalesceBadge`][coalescebadge] whose job is to
|
||||
8. The service invokes [`coalesceBadge`][coalescebadge] whose job is to
|
||||
coalesce query string overrides with values from the service and the
|
||||
service’s defaults to produce an object that fully describes the badge to
|
||||
be rendered.
|
||||
7. `sendBadge` is invoked with that object. It does some housekeeping on the
|
||||
9. `sendBadge` is invoked with that object. It does some housekeeping on the
|
||||
timeout. Then it renders the badge to svg or raster and pushes out the
|
||||
result over the HTTPS connection.
|
||||
|
||||
|
||||
@@ -25,9 +25,6 @@ export function getBaseUrl(): string {
|
||||
if (['shields.io', 'www.shields.io'].includes(hostname)) {
|
||||
return 'https://img.shields.io'
|
||||
}
|
||||
if (!port) {
|
||||
return `${protocol}//${hostname}`
|
||||
}
|
||||
return `${protocol}//${hostname}:${port}`
|
||||
} catch (e) {
|
||||
// server-side rendering
|
||||
|
||||
4282
package-lock.json
generated
4282
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
92
package.json
92
package.json
@@ -21,29 +21,28 @@
|
||||
"url": "https://github.com/badges/shields"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/lato": "^4.5.5",
|
||||
"@fontsource/lekton": "^4.5.6",
|
||||
"@renovate/pep440": "^1.0.0",
|
||||
"@sentry/node": "^6.19.6",
|
||||
"@fontsource/lato": "^4.5.1",
|
||||
"@fontsource/lekton": "^4.5.2",
|
||||
"@sentry/node": "^6.17.4",
|
||||
"@shields_io/camp": "^18.1.1",
|
||||
"badge-maker": "file:badge-maker",
|
||||
"bytes": "^3.1.2",
|
||||
"camelcase": "^6.3.0",
|
||||
"chalk": "^5.0.1",
|
||||
"chalk": "^5.0.0",
|
||||
"check-node-version": "^4.2.1",
|
||||
"cloudflare-middleware": "^1.0.4",
|
||||
"config": "^3.3.7",
|
||||
"cross-env": "^7.0.3",
|
||||
"decamelize": "^3.2.0",
|
||||
"emojic": "^1.1.17",
|
||||
"emojic": "^1.1.16",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"express": "^4.17.3",
|
||||
"fast-xml-parser": "^4.0.7",
|
||||
"glob": "^8.0.1",
|
||||
"fast-xml-parser": "^4.0.2",
|
||||
"glob": "^7.2.0",
|
||||
"global-agent": "^3.0.0",
|
||||
"got": "^12.0.3",
|
||||
"got": "^12.0.1",
|
||||
"graphql": "^15.6.1",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"ioredis": "5.0.4",
|
||||
"ioredis": "4.28.4",
|
||||
"joi": "17.6.0",
|
||||
"joi-extension-semver": "5.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
@@ -51,18 +50,17 @@
|
||||
"lodash.countby": "^4.6.0",
|
||||
"lodash.groupby": "^4.6.0",
|
||||
"lodash.times": "^4.3.2",
|
||||
"moment": "^2.29.2",
|
||||
"multer": "^1.4.4",
|
||||
"moment": "^2.29.1",
|
||||
"node-env-flag": "^0.1.0",
|
||||
"parse-link-header": "^2.0.0",
|
||||
"path-to-regexp": "^6.2.0",
|
||||
"pretty-bytes": "^6.0.0",
|
||||
"pretty-bytes": "^5.6.0",
|
||||
"priorityqueuejs": "^2.0.0",
|
||||
"prom-client": "^14.0.1",
|
||||
"qs": "^6.10.3",
|
||||
"query-string": "^7.1.1",
|
||||
"semver": "~7.3.7",
|
||||
"simple-icons": "6.18.0",
|
||||
"semver": "~7.3.5",
|
||||
"simple-icons": "6.8.0",
|
||||
"webextension-store-meta": "^1.0.5",
|
||||
"xmldom": "~0.6.0",
|
||||
"xpath": "~0.0.32"
|
||||
@@ -142,12 +140,12 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.17.9",
|
||||
"@babel/core": "^7.17.0",
|
||||
"@babel/polyfill": "^7.12.1",
|
||||
"@babel/register": "7.17.7",
|
||||
"@babel/register": "7.17.0",
|
||||
"@istanbuljs/schema": "^0.1.3",
|
||||
"@mapbox/react-click-to-select": "^2.2.1",
|
||||
"@types/chai": "^4.3.1",
|
||||
"@types/chai": "^4.3.0",
|
||||
"@types/lodash.debounce": "^4.0.6",
|
||||
"@types/lodash.groupby": "^4.6.6",
|
||||
"@types/mocha": "^9.1.0",
|
||||
@@ -155,60 +153,60 @@
|
||||
"@types/react-helmet": "^6.1.5",
|
||||
"@types/react-modal": "^3.13.1",
|
||||
"@types/react-select": "^4.0.17",
|
||||
"@types/styled-components": "5.1.25",
|
||||
"@typescript-eslint/eslint-plugin": "^5.19.0",
|
||||
"@typescript-eslint/parser": "^5.15.0",
|
||||
"@types/styled-components": "5.1.22",
|
||||
"@typescript-eslint/eslint-plugin": "^5.10.2",
|
||||
"@typescript-eslint/parser": "^5.10.0",
|
||||
"babel-plugin-inline-react-svg": "^2.0.1",
|
||||
"babel-preset-gatsby": "^2.11.1",
|
||||
"babel-preset-gatsby": "^2.5.1",
|
||||
"c8": "^7.11.0",
|
||||
"caller": "^1.1.0",
|
||||
"caller": "^1.0.1",
|
||||
"chai": "^4.3.6",
|
||||
"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.1.0",
|
||||
"cypress": "^9.5.4",
|
||||
"concurrently": "^7.0.0",
|
||||
"cypress": "^9.4.1",
|
||||
"danger": "^11.0.2",
|
||||
"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.2.7",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-jsdoc": "^37.7.1",
|
||||
"eslint-plugin-mocha": "^10.0.3",
|
||||
"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.29.4",
|
||||
"eslint-plugin-react-hooks": "^4.4.0",
|
||||
"eslint-plugin-promise": "^6.0.0",
|
||||
"eslint-plugin-react": "^7.28.0",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"eslint-plugin-sort-class-members": "^1.14.1",
|
||||
"fetch-ponyfill": "^7.1.0",
|
||||
"form-data": "^4.0.0",
|
||||
"gatsby": "4.6.2",
|
||||
"gatsby-plugin-catch-links": "^4.11.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.11.0",
|
||||
"gatsby-plugin-typescript": "^4.11.1",
|
||||
"gatsby-plugin-catch-links": "^4.5.0",
|
||||
"gatsby-plugin-page-creator": "^4.3.0",
|
||||
"gatsby-plugin-react-helmet": "^5.2.0",
|
||||
"gatsby-plugin-remove-trailing-slashes": "^4.2.0",
|
||||
"gatsby-plugin-styled-components": "^5.2.0",
|
||||
"gatsby-plugin-typescript": "^4.2.0",
|
||||
"humanize-string": "^2.1.0",
|
||||
"icedfrisby": "4.0.0",
|
||||
"icedfrisby-nock": "^2.1.0",
|
||||
"is-svg": "^4.3.2",
|
||||
"js-yaml-loader": "^1.2.2",
|
||||
"jsdoc": "^3.6.10",
|
||||
"lint-staged": "^12.3.8",
|
||||
"lint-staged": "^12.3.3",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.difference": "^4.5.0",
|
||||
"minimist": "^1.2.6",
|
||||
"mocha": "^9.2.2",
|
||||
"minimist": "^1.2.5",
|
||||
"mocha": "^9.2.0",
|
||||
"mocha-env-reporter": "^4.0.0",
|
||||
"mocha-junit-reporter": "^2.0.2",
|
||||
"mocha-yaml-loader": "^1.0.3",
|
||||
@@ -218,10 +216,10 @@
|
||||
"npm-run-all": "^4.1.5",
|
||||
"open-cli": "^7.0.1",
|
||||
"portfinder": "^1.0.28",
|
||||
"prettier": "2.6.2",
|
||||
"prettier": "2.5.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-error-overlay": "^6.0.11",
|
||||
"react-error-overlay": "^6.0.10",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-modal": "^3.14.4",
|
||||
"react-pose": "^4.0.10",
|
||||
@@ -231,14 +229,14 @@
|
||||
"rimraf": "^3.0.2",
|
||||
"sazerac": "^2.0.0",
|
||||
"simple-git-hooks": "^2.7.0",
|
||||
"sinon": "^13.0.2",
|
||||
"sinon": "^13.0.1",
|
||||
"sinon-chai": "^3.7.0",
|
||||
"snap-shot-it": "^7.9.6",
|
||||
"start-server-and-test": "1.14.0",
|
||||
"styled-components": "^5.3.5",
|
||||
"styled-components": "^5.3.3",
|
||||
"ts-mocha": "^9.0.2",
|
||||
"tsd": "^0.20.0",
|
||||
"typescript": "^4.6.3",
|
||||
"tsd": "^0.19.1",
|
||||
"typescript": "^4.5.5",
|
||||
"url": "^0.11.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -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}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,6 @@ class AmoWeeklyDownloads extends BaseAmoService {
|
||||
},
|
||||
]
|
||||
|
||||
static _cacheLength = 21600
|
||||
|
||||
static defaultBadgeData = { label: 'downloads' }
|
||||
|
||||
static render({ downloads }) {
|
||||
|
||||
@@ -23,8 +23,6 @@ export default class AmoRating extends BaseAmoService {
|
||||
},
|
||||
]
|
||||
|
||||
static _cacheLength = 7200
|
||||
|
||||
static render({ format, rating }) {
|
||||
rating = Math.round(rating)
|
||||
return {
|
||||
|
||||
@@ -14,8 +14,6 @@ export default class AmoUsers extends BaseAmoService {
|
||||
},
|
||||
]
|
||||
|
||||
static _cacheLength = 21600
|
||||
|
||||
static defaultBadgeData = { label: 'users' }
|
||||
|
||||
static render({ users: downloads }) {
|
||||
|
||||
@@ -21,8 +21,8 @@ export default class Bitrise extends BaseJsonService {
|
||||
static examples = [
|
||||
{
|
||||
title: 'Bitrise',
|
||||
namedParams: { appId: '3ff11fe8457bd304', branch: 'master' },
|
||||
queryParams: { token: 'lESRN9rEFFfDq92JtXs_jw' },
|
||||
namedParams: { appId: 'cde737473028420d', branch: 'master' },
|
||||
queryParams: { token: 'GCIdEzacE4GW32jLVrZb7A' },
|
||||
staticPreview: this.render({ status: 'success' }),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -3,14 +3,14 @@ import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('deploy status')
|
||||
.get('/3ff11fe8457bd304.json?token=lESRN9rEFFfDq92JtXs_jw')
|
||||
.get('/cde737473028420d.json?token=GCIdEzacE4GW32jLVrZb7A')
|
||||
.expectBadge({
|
||||
label: 'bitrise',
|
||||
message: isBuildStatus,
|
||||
})
|
||||
|
||||
t.create('deploy status with branch')
|
||||
.get('/3ff11fe8457bd304/master.json?token=lESRN9rEFFfDq92JtXs_jw')
|
||||
.get('/cde737473028420d/master.json?token=GCIdEzacE4GW32jLVrZb7A')
|
||||
.expectBadge({
|
||||
label: 'bitrise',
|
||||
message: isBuildStatus,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import Joi from 'joi'
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
import { InvalidResponse } from '../index.js'
|
||||
import { InvalidResponse, redirector } from '../index.js'
|
||||
import BaseBowerService from './bower-base.js'
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
include_prereleases: Joi.equal(''),
|
||||
}).required()
|
||||
|
||||
export default class BowerVersion extends BaseBowerService {
|
||||
class BowerVersion extends BaseBowerService {
|
||||
static category = 'version'
|
||||
static route = { base: 'bower/v', pattern: ':packageName', queryParamSchema }
|
||||
|
||||
@@ -47,3 +47,18 @@ export default class BowerVersion extends BaseBowerService {
|
||||
return renderVersionBadge({ version })
|
||||
}
|
||||
}
|
||||
|
||||
const BowerVersionRedirect = redirector({
|
||||
category: 'version',
|
||||
route: {
|
||||
base: 'bower/vpre',
|
||||
pattern: ':packageName',
|
||||
},
|
||||
transformPath: ({ packageName }) => `/bower/v/${packageName}`,
|
||||
transformQueryParams: params => ({
|
||||
include_prereleases: null,
|
||||
}),
|
||||
dateAdded: new Date('2019-12-15'),
|
||||
})
|
||||
|
||||
export { BowerVersion, BowerVersionRedirect }
|
||||
|
||||
@@ -4,7 +4,7 @@ import nock from 'nock'
|
||||
import { cleanUpNockAfterEach, defaultContext } from '../test-helpers.js'
|
||||
import { InvalidResponse } from '../index.js'
|
||||
import LibrariesIoApiProvider from '../librariesio/librariesio-api-provider.js'
|
||||
import BowerVersion from './bower-version.service.js'
|
||||
import { BowerVersion } from './bower-version.service.js'
|
||||
|
||||
describe('BowerVersion', function () {
|
||||
test(BowerVersion.transform, () => {
|
||||
|
||||
@@ -33,3 +33,7 @@ t.create('Pre Version for Invalid Package')
|
||||
.timeout(10000)
|
||||
.get('/v/it-is-a-invalid-package-should-error.json?include_prereleases')
|
||||
.expectBadge({ label: 'bower', message: 'package not found' })
|
||||
|
||||
t.create('Version (legacy redirect: vpre)')
|
||||
.get('/vpre/bootstrap.svg')
|
||||
.expectRedirect('/bower/v/bootstrap.svg?include_prereleases')
|
||||
|
||||
@@ -26,29 +26,31 @@ export default class Cirrus extends BaseJsonService {
|
||||
title: 'Cirrus CI - Base Branch Build Status',
|
||||
namedParams: { user: 'flutter', repo: 'flutter' },
|
||||
pattern: 'github/:user/:repo',
|
||||
queryParams: { task: 'analyze', script: 'test' },
|
||||
staticPreview: this.render({ status: 'passing' }),
|
||||
},
|
||||
{
|
||||
title: 'Cirrus CI - Specific Branch Build Status',
|
||||
pattern: 'github/:user/:repo/:branch',
|
||||
namedParams: { user: 'flutter', repo: 'flutter', branch: 'master' },
|
||||
queryParams: { task: 'analyze', script: 'test' },
|
||||
staticPreview: this.render({ status: 'passing' }),
|
||||
},
|
||||
{
|
||||
title: 'Cirrus CI - Specific Task Build Status',
|
||||
pattern: 'github/:user/:repo',
|
||||
queryParams: { task: 'build_docker' },
|
||||
namedParams: { user: 'flutter', repo: 'cocoon' },
|
||||
staticPreview: this.render({
|
||||
subject: 'build_docker',
|
||||
status: 'passing',
|
||||
}),
|
||||
queryParams: { task: 'analyze' },
|
||||
namedParams: { user: 'flutter', repo: 'flutter' },
|
||||
staticPreview: this.render({ subject: 'analyze', status: 'passing' }),
|
||||
},
|
||||
{
|
||||
title: 'Cirrus CI - Task and Script Build Status',
|
||||
pattern: 'github/:user/:repo',
|
||||
queryParams: { task: 'build_docker', script: 'test' },
|
||||
namedParams: { user: 'flutter', repo: 'cocoon' },
|
||||
queryParams: { task: 'analyze', script: 'test' },
|
||||
namedParams: {
|
||||
user: 'flutter',
|
||||
repo: 'flutter',
|
||||
},
|
||||
staticPreview: this.render({ subject: 'test', status: 'passing' }),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -138,19 +138,15 @@ export default class CodeclimateAnalysis extends BaseJsonService {
|
||||
}
|
||||
|
||||
async fetch({ user, repo }) {
|
||||
const repoInfos = await fetchRepo(this, { user, repo })
|
||||
const repoInfosWithSnapshot = repoInfos.filter(
|
||||
repoInfo => repoInfo.relationships.latest_default_branch_snapshot.data
|
||||
)
|
||||
if (repoInfosWithSnapshot.length === 0) {
|
||||
throw new NotFound({ prettyMessage: 'snapshot not found' })
|
||||
}
|
||||
const {
|
||||
id: repoId,
|
||||
relationships: {
|
||||
latest_default_branch_snapshot: { data: snapshotInfo },
|
||||
},
|
||||
} = repoInfosWithSnapshot[0]
|
||||
} = await fetchRepo(this, { user, repo })
|
||||
if (snapshotInfo === null) {
|
||||
throw new NotFound({ prettyMessage: 'snapshot not found' })
|
||||
}
|
||||
const { data } = await this._requestJson({
|
||||
schema,
|
||||
url: `https://api.codeclimate.com/v1/repos/${repoId}/snapshots/${snapshotInfo.id}`,
|
||||
|
||||
@@ -32,47 +32,6 @@ t.create('maintainability letter')
|
||||
message: Joi.equal('A', 'B', 'C', 'D', 'E', 'F'),
|
||||
})
|
||||
|
||||
t.create('issues when outer user repos query returns multiple items')
|
||||
.get('/issues/angular/angular.json')
|
||||
.intercept(nock =>
|
||||
nock('https://api.codeclimate.com', { allowUnmocked: true })
|
||||
.get('/v1/repos?github_slug=angular%2Fangular')
|
||||
.reply(200, {
|
||||
data: [
|
||||
{
|
||||
id: '54fd4e6b6956804a10003df4',
|
||||
relationships: {
|
||||
latest_default_branch_snapshot: {
|
||||
data: null,
|
||||
},
|
||||
latest_default_branch_test_report: {
|
||||
data: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '54fd4e6b6956804a10003df3',
|
||||
relationships: {
|
||||
latest_default_branch_snapshot: {
|
||||
data: {
|
||||
id: '620e2b491b6a72000100ca1d',
|
||||
type: 'snapshots',
|
||||
},
|
||||
},
|
||||
latest_default_branch_test_report: {
|
||||
data: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
.networkOn() // Combined with allowUnmocked: true, this allows the inner snapshots query to go through.
|
||||
.expectBadge({
|
||||
label: 'issues',
|
||||
message: Joi.number().integer().positive(),
|
||||
})
|
||||
|
||||
t.create('maintainability letter for non-existent repo')
|
||||
.get('/maintainability/unknown/unknown.json')
|
||||
.expectBadge({
|
||||
|
||||
@@ -7,6 +7,7 @@ const isLetterGrade = Joi.equal('A', 'B', 'C', 'D', 'E', 'F').required()
|
||||
|
||||
const repoSchema = Joi.object({
|
||||
data: Joi.array()
|
||||
.max(1)
|
||||
.items(
|
||||
Joi.object({
|
||||
id: Joi.string().required(),
|
||||
@@ -28,15 +29,17 @@ const repoSchema = Joi.object({
|
||||
}).required()
|
||||
|
||||
async function fetchRepo(serviceInstance, { user, repo }) {
|
||||
const { data: repoInfos } = await serviceInstance._requestJson({
|
||||
const {
|
||||
data: [repoInfo],
|
||||
} = await serviceInstance._requestJson({
|
||||
schema: repoSchema,
|
||||
url: 'https://api.codeclimate.com/v1/repos',
|
||||
options: { searchParams: { github_slug: `${user}/${repo}` } },
|
||||
})
|
||||
if (repoInfos.length === 0) {
|
||||
if (repoInfo === undefined) {
|
||||
throw new NotFound({ prettyMessage: 'repo not found' })
|
||||
}
|
||||
return repoInfos
|
||||
return repoInfo
|
||||
}
|
||||
|
||||
export { keywords, isLetterGrade, fetchRepo }
|
||||
|
||||
@@ -53,19 +53,15 @@ export default class CodeclimateCoverage extends BaseJsonService {
|
||||
}
|
||||
|
||||
async fetch({ user, repo }) {
|
||||
const repoInfos = await fetchRepo(this, { user, repo })
|
||||
const repoInfosWithTestReport = repoInfos.filter(
|
||||
repoInfo => repoInfo.relationships.latest_default_branch_test_report.data
|
||||
)
|
||||
if (repoInfosWithTestReport.length === 0) {
|
||||
throw new NotFound({ prettyMessage: 'test report not found' })
|
||||
}
|
||||
const {
|
||||
id: repoId,
|
||||
relationships: {
|
||||
latest_default_branch_test_report: { data: testReportInfo },
|
||||
},
|
||||
} = repoInfosWithTestReport[0]
|
||||
} = await fetchRepo(this, { user, repo })
|
||||
if (testReportInfo === null) {
|
||||
throw new NotFound({ prettyMessage: 'test report not found' })
|
||||
}
|
||||
const { data } = await this._requestJson({
|
||||
schema,
|
||||
url: `https://api.codeclimate.com/v1/repos/${repoId}/test_reports/${testReportInfo.id}`,
|
||||
|
||||
@@ -20,47 +20,6 @@ t.create('test coverage letter')
|
||||
message: Joi.equal('A', 'B', 'C', 'D', 'E', 'F'),
|
||||
})
|
||||
|
||||
t.create('test coverage when outer user repos query returns multiple items')
|
||||
.get('/coverage/codeclimate/codeclimate.json')
|
||||
.intercept(nock =>
|
||||
nock('https://api.codeclimate.com', { allowUnmocked: true })
|
||||
.get('/v1/repos?github_slug=codeclimate%2Fcodeclimate')
|
||||
.reply(200, {
|
||||
data: [
|
||||
{
|
||||
id: '558479d6e30ba034120008a8',
|
||||
relationships: {
|
||||
latest_default_branch_snapshot: {
|
||||
data: null,
|
||||
},
|
||||
latest_default_branch_test_report: {
|
||||
data: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '558479d6e30ba034120008a9',
|
||||
relationships: {
|
||||
latest_default_branch_snapshot: {
|
||||
data: null,
|
||||
},
|
||||
latest_default_branch_test_report: {
|
||||
data: {
|
||||
id: '62110434a7160b00010b4b59',
|
||||
type: 'test_reports',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
.networkOn() // Combined with allowUnmocked: true, this allows the inner test reports query to go through.
|
||||
.expectBadge({
|
||||
label: 'coverage',
|
||||
message: isIntegerPercentage,
|
||||
})
|
||||
|
||||
t.create('test coverage percentage for non-existent repo')
|
||||
.get('/coverage/unknown/unknown.json')
|
||||
.expectBadge({
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* including colours based off download count, version number, etc.
|
||||
*/
|
||||
import moment from 'moment'
|
||||
import pep440 from '@renovate/pep440'
|
||||
|
||||
function version(version) {
|
||||
if (typeof version !== 'string' && typeof version !== 'number') {
|
||||
@@ -21,17 +20,6 @@ function version(version) {
|
||||
}
|
||||
}
|
||||
|
||||
function pep440VersionColor(version) {
|
||||
if (!pep440.valid(version)) {
|
||||
return 'lightgrey'
|
||||
}
|
||||
const parsedVersion = pep440.explain(version)
|
||||
if (parsedVersion.is_prerelease || parsedVersion.public.startsWith('0.')) {
|
||||
return 'orange'
|
||||
}
|
||||
return 'blue'
|
||||
}
|
||||
|
||||
function floorCount(value, yellow, yellowgreen, green) {
|
||||
if (value <= 0) {
|
||||
return 'red'
|
||||
@@ -118,7 +106,6 @@ function age(date) {
|
||||
|
||||
export {
|
||||
version,
|
||||
pep440VersionColor,
|
||||
downloadCount,
|
||||
coveragePercentage,
|
||||
floorCount,
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
letterScore,
|
||||
age,
|
||||
version,
|
||||
pep440VersionColor,
|
||||
} from './color-formatters.js'
|
||||
|
||||
describe('Color formatters', function () {
|
||||
@@ -107,45 +106,4 @@ describe('Color formatters', function () {
|
||||
"Can't generate a version color for [object Object]"
|
||||
)
|
||||
})
|
||||
|
||||
test(pep440VersionColor, () => {
|
||||
forCases([
|
||||
given('1.0.1'),
|
||||
given('v2.1.6'),
|
||||
given('1.0.1+abcd'),
|
||||
given('1.0'),
|
||||
given('v1'),
|
||||
given(9),
|
||||
given(1.0),
|
||||
]).expect('blue')
|
||||
|
||||
forCases([
|
||||
given('1.0.1-rc1'),
|
||||
given('1.0.1rc1'),
|
||||
given('1.0.0-Beta'),
|
||||
given('1.0.0Beta'),
|
||||
given('1.1.0-alpha'),
|
||||
given('1.1.0alpha'),
|
||||
given('1.0.1-dev'),
|
||||
given('1.0.1dev'),
|
||||
given('2.1.6-b1'),
|
||||
given('2.1.6b1'),
|
||||
given('0.1.0'),
|
||||
given('v0.1.0'),
|
||||
given('v2.1.6-b1'),
|
||||
given('0.1.0+abcd'),
|
||||
given('2.1.6-b1+abcd'),
|
||||
given('0.0.0'),
|
||||
given(0.1),
|
||||
given('0.9'),
|
||||
]).expect('orange')
|
||||
|
||||
forCases([
|
||||
given('6.0.0-SNAPSHOT'),
|
||||
given('2.1.6-prerelease'),
|
||||
given(true),
|
||||
given(null),
|
||||
given('cheese'),
|
||||
]).expect('lightgrey')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import yaml from 'js-yaml'
|
||||
import { NotFound, InvalidResponse } from '../index.js'
|
||||
import { latest } from '../version.js'
|
||||
|
||||
export function parseLatestVersionFromConfig(configYaml) {
|
||||
let versions
|
||||
try {
|
||||
const config = yaml.load(configYaml)
|
||||
versions = Object.keys(config.versions)
|
||||
} catch (err) {
|
||||
throw new InvalidResponse({
|
||||
prettyMessage: 'invalid config.yml',
|
||||
underlyingError: err,
|
||||
})
|
||||
}
|
||||
const version = latest(versions)
|
||||
if (version == null) {
|
||||
throw new NotFound({ prettyMessage: 'no versions found' })
|
||||
}
|
||||
return version
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { expect } from 'chai'
|
||||
import { NotFound, InvalidResponse } from '../index.js'
|
||||
import { parseLatestVersionFromConfig } from './conan-version-helpers.js'
|
||||
|
||||
describe('parseLatestVersionFromConfig', function () {
|
||||
it('returns latest available version', function () {
|
||||
expect(
|
||||
parseLatestVersionFromConfig(`
|
||||
versions:
|
||||
1.68.0:
|
||||
folder: all
|
||||
1.70.0:
|
||||
folder: all
|
||||
1.69.0:
|
||||
folder: all
|
||||
`)
|
||||
).to.equal('1.70.0')
|
||||
})
|
||||
|
||||
it('rejects invalid yaml', function () {
|
||||
expect(() => parseLatestVersionFromConfig('[')).to.throw(InvalidResponse)
|
||||
})
|
||||
it('treats no results array as invalid', function () {
|
||||
expect(() =>
|
||||
parseLatestVersionFromConfig('somethingElse: whatever')
|
||||
).to.throw(InvalidResponse)
|
||||
})
|
||||
it('treats empty results array as not found', function () {
|
||||
expect(() => parseLatestVersionFromConfig('versions: []')).to.throw(
|
||||
NotFound
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,34 +0,0 @@
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
import { ConditionalGithubAuthV3Service } from '../github/github-auth-service.js'
|
||||
import { fetchRepoContent } from '../github/github-common-fetch.js'
|
||||
import { parseLatestVersionFromConfig } from './conan-version-helpers.js'
|
||||
|
||||
export default class ConanVersion extends ConditionalGithubAuthV3Service {
|
||||
static category = 'version'
|
||||
|
||||
static route = { base: 'conan/v', pattern: ':packageName' }
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'Conan Center',
|
||||
namedParams: { packageName: 'boost' },
|
||||
staticPreview: renderVersionBadge({ version: '1.78.0' }),
|
||||
keywords: ['c++'],
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = { label: 'conan' }
|
||||
|
||||
async handle({ packageName }) {
|
||||
const configContent = await fetchRepoContent(this, {
|
||||
user: 'conan-io',
|
||||
repo: 'conan-center-index',
|
||||
branch: 'master',
|
||||
filename: `recipes/${packageName}/config.yml`,
|
||||
})
|
||||
|
||||
const version = parseLatestVersionFromConfig(configContent)
|
||||
|
||||
return renderVersionBadge({ version })
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { isSemver } from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('gets the package version of zeromq')
|
||||
.get('/zeromq.json')
|
||||
.expectBadge({ label: 'conan', message: isSemver })
|
||||
|
||||
t.create('returns not found for invalid package')
|
||||
.get('/this package does not exist - shields test.json')
|
||||
.expectBadge({
|
||||
label: 'conan',
|
||||
color: 'red',
|
||||
message:
|
||||
'repo not found, branch not found, or recipes/this package does not exist - shields test/config.yml missing',
|
||||
})
|
||||
@@ -52,7 +52,9 @@ export default class DocsRs extends BaseJsonService {
|
||||
}
|
||||
|
||||
async handle({ crate, version = 'latest' }) {
|
||||
const [{ build_status: buildStatus }] = await this.fetch({ crate, version })
|
||||
const { build_status: buildStatus } = (
|
||||
await this.fetch({ crate, version })
|
||||
).pop()
|
||||
return this.constructor.render({ version, buildStatus })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,6 @@ t.create('Failing docs')
|
||||
.get('/tensorflow/0.16.1.json')
|
||||
.expectBadge({ label: 'docs@0.16.1', message: 'failing' })
|
||||
|
||||
t.create('Multiple builds, latest passing')
|
||||
.get('/bevy_tweening/0.3.1.json')
|
||||
.expectBadge({ label: 'docs@0.3.1', message: 'passing' })
|
||||
|
||||
t.create('Getting latest version works')
|
||||
.get('/rand/latest.json')
|
||||
.expectBadge({
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Joi from 'joi'
|
||||
import { floorCount as floorCountColor } from '../color-formatters.js'
|
||||
import { metric } from '../text-formatters.js'
|
||||
import { BaseJsonService } from '../index.js'
|
||||
|
||||
const ownerSchema = Joi.array().required()
|
||||
@@ -21,7 +20,7 @@ export default class GemOwner extends BaseJsonService {
|
||||
|
||||
static render({ count }) {
|
||||
return {
|
||||
message: metric(count),
|
||||
message: count,
|
||||
color: floorCountColor(count, 10, 50, 100),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import queryString from 'query-string'
|
||||
import multer from 'multer'
|
||||
import { fetch } from '../../../core/base-service/got.js'
|
||||
import log from '../../../core/server/log.js'
|
||||
|
||||
function setRoutes({ app, authHelper, onTokenAccepted }) {
|
||||
function setRoutes({ server, authHelper, onTokenAccepted }) {
|
||||
const baseUrl = process.env.GATSBY_BASE_URL || 'https://img.shields.io'
|
||||
|
||||
app.post('/github-auth', (req, res) => {
|
||||
res.status(302) // Found.
|
||||
server.route(/^\/github-auth$/, (data, match, end, ask) => {
|
||||
ask.res.statusCode = 302 // Found.
|
||||
const query = queryString.stringify({
|
||||
// TODO The `_user` property bypasses security checks in AuthHelper.
|
||||
// (e.g: enforceStrictSsl and shouldAuthenticateRequest).
|
||||
@@ -16,64 +15,56 @@ function setRoutes({ app, authHelper, onTokenAccepted }) {
|
||||
client_id: authHelper._user,
|
||||
redirect_uri: `${baseUrl}/github-auth/done`,
|
||||
})
|
||||
res.setHeader(
|
||||
ask.res.setHeader(
|
||||
'Location',
|
||||
`https://github.com/login/oauth/authorize?${query}`
|
||||
)
|
||||
res.end()
|
||||
end('')
|
||||
})
|
||||
|
||||
app.post('/github-auth/done', multer().none(), async (req, res) => {
|
||||
const code = (req.body ?? {}).code
|
||||
server.route(/^\/github-auth\/done$/, async (data, match, end, ask) => {
|
||||
if (!data.code) {
|
||||
log.log(`GitHub OAuth data: ${JSON.stringify(data)}`)
|
||||
return end('GitHub OAuth authentication failed to provide a code.')
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
log.log(`GitHub OAuth data: ${JSON.stringify(req.body)}`)
|
||||
res.send('GitHub OAuth authentication failed to provide a code.')
|
||||
res.end()
|
||||
return
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||
},
|
||||
form: {
|
||||
// TODO The `_user` and `_pass` properties bypass security checks in
|
||||
// AuthHelper (e.g: enforceStrictSsl and shouldAuthenticateRequest).
|
||||
// Do not use them elsewhere. It would be better to clean
|
||||
// this up so it's not setting a bad example.
|
||||
client_id: authHelper._user,
|
||||
client_secret: authHelper._pass,
|
||||
code: data.code,
|
||||
},
|
||||
}
|
||||
|
||||
let resp
|
||||
try {
|
||||
resp = await fetch('https://github.com/login/oauth/access_token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||
},
|
||||
form: {
|
||||
// TODO The `_user` and `_pass` properties bypass security checks in
|
||||
// AuthHelper (e.g: enforceStrictSsl and shouldAuthenticateRequest).
|
||||
// Do not use them elsewhere. It would be better to clean
|
||||
// this up so it's not setting a bad example.
|
||||
client_id: authHelper._user,
|
||||
client_secret: authHelper._pass,
|
||||
code,
|
||||
},
|
||||
})
|
||||
resp = await fetch('https://github.com/login/oauth/access_token', options)
|
||||
} catch (e) {
|
||||
res.send('The connection to GitHub failed.')
|
||||
res.end()
|
||||
return
|
||||
return end('The connection to GitHub failed.')
|
||||
}
|
||||
|
||||
let content
|
||||
try {
|
||||
content = queryString.parse(resp.buffer)
|
||||
} catch (e) {
|
||||
res.send('The GitHub OAuth token could not be parsed.')
|
||||
res.end()
|
||||
return
|
||||
return end('The GitHub OAuth token could not be parsed.')
|
||||
}
|
||||
|
||||
const { access_token: token } = content
|
||||
if (!token) {
|
||||
res.send('The GitHub OAuth process did not return a user token.')
|
||||
res.end()
|
||||
return
|
||||
return end('The GitHub OAuth process did not return a user token.')
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'text/html')
|
||||
res.send(
|
||||
ask.res.setHeader('Content-Type', 'text/html')
|
||||
end(
|
||||
'<p>Shields.io has received your app-specific GitHub user token. ' +
|
||||
'You can revoke it by going to ' +
|
||||
'<a href="https://github.com/settings/applications">GitHub</a>.</p>' +
|
||||
@@ -84,7 +75,6 @@ function setRoutes({ app, authHelper, onTokenAccepted }) {
|
||||
'everyone!</p>' +
|
||||
'<p><a href="/">Back to the website</a></p>'
|
||||
)
|
||||
res.end()
|
||||
|
||||
onTokenAccepted(token)
|
||||
})
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { expect } from 'chai'
|
||||
import Camp from '@shields_io/camp'
|
||||
import FormData from 'form-data'
|
||||
import sinon from 'sinon'
|
||||
import portfinder from 'portfinder'
|
||||
import queryString from 'query-string'
|
||||
import nock from 'nock'
|
||||
import { ExpressTestHarness } from '../../../core/express-test-harness.js'
|
||||
import got from '../../../core/got-test-client.js'
|
||||
import GithubConstellation from '../github-constellation.js'
|
||||
import { setRoutes } from './acceptor.js'
|
||||
|
||||
@@ -15,26 +17,36 @@ describe('Github token acceptor', function () {
|
||||
private: { gh_client_id: fakeClientId, gh_client_secret: fakeClientSecret },
|
||||
})
|
||||
|
||||
let harness, onTokenAccepted
|
||||
let port, baseUrl
|
||||
beforeEach(async function () {
|
||||
harness = new ExpressTestHarness()
|
||||
port = await portfinder.getPortPromise()
|
||||
baseUrl = `http://127.0.0.1:${port}`
|
||||
})
|
||||
|
||||
let camp
|
||||
beforeEach(async function () {
|
||||
camp = Camp.start({ port, hostname: '::' })
|
||||
await new Promise(resolve => camp.on('listening', () => resolve()))
|
||||
})
|
||||
afterEach(async function () {
|
||||
if (camp) {
|
||||
await new Promise(resolve => camp.close(resolve))
|
||||
camp = undefined
|
||||
}
|
||||
})
|
||||
|
||||
let onTokenAccepted
|
||||
beforeEach(function () {
|
||||
onTokenAccepted = sinon.stub()
|
||||
setRoutes({
|
||||
app: harness.app,
|
||||
server: camp,
|
||||
authHelper: oauthHelper,
|
||||
onTokenAccepted,
|
||||
})
|
||||
|
||||
await harness.start()
|
||||
})
|
||||
|
||||
afterEach(async function () {
|
||||
await harness.stop()
|
||||
})
|
||||
|
||||
it('should start the OAuth process', async function () {
|
||||
const res = await harness.post('/github-auth', { followRedirect: false })
|
||||
const res = await got(`${baseUrl}/github-auth`, { followRedirect: false })
|
||||
|
||||
expect(res.statusCode).to.equal(302)
|
||||
|
||||
@@ -49,8 +61,8 @@ describe('Github token acceptor', function () {
|
||||
describe('Finishing the OAuth process', function () {
|
||||
context('no code is provided', function () {
|
||||
it('should return an error', async function () {
|
||||
const { body } = await harness.post('/github-auth/done')
|
||||
expect(body).to.equal(
|
||||
const res = await got(`${baseUrl}/github-auth/done`)
|
||||
expect(res.body).to.equal(
|
||||
'GitHub OAuth authentication failed to provide a code.'
|
||||
)
|
||||
})
|
||||
@@ -99,7 +111,9 @@ describe('Github token acceptor', function () {
|
||||
const form = new FormData()
|
||||
form.append('code', fakeCode)
|
||||
|
||||
const res = await harness.post('/github-auth/done', { body: form })
|
||||
const res = await got.post(`${baseUrl}/github-auth/done`, {
|
||||
body: form,
|
||||
})
|
||||
expect(res.body).to.startWith(
|
||||
'<p>Shields.io has received your app-specific GitHub user token.'
|
||||
)
|
||||
|
||||
@@ -2,17 +2,7 @@ import Joi from 'joi'
|
||||
import { renderContributorBadge } from '../contributor-count.js'
|
||||
import { ConditionalGithubAuthV3Service } from './github-auth-service.js'
|
||||
import { fetchJsonFromRepo } from './github-common-fetch.js'
|
||||
import { documentation as commonDocumentation } from './github-helpers.js'
|
||||
|
||||
const documentation = `
|
||||
<p>
|
||||
The All Contributors service allows you to recognize all your project
|
||||
contributors, including those that don't push code. See
|
||||
<a href="https://allcontributors.org">https://allcontributors.org</a>
|
||||
for more information.
|
||||
</p>
|
||||
${commonDocumentation}
|
||||
`
|
||||
import { documentation } from './github-helpers.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
contributors: Joi.array().required(),
|
||||
@@ -27,7 +17,7 @@ export default class GithubAllContributorsService extends ConditionalGithubAuthV
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'GitHub contributors (via allcontributors.org)',
|
||||
title: 'Github All Contributors',
|
||||
namedParams: {
|
||||
repo: 'all-contributors',
|
||||
user: 'all-contributors',
|
||||
|
||||
@@ -77,7 +77,8 @@ class GithubApiProvider {
|
||||
}
|
||||
|
||||
getV4RateLimitFromBody(body) {
|
||||
const b = Joi.attempt(body, bodySchema)
|
||||
const parsedBody = JSON.parse(body)
|
||||
const b = Joi.attempt(parsedBody, bodySchema)
|
||||
return {
|
||||
rateLimit: b.data.rateLimit.limit,
|
||||
totalUsesRemaining: b.data.rateLimit.remaining,
|
||||
@@ -89,17 +90,8 @@ class GithubApiProvider {
|
||||
let rateLimit, totalUsesRemaining, nextReset
|
||||
if (url.startsWith('/graphql')) {
|
||||
try {
|
||||
const parsedBody = JSON.parse(res.body)
|
||||
|
||||
if ('message' in parsedBody && !('data' in parsedBody)) {
|
||||
if (parsedBody.message === 'Sorry. Your account was suspended.') {
|
||||
this.invalidateToken(token)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
;({ rateLimit, totalUsesRemaining, nextReset } =
|
||||
this.getV4RateLimitFromBody(parsedBody))
|
||||
this.getV4RateLimitFromBody(res.body))
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`Could not extract rate limit info from response body ${res.body}`
|
||||
|
||||
@@ -126,35 +126,14 @@ describe('Github API provider', function () {
|
||||
})
|
||||
})
|
||||
|
||||
context('unauthorized API responses', function () {
|
||||
it('should invoke the callback and update the token with the expected values (unauthorized, v3)', async function () {
|
||||
context('an unauthorized response', function () {
|
||||
it('should invoke the callback and update the token with the expected values', async function () {
|
||||
const mockResponse = { res: { statusCode: 401, headers: {} } }
|
||||
const mockRequest = sinon.stub().resolves(mockResponse)
|
||||
await provider.fetch(mockRequest, '/foo', {})
|
||||
expect(mockStandardToken.invalidate).to.have.been.calledOnce
|
||||
expect(mockStandardToken.update).not.to.have.been.called
|
||||
})
|
||||
|
||||
it('should invoke the callback and update the token with the expected values (unauthorized, v4)', async function () {
|
||||
const mockResponse = { res: { statusCode: 401, body: {} } }
|
||||
const mockRequest = sinon.stub().resolves(mockResponse)
|
||||
await provider.fetch(mockRequest, '/graphql', {})
|
||||
expect(mockGraphqlToken.invalidate).to.have.been.calledOnce
|
||||
expect(mockGraphqlToken.update).not.to.have.been.called
|
||||
})
|
||||
|
||||
it('should invoke the callback and update the token with the expected values (suspended, v4)', async function () {
|
||||
const mockResponse = {
|
||||
res: {
|
||||
statusCode: 200,
|
||||
body: '{ "message": "Sorry. Your account was suspended." }',
|
||||
},
|
||||
}
|
||||
const mockRequest = sinon.stub().resolves(mockResponse)
|
||||
await provider.fetch(mockRequest, '/graphql', {})
|
||||
expect(mockGraphqlToken.invalidate).to.have.been.calledOnce
|
||||
expect(mockGraphqlToken.update).not.to.have.been.called
|
||||
})
|
||||
})
|
||||
|
||||
context('a connection error', function () {
|
||||
|
||||
@@ -50,7 +50,7 @@ class GithubConstellation {
|
||||
}
|
||||
}
|
||||
|
||||
async initialize(app) {
|
||||
async initialize(server) {
|
||||
if (!this.apiProvider.withPooling) {
|
||||
return
|
||||
}
|
||||
@@ -74,7 +74,7 @@ class GithubConstellation {
|
||||
|
||||
if (this.oauthHelper.isConfigured) {
|
||||
setAcceptorRoutes({
|
||||
app,
|
||||
server,
|
||||
authHelper: this.oauthHelper,
|
||||
onTokenAccepted: tokenString => this.onTokenAdded(tokenString),
|
||||
})
|
||||
|
||||
@@ -58,7 +58,7 @@ export default class GithubDeployments extends GithubAuthV4Service {
|
||||
environment: 'shields-staging',
|
||||
},
|
||||
staticPreview: this.render({
|
||||
state: 'SUCCESS',
|
||||
state: 'success',
|
||||
}),
|
||||
documentation,
|
||||
},
|
||||
|
||||
@@ -307,9 +307,7 @@ export default class GithubIssues extends GithubAuthV4Service {
|
||||
|
||||
return {
|
||||
label: `${labelPrefix}${labelText}${labelSuffix}`,
|
||||
message: `${metric(issueCount)}${
|
||||
messageSuffix ? ' ' : ''
|
||||
}${messageSuffix}`,
|
||||
message: `${metric(issueCount)} ${messageSuffix}`,
|
||||
color: issueCount > 0 ? 'yellow' : 'brightgreen',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { metric } from '../text-formatters.js'
|
||||
import { BaseGithubLanguage } from './github-languages-base.js'
|
||||
import { documentation } from './github-helpers.js'
|
||||
|
||||
@@ -21,7 +20,7 @@ export default class GithubLanguageCount extends BaseGithubLanguage {
|
||||
|
||||
static render({ count }) {
|
||||
return {
|
||||
message: metric(count),
|
||||
message: count,
|
||||
color: 'blue',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ export default class GithubMilestoneDetail extends GithubAuthV3Service {
|
||||
}
|
||||
|
||||
return {
|
||||
label: `${milestone.title}${label ? ' ' : ''}${label}`,
|
||||
label: `${milestone.title} ${label}`,
|
||||
message: metric(milestoneMetric),
|
||||
color,
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export default class GithubMilestone extends GithubAuthV3Service {
|
||||
static render({ user, repo, variant, milestones }) {
|
||||
const milestoneLength = milestones.length
|
||||
let color
|
||||
let qualifier = ''
|
||||
let label = ''
|
||||
|
||||
switch (variant) {
|
||||
case 'all':
|
||||
@@ -51,16 +51,16 @@ export default class GithubMilestone extends GithubAuthV3Service {
|
||||
break
|
||||
case 'open':
|
||||
color = 'red'
|
||||
qualifier = 'active'
|
||||
label = 'active'
|
||||
break
|
||||
case 'closed':
|
||||
color = 'green'
|
||||
qualifier = 'completed'
|
||||
label = 'completed'
|
||||
break
|
||||
}
|
||||
|
||||
return {
|
||||
label: `${qualifier}${qualifier ? ' ' : ''}milestones`,
|
||||
label: `${label} milestones`,
|
||||
message: metric(milestoneLength),
|
||||
color,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { pep440VersionColor } from '../color-formatters.js'
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
import { isLockfile, getDependencyVersion } from '../pipenv-helpers.js'
|
||||
import { addv } from '../text-formatters.js'
|
||||
@@ -81,7 +80,6 @@ class GithubPipenvLockedPythonVersion extends ConditionalGithubAuthV3Service {
|
||||
version,
|
||||
tag: branch,
|
||||
defaultLabel: 'python',
|
||||
versionFormatter: pep440VersionColor,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -149,7 +147,7 @@ class GithubPipenvLockedDependencyVersion extends ConditionalGithubAuthV3Service
|
||||
return {
|
||||
label: dependency,
|
||||
message: version ? addv(version) : ref,
|
||||
color: version ? pep440VersionColor(version) : 'blue',
|
||||
color: 'blue',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,8 +82,10 @@ t.create('Locked version of unknown dependency')
|
||||
})
|
||||
|
||||
t.create('Locked version of VCS dependency')
|
||||
.get('/locked/dependency-version/GSS-Cogs/databaker-docker/databaker.json')
|
||||
.get(
|
||||
'/locked/dependency-version/DemocracyClub/aggregator-api/dc-base-theme.json'
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'databaker',
|
||||
label: 'dc-base-theme',
|
||||
message: isShortSha,
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Joi from 'joi'
|
||||
import { renderDownloadsBadge } from '../downloads.js'
|
||||
import { maybePluralize } from '../text-formatters.js'
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
import { addv, maybePluralize } from '../text-formatters.js'
|
||||
import { version as versionColor } from '../color-formatters.js'
|
||||
import { BaseJsonService } from '../index.js'
|
||||
|
||||
const hexSchema = Joi.object({
|
||||
@@ -14,8 +14,7 @@ const hexSchema = Joi.object({
|
||||
meta: Joi.object({
|
||||
licenses: Joi.array().required(),
|
||||
}).required(),
|
||||
latest_stable_version: Joi.string(),
|
||||
latest_version: Joi.string().required(),
|
||||
latest_stable_version: Joi.string().required(),
|
||||
}).required()
|
||||
|
||||
class BaseHexPmService extends BaseJsonService {
|
||||
@@ -85,14 +84,12 @@ class HexPmVersion extends BaseHexPmService {
|
||||
]
|
||||
|
||||
static render({ version }) {
|
||||
return renderVersionBadge({ version })
|
||||
return { message: addv(version), color: versionColor(version) }
|
||||
}
|
||||
|
||||
async handle({ packageName }) {
|
||||
const json = await this.fetch({ packageName })
|
||||
return this.constructor.render({
|
||||
version: json.latest_stable_version || json.latest_version,
|
||||
})
|
||||
return this.constructor.render({ version: json.latest_stable_version })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import Joi from 'joi'
|
||||
import { ServiceTester } from '../tester.js'
|
||||
import {
|
||||
isMetric,
|
||||
isMetricOverTimePeriod,
|
||||
isVPlusDottedVersionNClausesWithOptionalSuffix,
|
||||
} from '../test-validators.js'
|
||||
import { isMetric, isMetricOverTimePeriod } from '../test-validators.js'
|
||||
|
||||
const isHexpmVersion = Joi.string().regex(/^v\d+.\d+.?\d?$/)
|
||||
|
||||
export const t = new ServiceTester({ id: 'hexpm', title: 'Hex.pm' })
|
||||
|
||||
@@ -24,7 +22,6 @@ t.create('downloads (zero for period)')
|
||||
.reply(200, {
|
||||
downloads: { all: 100 }, // there is no 'day' key here
|
||||
latest_stable_version: '1.0',
|
||||
latest_version: '1.0',
|
||||
meta: { licenses: ['MIT'] },
|
||||
})
|
||||
)
|
||||
@@ -38,26 +35,9 @@ t.create('downloads (not found)')
|
||||
.get('/dt/this-package-does-not-exist.json')
|
||||
.expectBadge({ label: 'downloads', message: 'not found' })
|
||||
|
||||
t.create('version').get('/v/cowboy.json').expectBadge({
|
||||
label: 'hex',
|
||||
message: isVPlusDottedVersionNClausesWithOptionalSuffix,
|
||||
})
|
||||
|
||||
t.create('version (no stable version)')
|
||||
.get('/v/prima_opentelemetry_ex.json')
|
||||
.intercept(nock =>
|
||||
nock('https://hex.pm/')
|
||||
.get('/api/packages/prima_opentelemetry_ex')
|
||||
.reply(200, {
|
||||
downloads: { all: 100 },
|
||||
latest_version: '1.0.0-rc.3',
|
||||
meta: { licenses: ['MIT'] },
|
||||
})
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'hex',
|
||||
message: isVPlusDottedVersionNClausesWithOptionalSuffix,
|
||||
})
|
||||
t.create('version')
|
||||
.get('/v/cowboy.json')
|
||||
.expectBadge({ label: 'hex', message: isHexpmVersion })
|
||||
|
||||
t.create('version (not found)')
|
||||
.get('/v/this-package-does-not-exist.json')
|
||||
@@ -77,7 +57,6 @@ t.create('license (multiple licenses)')
|
||||
.reply(200, {
|
||||
downloads: { all: 100 },
|
||||
latest_stable_version: '1.0',
|
||||
latest_version: '1.0',
|
||||
meta: { licenses: ['GPLv2', 'MIT'] },
|
||||
})
|
||||
)
|
||||
@@ -95,7 +74,6 @@ t.create('license (no license)')
|
||||
.reply(200, {
|
||||
downloads: { all: 100 },
|
||||
latest_stable_version: '1.0',
|
||||
latest_version: '1.0',
|
||||
meta: { licenses: [] },
|
||||
})
|
||||
)
|
||||
|
||||
@@ -81,8 +81,6 @@ class LibrariesIoProjectDependencies extends LibrariesIoBase {
|
||||
},
|
||||
]
|
||||
|
||||
static _cacheLength = 900
|
||||
|
||||
async handle({ platform, scope, packageName, version = 'latest' }) {
|
||||
const url = `/${encodeURIComponent(platform)}/${
|
||||
scope ? encodeURIComponent(`${scope}/`) : ''
|
||||
@@ -118,8 +116,6 @@ class LibrariesIoRepoDependencies extends LibrariesIoBase {
|
||||
},
|
||||
]
|
||||
|
||||
static _cacheLength = 900
|
||||
|
||||
async handle({ user, repo }) {
|
||||
const url = `/github/${encodeURIComponent(user)}/${encodeURIComponent(
|
||||
repo
|
||||
|
||||
@@ -32,8 +32,6 @@ export default class LibrariesIoDependentRepos extends LibrariesIoBase {
|
||||
},
|
||||
]
|
||||
|
||||
static _cacheLength = 900
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'dependent repos',
|
||||
}
|
||||
|
||||
@@ -32,8 +32,6 @@ export default class LibrariesIoDependents extends LibrariesIoBase {
|
||||
},
|
||||
]
|
||||
|
||||
static _cacheLength = 900
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'dependents',
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Joi from 'joi'
|
||||
import { nonNegativeInteger } from '../validators.js'
|
||||
import { BaseJsonService } from '../index.js'
|
||||
import { metric } from '../text-formatters.js'
|
||||
|
||||
// https://developer.opencollective.com/#/api/collectives?id=get-info
|
||||
const collectiveDetailsSchema = Joi.object().keys({
|
||||
@@ -31,11 +30,12 @@ export default class OpencollectiveBase extends BaseJsonService {
|
||||
}
|
||||
|
||||
static render(backersCount, label) {
|
||||
return {
|
||||
label,
|
||||
message: metric(backersCount),
|
||||
const badge = {
|
||||
message: backersCount,
|
||||
color: backersCount > 0 ? 'brightgreen' : 'lightgrey',
|
||||
}
|
||||
if (label) badge.label = label
|
||||
return badge
|
||||
}
|
||||
|
||||
async fetchCollectiveInfo(collective) {
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import Joi from 'joi'
|
||||
import { BaseJsonService } from '../index.js'
|
||||
import { colorScale } from '../color-formatters.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
score: Joi.number().min(0).required(),
|
||||
}).required()
|
||||
|
||||
const ossfScorecardColorScale = colorScale(
|
||||
[2, 5, 8, 10],
|
||||
['red', 'yellow', 'yellowgreen', 'green', 'brightgreen']
|
||||
)
|
||||
|
||||
export default class OSSFScorecard extends BaseJsonService {
|
||||
static category = 'analysis'
|
||||
|
||||
static route = { base: 'ossf-scorecard', pattern: ':host/:orgName/:repoName' }
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'OSSF-Scorecard Score',
|
||||
namedParams: {
|
||||
host: 'github.com',
|
||||
orgName: 'rohankh532',
|
||||
repoName: 'org-workflow-add',
|
||||
},
|
||||
staticPreview: this.render({ score: '7.5' }),
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = { label: 'score' }
|
||||
|
||||
static render({ score }) {
|
||||
return {
|
||||
message: score,
|
||||
color: ossfScorecardColorScale(score),
|
||||
}
|
||||
}
|
||||
|
||||
async fetch({ host, orgName, repoName }) {
|
||||
return this._requestJson({
|
||||
schema,
|
||||
url: `https://api.securityscorecards.dev/projects/${host}/${orgName}/${repoName}`,
|
||||
errorMessages: {
|
||||
404: 'invalid repo path',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async handle({ host, orgName, repoName }) {
|
||||
const { score } = await this.fetch({ host, orgName, repoName })
|
||||
|
||||
return this.constructor.render({ score })
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import Joi from 'joi'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('score valid')
|
||||
.get('/github.com/rohankh532/org-workflow-add.json')
|
||||
.expectBadge({
|
||||
label: 'score',
|
||||
message: Joi.number().min(0),
|
||||
color: Joi.string().allow(
|
||||
'red',
|
||||
'yellow',
|
||||
'yellowgreen',
|
||||
'green',
|
||||
'brightgreen'
|
||||
),
|
||||
})
|
||||
|
||||
t.create('score ivalid')
|
||||
.get('/github.com/invalid-user/invalid-repo.json')
|
||||
.expectBadge({
|
||||
label: 'score',
|
||||
message: 'invalid repo path',
|
||||
color: 'red',
|
||||
})
|
||||
@@ -1,18 +1,18 @@
|
||||
import Joi from 'joi'
|
||||
import { BaseJsonService, NotFound } from '../index.js'
|
||||
import { isStable, latest } from '../php-version.js'
|
||||
import { BaseJsonService } from '../index.js'
|
||||
|
||||
const packageSchema = Joi.array().items(
|
||||
Joi.object({
|
||||
version: Joi.string().required(),
|
||||
require: Joi.alternatives(
|
||||
Joi.object({
|
||||
const packageSchema = Joi.object()
|
||||
.pattern(
|
||||
/^/,
|
||||
Joi.object({
|
||||
'default-branch': Joi.bool(),
|
||||
version: Joi.string(),
|
||||
require: Joi.object({
|
||||
php: Joi.string(),
|
||||
}).required(),
|
||||
Joi.string().valid('__unset')
|
||||
),
|
||||
})
|
||||
)
|
||||
}),
|
||||
}).required()
|
||||
)
|
||||
.required()
|
||||
|
||||
const allVersionsSchema = Joi.object({
|
||||
packages: Joi.object().pattern(/^/, packageSchema).required(),
|
||||
@@ -36,31 +36,7 @@ class BasePackagistService extends BaseJsonService {
|
||||
* @returns {object} Parsed response
|
||||
*/
|
||||
async fetch({ user, repo, schema, server = 'https://packagist.org' }) {
|
||||
const url = `${server}/p2/${user.toLowerCase()}/${repo.toLowerCase()}.json`
|
||||
|
||||
return this._requestJson({
|
||||
schema,
|
||||
url,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch dev releases method.
|
||||
*
|
||||
* This method utilize composer metadata API which
|
||||
* "... is the preferred way to access the data as it is always up to date,
|
||||
* and dumped to static files so it is very efficient on our end." (comment from official documentation).
|
||||
* For more information please refer to https://packagist.org/apidoc#get-package-data.
|
||||
*
|
||||
* @param {object} attrs Refer to individual attrs
|
||||
* @param {string} attrs.user package user
|
||||
* @param {string} attrs.repo package repository
|
||||
* @param {Joi} attrs.schema Joi schema to validate the response transformed to JSON
|
||||
* @param {string} attrs.server URL for the packagist registry server (Optional)
|
||||
* @returns {object} Parsed response
|
||||
*/
|
||||
async fetchDev({ user, repo, schema, server = 'https://packagist.org' }) {
|
||||
const url = `${server}/p2/${user.toLowerCase()}/${repo.toLowerCase()}~dev.json`
|
||||
const url = `${server}/p/${user.toLowerCase()}/${repo.toLowerCase()}.json`
|
||||
|
||||
return this._requestJson({
|
||||
schema,
|
||||
@@ -97,74 +73,16 @@ class BasePackagistService extends BaseJsonService {
|
||||
})
|
||||
}
|
||||
|
||||
getDefaultBranch(json, user, repo) {
|
||||
const packageName = this.getPackageName(user, repo)
|
||||
return Object.values(json.packages[packageName]).find(
|
||||
b => b['default-branch'] === true
|
||||
)
|
||||
}
|
||||
|
||||
getPackageName(user, repo) {
|
||||
return `${user.toLowerCase()}/${repo.toLowerCase()}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the array of minified versions of the given packageName,
|
||||
* expand them back to their original format then return.
|
||||
*
|
||||
* @param {object} json The response of Packagist v2 API.
|
||||
* @param {string} packageName The package name.
|
||||
*
|
||||
* @returns {object[]} An array of version metadata object.
|
||||
*
|
||||
* @see https://github.com/composer/metadata-minifier/blob/c549d23829536f0d0e984aaabbf02af91f443207/src/MetadataMinifier.php#L16-L46
|
||||
*/
|
||||
static expandPackageVersions(json, packageName) {
|
||||
const versions = json.packages[packageName]
|
||||
const expanded = []
|
||||
let expandedVersion = null
|
||||
|
||||
for (const i in versions) {
|
||||
const versionData = versions[i]
|
||||
if (!expandedVersion) {
|
||||
expandedVersion = { ...versionData }
|
||||
expanded.push(expandedVersion)
|
||||
continue
|
||||
}
|
||||
|
||||
expandedVersion = { ...expandedVersion, ...versionData }
|
||||
for (const key in expandedVersion) {
|
||||
if (expandedVersion[key] === '__unset') {
|
||||
delete expandedVersion[key]
|
||||
}
|
||||
}
|
||||
expanded.push(expandedVersion)
|
||||
}
|
||||
|
||||
return expanded
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the object representation of the latest release.
|
||||
*
|
||||
* @param {object[]} versions An array of object representing a version.
|
||||
* @param {boolean} includePrereleases Includes pre-release semver for the search.
|
||||
*
|
||||
* @returns {object} The object of the latest version.
|
||||
* @throws {NotFound} Thrown if there is no item from the version array.
|
||||
*/
|
||||
findLatestRelease(versions, includePrereleases = false) {
|
||||
// Find the latest version string, if not found, throw NotFound.
|
||||
const versionStrings = versions
|
||||
.filter(
|
||||
version =>
|
||||
typeof version.version === 'string' ||
|
||||
version.version instanceof String
|
||||
)
|
||||
.map(version => version.version)
|
||||
if (versionStrings.length < 1) {
|
||||
throw new NotFound({ prettyMessage: 'no released version found' })
|
||||
}
|
||||
|
||||
let release = latest(versionStrings)
|
||||
if (!includePrereleases) {
|
||||
release = latest(versionStrings.filter(isStable)) || release
|
||||
}
|
||||
return versions.filter(version => version.version === release)[0]
|
||||
}
|
||||
}
|
||||
|
||||
const customServerDocumentationFragment = `
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import { strict as assert } from 'assert'
|
||||
import { describe, it } from 'mocha'
|
||||
import { BasePackagistService } from './packagist-base.js'
|
||||
|
||||
// @reference: https://github.com/composer/metadata-minifier/blob/c549d23829536f0d0e984aaabbf02af91f443207/tests/MetadataMinifierTest.php#L36-L40
|
||||
const minifiedSample = [
|
||||
{
|
||||
name: 'foo/bar',
|
||||
version: '2.0.0',
|
||||
version_normalized: '2.0.0.0',
|
||||
type: 'library',
|
||||
scripts: {
|
||||
foo: 'bar',
|
||||
},
|
||||
license: ['MIT'],
|
||||
},
|
||||
{
|
||||
version: '1.2.0',
|
||||
version_normalized: '1.2.0.0',
|
||||
license: ['GPL'],
|
||||
homepage: 'https://example.org',
|
||||
scripts: '__unset',
|
||||
},
|
||||
{
|
||||
version: '1.0.0',
|
||||
version_normalized: '1.0.0.0',
|
||||
homepage: '__unset',
|
||||
},
|
||||
]
|
||||
|
||||
const expandedSample = [
|
||||
{
|
||||
name: 'foo/bar',
|
||||
version: '2.0.0',
|
||||
version_normalized: '2.0.0.0',
|
||||
type: 'library',
|
||||
scripts: {
|
||||
foo: 'bar',
|
||||
},
|
||||
license: ['MIT'],
|
||||
},
|
||||
{
|
||||
name: 'foo/bar',
|
||||
version: '1.2.0',
|
||||
version_normalized: '1.2.0.0',
|
||||
type: 'library',
|
||||
license: ['GPL'],
|
||||
homepage: 'https://example.org',
|
||||
},
|
||||
{
|
||||
name: 'foo/bar',
|
||||
version: '1.0.0',
|
||||
version_normalized: '1.0.0.0',
|
||||
type: 'library',
|
||||
license: ['GPL'],
|
||||
},
|
||||
]
|
||||
|
||||
describe('BasePackagistService', function () {
|
||||
describe('expandPackageVersions', function () {
|
||||
const expanded = BasePackagistService.expandPackageVersions(
|
||||
{
|
||||
packages: {
|
||||
'foobar/foobar': minifiedSample,
|
||||
},
|
||||
},
|
||||
'foobar/foobar'
|
||||
)
|
||||
it('should expand the minified package array to match the expanded sample', function () {
|
||||
assert.deepStrictEqual(
|
||||
expanded,
|
||||
expandedSample,
|
||||
'The expanded array should match the sample'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -8,11 +8,12 @@ import {
|
||||
customServerDocumentationFragment,
|
||||
} from './packagist-base.js'
|
||||
|
||||
const packageSchema = Joi.array()
|
||||
.items(
|
||||
const packageSchema = Joi.object()
|
||||
.pattern(
|
||||
/^/,
|
||||
Joi.object({
|
||||
version: Joi.string(),
|
||||
license: Joi.array(),
|
||||
'default-branch': Joi.bool(),
|
||||
license: Joi.array().required(),
|
||||
}).required()
|
||||
)
|
||||
.required()
|
||||
@@ -56,27 +57,17 @@ export default class PackagistLicense extends BasePackagistService {
|
||||
}
|
||||
|
||||
transform({ json, user, repo }) {
|
||||
const packageName = this.getPackageName(user, repo)
|
||||
|
||||
const versions = BasePackagistService.expandPackageVersions(
|
||||
json,
|
||||
packageName
|
||||
)
|
||||
|
||||
const version = this.findLatestRelease(versions)
|
||||
const license = version.license
|
||||
if (!license) {
|
||||
throw new NotFound({ prettyMessage: 'license not found' })
|
||||
const branch = this.getDefaultBranch(json, user, repo)
|
||||
if (!branch) {
|
||||
throw new NotFound({ prettyMessage: 'default branch not found' })
|
||||
}
|
||||
|
||||
const { license } = branch
|
||||
return { license }
|
||||
}
|
||||
|
||||
async handle({ user, repo }, { server }) {
|
||||
const json = await this.fetch({ user, repo, schema, server })
|
||||
|
||||
const { license } = this.transform({ json, user, repo })
|
||||
|
||||
return renderLicenseBadge({ license })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,101 +3,17 @@ import { NotFound } from '../index.js'
|
||||
import PackagistLicense from './packagist-license.service.js'
|
||||
|
||||
describe('PackagistLicense', function () {
|
||||
it('should return the license of the most recent release', function () {
|
||||
it('should throw NotFound when default branch is missing', function () {
|
||||
const json = {
|
||||
packages: {
|
||||
'frodo/the-one-package': [
|
||||
{
|
||||
version: '1.2.4',
|
||||
license: 'MIT-latest',
|
||||
},
|
||||
{
|
||||
version: '1.2.3',
|
||||
license: 'MIT',
|
||||
},
|
||||
],
|
||||
'frodo/the-one-package': {
|
||||
'1.0.x-dev': { license: 'MIT' },
|
||||
'1.1.x-dev': { license: 'MIT' },
|
||||
'2.0.x-dev': { license: 'MIT' },
|
||||
'2.1.x-dev': { license: 'MIT' },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expect(
|
||||
PackagistLicense.prototype.transform({
|
||||
json,
|
||||
user: 'frodo',
|
||||
repo: 'the-one-package',
|
||||
})
|
||||
)
|
||||
.to.have.property('license')
|
||||
.that.equals('MIT-latest')
|
||||
})
|
||||
|
||||
it('should return the license of the most recent stable release', function () {
|
||||
const json = {
|
||||
packages: {
|
||||
'frodo/the-one-package': [
|
||||
{
|
||||
version: '1.2.4-RC1', // Pre-release
|
||||
license: 'MIT-latest',
|
||||
},
|
||||
{
|
||||
version: '1.2.3', // Stable release
|
||||
license: 'MIT',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
expect(
|
||||
PackagistLicense.prototype.transform({
|
||||
json,
|
||||
user: 'frodo',
|
||||
repo: 'the-one-package',
|
||||
})
|
||||
)
|
||||
.to.have.property('license')
|
||||
.that.equals('MIT')
|
||||
})
|
||||
|
||||
it('should return the license of the most recent pre-release if no stable releases', function () {
|
||||
const json = {
|
||||
packages: {
|
||||
'frodo/the-one-package': [
|
||||
{
|
||||
version: '1.2.4-RC2',
|
||||
license: 'MIT-latest',
|
||||
},
|
||||
{
|
||||
version: '1.2.4-RC1',
|
||||
license: 'MIT',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
expect(
|
||||
PackagistLicense.prototype.transform({
|
||||
json,
|
||||
user: 'frodo',
|
||||
repo: 'the-one-package',
|
||||
})
|
||||
)
|
||||
.to.have.property('license')
|
||||
.that.equals('MIT-latest')
|
||||
})
|
||||
|
||||
it('should throw NotFound when license key not in response', function () {
|
||||
const json = {
|
||||
packages: {
|
||||
'frodo/the-one-package': [
|
||||
{
|
||||
version: '1.2.4',
|
||||
},
|
||||
{
|
||||
version: '1.2.3',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
expect(() =>
|
||||
PackagistLicense.prototype.transform({
|
||||
json,
|
||||
@@ -106,6 +22,31 @@ describe('PackagistLicense', function () {
|
||||
})
|
||||
)
|
||||
.to.throw(NotFound)
|
||||
.with.property('prettyMessage', 'license not found')
|
||||
.with.property('prettyMessage', 'default branch not found')
|
||||
})
|
||||
|
||||
it('should return default branch when default branch is found', function () {
|
||||
const json = {
|
||||
packages: {
|
||||
'frodo/the-one-package': {
|
||||
'1.0.x-dev': { license: 'MIT' },
|
||||
'1.1.x-dev': { license: 'MIT' },
|
||||
'2.0.x-dev': {
|
||||
license: 'MIT-default-branch',
|
||||
'default-branch': true,
|
||||
},
|
||||
'2.1.x-dev': { license: 'MIT' },
|
||||
},
|
||||
},
|
||||
}
|
||||
expect(
|
||||
PackagistLicense.prototype.transform({
|
||||
json,
|
||||
user: 'frodo',
|
||||
repo: 'the-one-package',
|
||||
})
|
||||
)
|
||||
.to.have.property('license')
|
||||
.that.equals('MIT-default-branch')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -66,58 +66,11 @@ export default class PackagistPhpVersion extends BasePackagistService {
|
||||
}
|
||||
}
|
||||
|
||||
findVersionIndex(json, version) {
|
||||
return json.findIndex(v => v.version === version)
|
||||
}
|
||||
|
||||
async findSpecifiedVersion(json, user, repo, version, server) {
|
||||
let release
|
||||
|
||||
if ((release = json[this.findVersionIndex(json, version)])) {
|
||||
return release
|
||||
} else {
|
||||
try {
|
||||
const allData = await this.fetchDev({
|
||||
user,
|
||||
repo,
|
||||
schema: allVersionsSchema,
|
||||
server,
|
||||
})
|
||||
|
||||
const versions = BasePackagistService.expandPackageVersions(
|
||||
allData,
|
||||
this.getPackageName(user, repo)
|
||||
)
|
||||
|
||||
return versions[this.findVersionIndex(versions, version)]
|
||||
} catch (e) {
|
||||
return release
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getPhpVersion({ json, user, repo, version = '', server }) {
|
||||
let packageVersion
|
||||
const versions = BasePackagistService.expandPackageVersions(
|
||||
json,
|
||||
this.getPackageName(user, repo)
|
||||
)
|
||||
|
||||
if (version === '') {
|
||||
packageVersion = this.findLatestRelease(versions)
|
||||
} else {
|
||||
try {
|
||||
packageVersion = await this.findSpecifiedVersion(
|
||||
versions,
|
||||
user,
|
||||
repo,
|
||||
version,
|
||||
server
|
||||
)
|
||||
} catch (e) {
|
||||
packageVersion = null
|
||||
}
|
||||
}
|
||||
transform({ json, user, repo, version = '' }) {
|
||||
const packageVersion =
|
||||
version === ''
|
||||
? this.getDefaultBranch(json, user, repo)
|
||||
: json.packages[this.getPackageName(user, repo)][version]
|
||||
|
||||
if (!packageVersion) {
|
||||
throw new NotFound({ prettyMessage: 'invalid version' })
|
||||
@@ -137,12 +90,11 @@ export default class PackagistPhpVersion extends BasePackagistService {
|
||||
schema: allVersionsSchema,
|
||||
server,
|
||||
})
|
||||
const { phpVersion } = await this.getPhpVersion({
|
||||
const { phpVersion } = this.transform({
|
||||
json: allData,
|
||||
user,
|
||||
repo,
|
||||
version,
|
||||
server,
|
||||
})
|
||||
return this.constructor.render({ php: phpVersion })
|
||||
}
|
||||
|
||||
@@ -1,115 +1,99 @@
|
||||
import { expect } from 'chai'
|
||||
import { NotFound } from '../index.js'
|
||||
import PackagistPhpVersion from './packagist-php-version.service.js'
|
||||
|
||||
describe('PackagistPhpVersion', function () {
|
||||
const json = {
|
||||
packages: {
|
||||
'frodo/the-one-package': [
|
||||
{
|
||||
version: '3.0.0',
|
||||
require: { php: '^7.4 || 8' },
|
||||
},
|
||||
{
|
||||
version: '2.0.0',
|
||||
require: { php: '^7.2' },
|
||||
},
|
||||
{
|
||||
version: '1.0.0',
|
||||
require: { php: '^5.6 || ^7' },
|
||||
},
|
||||
],
|
||||
'frodo/the-one-package': {
|
||||
'1.0.0': { require: { php: '^5.6 || ^7' } },
|
||||
'2.0.0': { require: { php: '^7.2' } },
|
||||
'3.0.0': { require: { php: '^7.4 || 8' } },
|
||||
'dev-main': { require: { php: '^8' }, 'default-branch': true },
|
||||
},
|
||||
'samwise/gardening': {
|
||||
'1.0.x-dev': {},
|
||||
'2.0.x-dev': {},
|
||||
},
|
||||
'pippin/mischief': {
|
||||
'1.0.0': {},
|
||||
'dev-main': { require: {}, 'default-branch': true },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
it('should throw NotFound when package version is missing', async function () {
|
||||
await expect(
|
||||
PackagistPhpVersion.prototype.getPhpVersion({
|
||||
it('should throw NotFound when package version is missing', function () {
|
||||
expect(() =>
|
||||
PackagistPhpVersion.prototype.transform({
|
||||
json,
|
||||
user: 'frodo',
|
||||
repo: 'the-one-package',
|
||||
version: '4.0.0',
|
||||
})
|
||||
).to.be.rejectedWith('invalid version')
|
||||
)
|
||||
.to.throw(NotFound)
|
||||
.with.property('prettyMessage', 'invalid version')
|
||||
})
|
||||
|
||||
it('should throw NotFound when PHP version not found on package when using default release', async function () {
|
||||
const specJson = {
|
||||
packages: {
|
||||
'frodo/the-one-package': [
|
||||
{
|
||||
version: '3.0.0',
|
||||
},
|
||||
{
|
||||
version: '2.0.0',
|
||||
require: { php: '^7.2' },
|
||||
},
|
||||
{
|
||||
version: '1.0.0',
|
||||
require: { php: '^5.6 || ^7' },
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
await expect(
|
||||
PackagistPhpVersion.prototype.getPhpVersion({
|
||||
json: specJson,
|
||||
user: 'frodo',
|
||||
repo: 'the-one-package',
|
||||
it('should throw NotFound when version not specified and no default branch found', function () {
|
||||
expect(() =>
|
||||
PackagistPhpVersion.prototype.transform({
|
||||
json,
|
||||
user: 'samwise',
|
||||
repo: 'gardening',
|
||||
})
|
||||
).to.be.rejectedWith('version requirement not found')
|
||||
)
|
||||
.to.throw(NotFound)
|
||||
.with.property('prettyMessage', 'invalid version')
|
||||
})
|
||||
|
||||
it('should throw NotFound when PHP version not found on package when using specified release', async function () {
|
||||
const specJson = {
|
||||
packages: {
|
||||
'frodo/the-one-package': [
|
||||
{
|
||||
version: '3.0.0',
|
||||
require: { php: '^7.4 || 8' },
|
||||
},
|
||||
{
|
||||
version: '2.0.0',
|
||||
require: { php: '^7.2' },
|
||||
},
|
||||
{
|
||||
version: '1.0.0',
|
||||
require: '__unset',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
await expect(
|
||||
PackagistPhpVersion.prototype.getPhpVersion({
|
||||
json: specJson,
|
||||
user: 'frodo',
|
||||
repo: 'the-one-package',
|
||||
it('should throw NotFound when PHP version not found on package when using default branch', function () {
|
||||
expect(() =>
|
||||
PackagistPhpVersion.prototype.transform({
|
||||
json,
|
||||
user: 'pippin',
|
||||
repo: 'mischief',
|
||||
})
|
||||
)
|
||||
.to.throw(NotFound)
|
||||
.with.property('prettyMessage', 'version requirement not found')
|
||||
})
|
||||
|
||||
it('should throw NotFound when PHP version not found on package when using specified version', function () {
|
||||
expect(() =>
|
||||
PackagistPhpVersion.prototype.transform({
|
||||
json,
|
||||
user: 'pippin',
|
||||
repo: 'mischief',
|
||||
version: '1.0.0',
|
||||
})
|
||||
).to.be.rejectedWith('version requirement not found')
|
||||
)
|
||||
.to.throw(NotFound)
|
||||
.with.property('prettyMessage', 'version requirement not found')
|
||||
})
|
||||
|
||||
it('should return PHP version for the default release', async function () {
|
||||
it('should return PHP version for the default branch', function () {
|
||||
expect(
|
||||
await PackagistPhpVersion.prototype.getPhpVersion({
|
||||
PackagistPhpVersion.prototype.transform({
|
||||
json,
|
||||
user: 'frodo',
|
||||
repo: 'the-one-package',
|
||||
})
|
||||
)
|
||||
.to.have.property('phpVersion')
|
||||
.that.equals('^8')
|
||||
})
|
||||
|
||||
it('should return PHP version for the specified branch', function () {
|
||||
expect(
|
||||
PackagistPhpVersion.prototype.transform({
|
||||
json,
|
||||
user: 'frodo',
|
||||
repo: 'the-one-package',
|
||||
version: '3.0.0',
|
||||
})
|
||||
)
|
||||
.to.have.property('phpVersion')
|
||||
.that.equals('^7.4 || 8')
|
||||
})
|
||||
|
||||
it('should return PHP version for the specified release', async function () {
|
||||
expect(
|
||||
await PackagistPhpVersion.prototype.getPhpVersion({
|
||||
json,
|
||||
user: 'frodo',
|
||||
repo: 'the-one-package',
|
||||
version: '2.0.0',
|
||||
})
|
||||
)
|
||||
.to.have.property('phpVersion')
|
||||
.that.equals('^7.2')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,8 +6,8 @@ t.create('gets the package version of symfony')
|
||||
.get('/symfony/symfony.json')
|
||||
.expectBadge({ label: 'php', message: isComposerVersion })
|
||||
|
||||
t.create('gets the package version of symfony 5.2.3')
|
||||
.get('/symfony/symfony/v5.2.3.json')
|
||||
t.create('gets the package version of symfony 2.8')
|
||||
.get('/symfony/symfony/v2.8.0.json')
|
||||
.expectBadge({ label: 'php', message: isComposerVersion })
|
||||
|
||||
t.create('package with no requirements')
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
import Joi from 'joi'
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
import { compare, isStable, latest } from '../php-version.js'
|
||||
import { optionalUrl } from '../validators.js'
|
||||
import { redirector } from '../index.js'
|
||||
import { NotFound, redirector } from '../index.js'
|
||||
import {
|
||||
allVersionsSchema,
|
||||
keywords,
|
||||
BasePackagistService,
|
||||
customServerDocumentationFragment,
|
||||
} from './packagist-base.js'
|
||||
|
||||
const packageSchema = Joi.array().items(
|
||||
Joi.object({
|
||||
version: Joi.string().required(),
|
||||
})
|
||||
)
|
||||
const packageSchema = Joi.object()
|
||||
.pattern(
|
||||
/^/,
|
||||
Joi.object({
|
||||
version: Joi.string(),
|
||||
extra: Joi.object({
|
||||
'branch-alias': Joi.object().pattern(/^/, Joi.string()),
|
||||
}),
|
||||
}).required()
|
||||
)
|
||||
.required()
|
||||
|
||||
const schema = Joi.object({
|
||||
packages: Joi.object().pattern(/^/, packageSchema).required(),
|
||||
@@ -72,9 +80,45 @@ class PackagistVersion extends BasePackagistService {
|
||||
}
|
||||
|
||||
static render({ version }) {
|
||||
if (version === undefined) {
|
||||
throw new NotFound({ prettyMessage: 'no released version found' })
|
||||
}
|
||||
return renderVersionBadge({ version })
|
||||
}
|
||||
|
||||
transform({ includePrereleases, json, user, repo }) {
|
||||
const versionsData = json.packages[this.getPackageName(user, repo)]
|
||||
let versions = Object.keys(versionsData)
|
||||
const aliasesMap = {}
|
||||
versions.forEach(version => {
|
||||
const versionData = versionsData[version]
|
||||
if (
|
||||
versionData.extra &&
|
||||
versionData.extra['branch-alias'] &&
|
||||
versionData.extra['branch-alias'][version]
|
||||
) {
|
||||
// eg, version is 'dev-master', mapped to '2.0.x-dev'.
|
||||
const validVersion = versionData.extra['branch-alias'][version]
|
||||
if (
|
||||
aliasesMap[validVersion] === undefined ||
|
||||
compare(aliasesMap[validVersion], validVersion) < 0
|
||||
) {
|
||||
versions.push(validVersion)
|
||||
aliasesMap[validVersion] = version
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
versions = versions.filter(version => !/^dev-/.test(version))
|
||||
|
||||
if (includePrereleases) {
|
||||
return { version: latest(versions) }
|
||||
} else {
|
||||
const stableVersion = latest(versions.filter(isStable))
|
||||
return { version: stableVersion || latest(versions) }
|
||||
}
|
||||
}
|
||||
|
||||
async handle(
|
||||
{ user, repo },
|
||||
{ include_prereleases: includePrereleases, server }
|
||||
@@ -83,11 +127,10 @@ class PackagistVersion extends BasePackagistService {
|
||||
const json = await this.fetch({
|
||||
user,
|
||||
repo,
|
||||
schema,
|
||||
schema: includePrereleases ? schema : allVersionsSchema,
|
||||
server,
|
||||
})
|
||||
const versions = json.packages[this.getPackageName(user, repo)]
|
||||
const { version } = this.findLatestRelease(versions, includePrereleases)
|
||||
const { version } = this.transform({ includePrereleases, json, user, repo })
|
||||
return this.constructor.render({ version })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import Joi from 'joi'
|
||||
import { BaseJsonService, InvalidResponse } from '../index.js'
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
import { pep440VersionColor } from '../color-formatters.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
releases: Joi.object()
|
||||
.pattern(
|
||||
Joi.string(),
|
||||
Joi.object({
|
||||
prerelease: Joi.boolean().required(),
|
||||
yanked: Joi.boolean().required(),
|
||||
files: Joi.object().required(),
|
||||
})
|
||||
)
|
||||
.required(),
|
||||
}).required()
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
include_prereleases: Joi.equal(''),
|
||||
}).required()
|
||||
|
||||
const keywords = ['python', 'arm', 'raspberry pi']
|
||||
|
||||
export default class PiWheelsVersion extends BaseJsonService {
|
||||
static category = 'version'
|
||||
|
||||
static route = { base: 'piwheels/v', pattern: ':wheel', queryParamSchema }
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'piwheels',
|
||||
namedParams: { wheel: 'numpy' },
|
||||
staticPreview: this.render({ version: '1.22.2' }),
|
||||
keywords,
|
||||
},
|
||||
{
|
||||
title: 'piwheels (including prereleases)',
|
||||
namedParams: { wheel: 'flask' },
|
||||
queryParams: {
|
||||
include_prereleases: null,
|
||||
},
|
||||
staticPreview: this.render({ version: '2.0.0rc2' }),
|
||||
keywords,
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = { label: 'piwheels' }
|
||||
|
||||
static render({ version }) {
|
||||
return renderVersionBadge({ version, versionFormatter: pep440VersionColor })
|
||||
}
|
||||
|
||||
async fetch({ wheel }) {
|
||||
return this._requestJson({
|
||||
schema,
|
||||
url: `https://www.piwheels.org/project/${wheel}/json/`,
|
||||
errorMessages: { 404: 'package not found' },
|
||||
})
|
||||
}
|
||||
|
||||
static transform(releases, includePrereleases) {
|
||||
const allReleases = Object.keys(releases)
|
||||
.reduce(
|
||||
(acc, key) =>
|
||||
acc.concat({
|
||||
version: key,
|
||||
prerelease: releases[key].prerelease,
|
||||
yanked: releases[key].yanked,
|
||||
hasFiles: Object.keys(releases[key].files).length > 0,
|
||||
}),
|
||||
[]
|
||||
)
|
||||
.filter(release => !release.yanked) // exclude any yanked releases
|
||||
.filter(release => release.hasFiles) // exclude any releases with no wheels
|
||||
|
||||
if (allReleases.length === 0) {
|
||||
throw new InvalidResponse({ prettyMessage: 'no versions found' })
|
||||
}
|
||||
|
||||
if (includePrereleases) {
|
||||
return allReleases[0].version
|
||||
}
|
||||
|
||||
const stableReleases = allReleases.filter(release => !release.prerelease)
|
||||
if (stableReleases.length > 0) {
|
||||
return stableReleases[0].version
|
||||
}
|
||||
return allReleases[0].version
|
||||
}
|
||||
|
||||
async handle({ wheel }, queryParams) {
|
||||
const includePrereleases = queryParams.include_prereleases !== undefined
|
||||
const { releases } = await this.fetch({ wheel })
|
||||
const version = this.constructor.transform(releases, includePrereleases)
|
||||
return this.constructor.render({ version })
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { expect } from 'chai'
|
||||
import { test, given } from 'sazerac'
|
||||
import { InvalidResponse } from '../index.js'
|
||||
import PiWheelsVersion from './piwheels-version.service.js'
|
||||
|
||||
describe('PiWheelsVersion', function () {
|
||||
test(PiWheelsVersion.transform, () => {
|
||||
given(
|
||||
{
|
||||
'2.0.0rc1': { prerelease: true, yanked: false, files: { foobar: {} } },
|
||||
'1.9.0': { prerelease: false, yanked: false, files: { foobar: {} } },
|
||||
},
|
||||
false
|
||||
).expect('1.9.0')
|
||||
given(
|
||||
{
|
||||
'2.0.0rc1': { prerelease: true, yanked: false, files: { foobar: {} } },
|
||||
'1.9.0': { prerelease: false, yanked: false, files: { foobar: {} } },
|
||||
},
|
||||
true
|
||||
).expect('2.0.0rc1')
|
||||
given(
|
||||
{
|
||||
'2.0.0': { prerelease: false, yanked: true, files: { foobar: {} } },
|
||||
'1.9.0': { prerelease: false, yanked: false, files: { foobar: {} } },
|
||||
},
|
||||
false
|
||||
).expect('1.9.0')
|
||||
given(
|
||||
{
|
||||
'2.0.0': { prerelease: false, yanked: false, files: {} },
|
||||
'1.9.0': { prerelease: false, yanked: false, files: { foobar: {} } },
|
||||
},
|
||||
false
|
||||
).expect('1.9.0')
|
||||
given(
|
||||
{
|
||||
'2.0.0': { prerelease: false, yanked: false, files: { foobar: {} } },
|
||||
'1.9.0': { prerelease: false, yanked: false, files: { foobar: {} } },
|
||||
},
|
||||
false
|
||||
).expect('2.0.0')
|
||||
given(
|
||||
{
|
||||
'2.0.0rc2': { prerelease: true, yanked: false, files: { foobar: {} } },
|
||||
'2.0.0rc1': { prerelease: true, yanked: false, files: { foobar: {} } },
|
||||
},
|
||||
false
|
||||
).expect('2.0.0rc2')
|
||||
})
|
||||
|
||||
it('throws `no releases` InvalidResponse if no versions', function () {
|
||||
expect(() =>
|
||||
PiWheelsVersion.transform(
|
||||
{
|
||||
'1.0.1': { prerelease: false, yanked: false, files: {} },
|
||||
'1.0.0': { prerelease: false, yanked: true, files: { foobar: {} } },
|
||||
},
|
||||
false
|
||||
)
|
||||
)
|
||||
.to.throw(InvalidResponse)
|
||||
.with.property('prettyMessage', 'no versions found')
|
||||
})
|
||||
})
|
||||
@@ -1,13 +0,0 @@
|
||||
import { isVPlusDottedVersionNClauses } from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('version (valid)').get('/flask.json').expectBadge({
|
||||
label: 'piwheels',
|
||||
message: isVPlusDottedVersionNClauses,
|
||||
})
|
||||
|
||||
t.create('version (does not exist)').get('/doesn-not-exist.json').expectBadge({
|
||||
label: 'piwheels',
|
||||
message: 'package not found',
|
||||
})
|
||||
@@ -1,49 +0,0 @@
|
||||
import Joi from 'joi'
|
||||
import { BaseJsonService } from '../index.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
publisherId: Joi.string().allow(null).required(),
|
||||
}).required()
|
||||
|
||||
export class PubPublisher extends BaseJsonService {
|
||||
static category = 'other'
|
||||
|
||||
static route = {
|
||||
base: 'pub/publisher',
|
||||
pattern: ':packageName',
|
||||
}
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'Pub Publisher',
|
||||
namedParams: { packageName: 'path' },
|
||||
staticPreview: this.render({ publisher: 'dart.dev' }),
|
||||
keywords: ['dart', 'dartlang'],
|
||||
},
|
||||
]
|
||||
|
||||
static _cacheLength = 3600
|
||||
|
||||
static defaultBadgeData = { label: 'publisher' }
|
||||
|
||||
static render({ publisher }) {
|
||||
return {
|
||||
label: 'publisher',
|
||||
message: publisher == null ? 'unverified' : publisher,
|
||||
color: publisher == null ? 'lightgrey' : 'blue',
|
||||
}
|
||||
}
|
||||
|
||||
async fetch({ packageName }) {
|
||||
return this._requestJson({
|
||||
schema,
|
||||
url: `https://pub.dev/api/packages/${packageName}/publisher`,
|
||||
})
|
||||
}
|
||||
|
||||
async handle({ packageName }) {
|
||||
const data = await this.fetch({ packageName })
|
||||
const publisher = data.publisherId
|
||||
return this.constructor.render({ publisher })
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('package publisher').get('/path.json').expectBadge({
|
||||
label: 'publisher',
|
||||
message: 'dart.dev',
|
||||
})
|
||||
|
||||
t.create('package not verified publisher').get('/utf.json').expectBadge({
|
||||
label: 'publisher',
|
||||
message: 'unverified',
|
||||
color: 'lightgrey',
|
||||
})
|
||||
|
||||
t.create('package not found').get('/does-not-exist.json').expectBadge({
|
||||
label: 'publisher',
|
||||
message: 'not found',
|
||||
})
|
||||
@@ -5,8 +5,7 @@ const schema = Joi.object({
|
||||
info: Joi.object({
|
||||
version: Joi.string().required(),
|
||||
// https://github.com/badges/shields/issues/2022
|
||||
// https://github.com/badges/shields/issues/7728
|
||||
license: Joi.string().allow('').allow(null),
|
||||
license: Joi.string().allow(''),
|
||||
classifiers: Joi.array().items(Joi.string()).required(),
|
||||
}).required(),
|
||||
releases: Joi.object()
|
||||
|
||||
@@ -104,12 +104,6 @@ describe('PyPI helpers', function () {
|
||||
'MIT',
|
||||
])
|
||||
forCases([
|
||||
given({
|
||||
info: {
|
||||
license: null,
|
||||
classifiers: ['License :: OSI Approved :: MIT License'],
|
||||
},
|
||||
}),
|
||||
given({
|
||||
info: {
|
||||
license: '',
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { pep440VersionColor } from '../color-formatters.js'
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
import PypiBase from './pypi-base.js'
|
||||
|
||||
@@ -20,7 +19,7 @@ export default class PypiVersion extends PypiBase {
|
||||
static defaultBadgeData = { label: 'pypi' }
|
||||
|
||||
static render({ version }) {
|
||||
return renderVersionBadge({ version, versionFormatter: pep440VersionColor })
|
||||
return renderVersionBadge({ version })
|
||||
}
|
||||
|
||||
async handle({ egg }) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user