Compare commits

..

5 Commits

Author SHA1 Message Date
chris48s
046856c056 fix 2022-02-08 19:36:56 +00:00
chris48s
851a30be39 test 2022-02-08 19:29:56 +00:00
chris48s
785ee090a9 test 2022-02-08 19:28:19 +00:00
chris48s
0002d6749e fix 2022-02-08 19:24:22 +00:00
chris48s
fb379c0556 lets not pretend this commit history is going to be meaningful 2022-02-08 19:22:47 +00:00
130 changed files with 4682 additions and 4362 deletions

View File

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

View File

@@ -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",

View File

@@ -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
View 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
View 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
View 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
View 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"
}
}

View File

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

View File

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

View File

@@ -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
View 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 }}

View File

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

View File

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

View File

@@ -35,7 +35,7 @@ and legible badges in SVG and raster format, which can easily be included in
GitHub readmes or any other web page. The service supports dozens of
continuous integration services, package registries, distributions, app
stores, social networks, code coverage services, and code analysis services.
Every month it serves over 870 million images and is used by some of the
Every month it serves over 770 million images and is used by some of the
world's most popular open-source projects, [VS Code][vscode], [Vue.js][vue]
and [Bootstrap][bootstrap] to name a few.

View File

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

View File

@@ -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}'`)

View File

@@ -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',
})
})

View File

@@ -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()
})
}
}

View File

@@ -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))
)
}
}

View File

@@ -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',

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

View 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)
})
})
})
})

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

View File

@@ -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,
}
}

View File

@@ -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,
})
})
})

View File

@@ -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()
})

View File

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

View File

@@ -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,
}

View File

@@ -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 () {

View File

@@ -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)
}
}

View File

@@ -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())
})
}

View File

@@ -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')

View File

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

View File

@@ -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()

View File

@@ -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 () {

View File

@@ -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
services 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.

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -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": {

View File

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

View File

@@ -24,8 +24,6 @@ class AmoWeeklyDownloads extends BaseAmoService {
},
]
static _cacheLength = 21600
static defaultBadgeData = { label: 'downloads' }
static render({ downloads }) {

View File

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

View File

@@ -14,8 +14,6 @@ export default class AmoUsers extends BaseAmoService {
},
]
static _cacheLength = 21600
static defaultBadgeData = { label: 'users' }
static render({ users: downloads }) {

View File

@@ -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' }),
},
]

View File

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

View File

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

View File

@@ -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, () => {

View File

@@ -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')

View File

@@ -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' }),
},
]

View File

@@ -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}`,

View File

@@ -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({

View File

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

View File

@@ -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}`,

View File

@@ -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({

View File

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

View File

@@ -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')
})
})

View File

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

View File

@@ -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
)
})
})

View File

@@ -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 })
}
}

View File

@@ -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',
})

View File

@@ -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 })
}
}

View File

@@ -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({

View File

@@ -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),
}
}

View File

@@ -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)
})

View File

@@ -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.'
)

View File

@@ -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',

View File

@@ -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}`

View File

@@ -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 () {

View File

@@ -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),
})

View File

@@ -58,7 +58,7 @@ export default class GithubDeployments extends GithubAuthV4Service {
environment: 'shields-staging',
},
staticPreview: this.render({
state: 'SUCCESS',
state: 'success',
}),
documentation,
},

View File

@@ -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',
}
}

View File

@@ -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',
}
}

View File

@@ -75,7 +75,7 @@ export default class GithubMilestoneDetail extends GithubAuthV3Service {
}
return {
label: `${milestone.title}${label ? ' ' : ''}${label}`,
label: `${milestone.title} ${label}`,
message: metric(milestoneMetric),
color,
}

View File

@@ -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,
}

View File

@@ -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',
}
}

View File

@@ -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,
})

View File

@@ -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 })
}
}

View File

@@ -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: [] },
})
)

View File

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

View File

@@ -32,8 +32,6 @@ export default class LibrariesIoDependentRepos extends LibrariesIoBase {
},
]
static _cacheLength = 900
static defaultBadgeData = {
label: 'dependent repos',
}

View File

@@ -32,8 +32,6 @@ export default class LibrariesIoDependents extends LibrariesIoBase {
},
]
static _cacheLength = 900
static defaultBadgeData = {
label: 'dependents',
}

View File

@@ -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) {

View File

@@ -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 })
}
}

View File

@@ -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',
})

View File

@@ -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 = `

View File

@@ -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'
)
})
})
})

View File

@@ -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 })
}
}

View File

@@ -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')
})
})

View File

@@ -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 })
}

View File

@@ -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')
})
})

View File

@@ -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')

View File

@@ -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 })
}
}

View File

@@ -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 })
}
}

View File

@@ -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')
})
})

View File

@@ -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',
})

View File

@@ -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 })
}
}

View File

@@ -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',
})

View File

@@ -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()

View File

@@ -104,12 +104,6 @@ describe('PyPI helpers', function () {
'MIT',
])
forCases([
given({
info: {
license: null,
classifiers: ['License :: OSI Approved :: MIT License'],
},
}),
given({
info: {
license: '',

View File

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