Compare commits

...

52 Commits

Author SHA1 Message Date
Marcin Mielnicki
a39c7901b5 Require default values 2020-04-21 21:09:50 +02:00
Marcin Mielnicki
a1cdd620e9 Comment added plus small refactring 2020-04-21 20:42:13 +02:00
Marcin Mielnicki
d02c3f045a Unused code removed 2020-04-20 21:35:48 +02:00
Marcin Mielnicki
06eb88eb31 Fetch from new domain in tests 2020-04-20 21:31:53 +02:00
Marcin Mielnicki
85f65734a0 Merge remote-tracking branch 'badges/master' into custom-fetch-limit 2020-04-20 21:23:42 +02:00
Marcin Mielnicki
e66cfa3c21 📈 PaaS-friendly metrics (#4874)
* prom-client JSON to InfluxDB line protocol converter

* Converts a metric with separate names

* prom-client JSON to InfluxDB line protocol (version 2) converter

* Server has instance id

* Read the instance id from an environment variable

* More unit tests for instance-metadata

* Log instance id

* Push influx metrics

* INSTANCE_ID with dyno metadata

* Prepare influx metrics in one place

* Influx metrics endpoint should return metrics

* More readable tests

* Env added to instance metadata

* hostname as an instance label value

* HEROKU_DYNO_ID as an instance id for heroku

* Instance env can be set by env variable

* HEROKU_APP_NAME as an instance env

* Log instance metadata as a JSON

* Typo fix

* Code refactoring in tests

* wait-for-expect dev dependency added

* Test for pushing metrics

* Test for pushing metrics

* Use basic authentication for pushing metrics

* intervalSeconds=2 for development env

* Using existing methods

* TODOs removed

* Schema for influx credentials

* Influx config removed from config files

* Require username and password when influx metrics are enabled

* Unused args removed

* pushing component should log errors

* Speed up tests

* should log error responses

* InstanceMetadata class replaces by simple object

* Influx metrics can be configuredd by env variables

* Use application label name instead of service

* Unused code removed

* Integration test for prom-client and converter

* metrics.influx.enabled configuration option added

* Improved influx configuration schema

* instanceMetadata validation

* Typo fix

* Default value for env

* metrics.infux.hostnameAsAInstanceId added

* should add hostname as an instance label when hostnameAsAInstanceId is enabled

* Default values for influx configuration

* flatMap is not available in Node.js 9.4

* Env vars removed from Procfile

* Better instance metadata values in tests

* Typo fix

* lodash.groupby added to prod dependencies

* Allow other keys in private config

* Missing test - should allow other private keys when influx metrics are enabled

* Missing test - should require private metrics config when influx configuration is enabled

* log.error instead of console.log

* metrics.influx.uri -> metrics.influx.url

* Unused arguments removed

* async removed

* promisify sendMetrics

* Allow to disable prometheus metrics

* Create test server with custom config

* 'metrics-influx' resource removed

* 'metrics-influx' resource removed

* Private config schema flattened out

* Extra code removed in Prometheus tests

* promisify moved outside of the class

* Do not throw errors from got in a specific test

* hostnameAliases added

* instanceIdFrom added

* instanceIdEnvVarName added

* envLabel added to schema

* instanceMetadata is not used by InfluxMetrics

* Instance metadata removed

* hostnameAsAnInstanceId removed

* A comment added

* waitForExpect removed

* Unused code removed
2020-04-19 20:03:00 +02:00
dependabot-preview[bot]
1c48c2207f Build(deps-dev): bump @typescript-eslint/parser from 2.27.0 to 2.28.0 (#4918)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 2.27.0 to 2.28.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v2.28.0/packages/parser)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2020-04-19 16:02:29 +00:00
Pierre-Yves B
a2e0e11ead Fix [CodeclimateCoverage] tests (#4927) 2020-04-19 17:06:35 +02:00
chris48s
2c89a8c59f improve latest() fallback result if no stable versions available (#4901)
* ignore case if we fall back to string sorting versions

* if no latest MaybeSemVer, try latest pre-MaybeSemVer

* filter versions before we pass it to latestMaybeSemVer()
2020-04-18 18:40:39 +01:00
dependabot-preview[bot]
d11fa30f06 Build(deps-dev): bump @typescript-eslint/eslint-plugin (#4916)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 2.27.0 to 2.28.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v2.28.0/packages/eslint-plugin)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2020-04-17 23:38:29 +00:00
dependabot-preview[bot]
05b324093a Build(deps-dev): bump danger from 10.1.0 to 10.1.1 (#4914)
Bumps [danger](https://github.com/danger/danger-js) from 10.1.0 to 10.1.1.
- [Release notes](https://github.com/danger/danger-js/releases)
- [Changelog](https://github.com/danger/danger-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/danger/danger-js/compare/10.1.0...10.1.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2020-04-17 23:29:42 +00:00
dependabot-preview[bot]
42ed874112 Build(deps): bump ioredis from 4.16.1 to 4.16.2 (#4913)
Bumps [ioredis](https://github.com/luin/ioredis) from 4.16.1 to 4.16.2.
- [Release notes](https://github.com/luin/ioredis/releases)
- [Changelog](https://github.com/luin/ioredis/blob/master/Changelog.md)
- [Commits](https://github.com/luin/ioredis/compare/v4.16.1...v4.16.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2020-04-17 23:15:24 +00:00
dependabot-preview[bot]
e2d8c94dab Build(deps): bump query-string from 6.12.0 to 6.12.1 (#4922)
Bumps [query-string](https://github.com/sindresorhus/query-string) from 6.12.0 to 6.12.1.
- [Release notes](https://github.com/sindresorhus/query-string/releases)
- [Commits](https://github.com/sindresorhus/query-string/compare/v6.12.0...v6.12.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2020-04-17 20:30:50 +00:00
dependabot-preview[bot]
91577cb6e9 Build(deps): bump semver from 7.2.2 to 7.3.2 (#4915)
Bumps [semver](https://github.com/npm/node-semver) from 7.2.2 to 7.3.2.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/master/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.2.2...v7.3.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-04-17 20:22:01 +00:00
dependabot-preview[bot]
d578faab50 Build(deps): bump simple-icons from 2.8.0 to 2.9.0 (#4919)
Bumps [simple-icons](https://github.com/simple-icons/simple-icons) from 2.8.0 to 2.9.0.
- [Release notes](https://github.com/simple-icons/simple-icons/releases)
- [Commits](https://github.com/simple-icons/simple-icons/compare/2.8.0...2.9.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2020-04-17 18:48:14 +00:00
dependabot-preview[bot]
b0f367cdfb Build(deps-dev): bump eslint-plugin-sort-class-members (#4921)
Bumps [eslint-plugin-sort-class-members](https://github.com/bryanrsmith/eslint-plugin-sort-class-members) from 1.6.0 to 1.7.0.
- [Release notes](https://github.com/bryanrsmith/eslint-plugin-sort-class-members/releases)
- [Commits](https://github.com/bryanrsmith/eslint-plugin-sort-class-members/compare/v1.6.0...v1.7.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2020-04-17 14:17:57 +00:00
dependabot-preview[bot]
430be7e7d1 Build(deps-dev): bump cypress from 4.3.0 to 4.4.0 (#4917)
Bumps [cypress](https://github.com/cypress-io/cypress) from 4.3.0 to 4.4.0.
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Commits](https://github.com/cypress-io/cypress/compare/v4.3.0...v4.4.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-04-17 14:00:16 +00:00
Pierre-Yves B
2df8289ec8 Fix failing Nexus tests (#4905)
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2020-04-16 23:46:51 +00:00
dependabot-preview[bot]
478d14300c Build(deps-dev): bump eslint-plugin-import from 2.20.1 to 2.20.2 (#4859)
* Build(deps-dev): bump eslint-plugin-import from 2.20.1 to 2.20.2

Bumps [eslint-plugin-import](https://github.com/benmosher/eslint-plugin-import) from 2.20.1 to 2.20.2.
- [Release notes](https://github.com/benmosher/eslint-plugin-import/releases)
- [Changelog](https://github.com/benmosher/eslint-plugin-import/blob/master/CHANGELOG.md)
- [Commits](https://github.com/benmosher/eslint-plugin-import/compare/v2.20.1...v2.20.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

* Fixes

* refactor: combine imports

* refactor: combine imports

* refactor: combine imports

* refactor: update import ordering

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
Co-authored-by: Paul Melnikow <email@paulmelnikow.com>
Co-authored-by: Caleb Cartwright <calebcartwright@users.noreply.github.com>
Co-authored-by: Caleb Cartwright <caleb.cartwright@outlook.com>
2020-04-16 18:39:13 -05:00
Caleb Cartwright
23ceea1d72 fix: TypeError in DockerVersion badge (#4907) 2020-04-15 13:35:05 -05:00
ChrisCarini
2bf6dfdeea Adding badges for JetBrains Plugin Ratings, run [JetBrainsRating]. #4897 (#4898)
* Adding badges for JetBrains Plugin Ratings. Addresses feature request #4897

* Removing unnecessary undefined label and improving variable name in tests.
2020-04-12 17:05:49 +00:00
dependabot-preview[bot]
e4b1fd23b1 Build(deps-dev): bump @typescript-eslint/eslint-plugin (#4894)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 2.26.0 to 2.27.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v2.27.0/packages/eslint-plugin)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-04-11 00:07:53 -05:00
dependabot-preview[bot]
8df5eed088 Build(deps-dev): bump @typescript-eslint/parser from 2.26.0 to 2.27.0 (#4893)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 2.26.0 to 2.27.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v2.27.0/packages/parser)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2020-04-11 03:32:59 +00:00
dependabot-preview[bot]
4c1eee9218 Build(deps-dev): bump styled-components from 5.0.1 to 5.1.0 (#4891)
Bumps [styled-components](https://github.com/styled-components/styled-components) from 5.0.1 to 5.1.0.
- [Release notes](https://github.com/styled-components/styled-components/releases)
- [Changelog](https://github.com/styled-components/styled-components/blob/master/CHANGELOG.md)
- [Commits](https://github.com/styled-components/styled-components/compare/v5.0.1...v5.1.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2020-04-11 03:24:06 +00:00
dependabot-preview[bot]
1c2920ac31 Build(deps-dev): bump nyc from 15.0.0 to 15.0.1 (#4882)
Bumps [nyc](https://github.com/istanbuljs/nyc) from 15.0.0 to 15.0.1.
- [Release notes](https://github.com/istanbuljs/nyc/releases)
- [Changelog](https://github.com/istanbuljs/nyc/blob/master/CHANGELOG.md)
- [Commits](https://github.com/istanbuljs/nyc/compare/v15.0.0...v15.0.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2020-04-10 23:53:44 +00:00
dependabot-preview[bot]
9df6aade11 Build(deps-dev): bump @types/node from 13.11.0 to 13.11.1 (#4883)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 13.11.0 to 13.11.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2020-04-10 20:30:38 +00:00
mondeja
01950a7852 [Conda] license badge added. (#4875)
* Conda license badge added.

* Added schema for Conda license badge.

* Remove comment in conda license service

Co-authored-by: Pierre-Yves B <PyvesDev@gmail.com>
2020-04-10 22:23:21 +02:00
chris48s
935dd25264 ignore camelcase 6, escape-string-regexp 3 updates (#4896)
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2020-04-10 19:55:13 +00:00
dependabot-preview[bot]
c320a58a24 Build(deps): bump query-string from 6.11.1 to 6.12.0 (#4885)
Bumps [query-string](https://github.com/sindresorhus/query-string) from 6.11.1 to 6.12.0.
- [Release notes](https://github.com/sindresorhus/query-string/releases)
- [Commits](https://github.com/sindresorhus/query-string/compare/v6.11.1...v6.12.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2020-04-10 19:48:51 +00:00
dependabot-preview[bot]
10f06ff175 Build(deps): bump semver from 7.1.3 to 7.2.2 (#4895)
Bumps [semver](https://github.com/npm/node-semver) from 7.1.3 to 7.2.2.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/master/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.1.3...v7.2.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-04-10 19:38:35 +00:00
dependabot-preview[bot]
68b1a0cfe5 Build(deps-dev): bump nodemon from 2.0.2 to 2.0.3 (#4886)
Bumps [nodemon](https://github.com/remy/nodemon) from 2.0.2 to 2.0.3.
- [Release notes](https://github.com/remy/nodemon/releases)
- [Commits](https://github.com/remy/nodemon/compare/v2.0.2...v2.0.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2020-04-10 19:18:05 +00:00
dependabot-preview[bot]
39b8ff0aa8 Build(deps): bump check-node-version from 4.0.2 to 4.0.3 (#4890)
Bumps [check-node-version](https://github.com/parshap/check-node-version) from 4.0.2 to 4.0.3.
- [Release notes](https://github.com/parshap/check-node-version/releases)
- [Changelog](https://github.com/parshap/check-node-version/blob/master/CHANGELOG.md)
- [Commits](https://github.com/parshap/check-node-version/compare/v4.0.2...v4.0.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2020-04-10 20:08:57 +01:00
dependabot-preview[bot]
6dc672a03d Build(deps-dev): bump jsdoc from 3.6.3 to 3.6.4 (#4892)
Bumps [jsdoc](https://github.com/jsdoc/jsdoc) from 3.6.3 to 3.6.4.
- [Release notes](https://github.com/jsdoc/jsdoc/releases)
- [Changelog](https://github.com/jsdoc/jsdoc/blob/3.6.4/CHANGES.md)
- [Commits](https://github.com/jsdoc/jsdoc/compare/3.6.3...3.6.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-04-10 18:57:57 +00:00
Caleb Cartwright
642aac6408 fix: SymfonyInsight transform (#4877)
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2020-04-08 19:59:42 +00:00
Joschua Becker
b2bb50234f updating node js version (#4879)
Node.js 8.x LTS Carbon is no longer actively supported!

Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2020-04-08 19:30:26 +00:00
Owen Voke
d16c8404d6 Add [OffsetEarth] badges (#4815)
* Add [OffsetEarth] trees service

* Add mocked tests for [OffsetEarth] tree service

* Update [OffsetEarth] trees name to be more unique

* Add [OffsetEarth] carbon offset service

* Update [OffsetEarth] services to use floorCount()

* Clean up [OffsetEarth] loops and unnecessary code

* Update [OffsetEarth] services to use proper APIs

* Fix order of imports in [OffsetEarth] services

* Update [OffsetEarth] tests to be more robust

* Update [OffsetEarth] tests to check colour

* Update to use unmocked [OffsetEarth] tests

* Update to use nonNegativeInteger in [OffsetEarth]

* Apply additional [OffsetEarth] review comments

* Update [OffsetEarth] references to username

Co-authored-by: Pierre-Yves B <PyvesDev@gmail.com>
2020-04-08 19:18:38 +00:00
dependabot-preview[bot]
b36de3dbf3 Build(deps-dev): bump danger from 10.0.0 to 10.1.0 (#4856)
Bumps [danger](https://github.com/danger/danger-js) from 10.0.0 to 10.1.0.
- [Release notes](https://github.com/danger/danger-js/releases)
- [Changelog](https://github.com/danger/danger-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/danger/danger-js/compare/10.0.0...10.1.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2020-04-08 01:28:16 +00:00
dependabot-preview[bot]
20d4143dfb Build(deps): bump ioredis from 4.16.0 to 4.16.1 (#4861)
Bumps [ioredis](https://github.com/luin/ioredis) from 4.16.0 to 4.16.1.
- [Release notes](https://github.com/luin/ioredis/releases)
- [Changelog](https://github.com/luin/ioredis/blob/master/Changelog.md)
- [Commits](https://github.com/luin/ioredis/compare/v4.16.0...v4.16.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: Paul Melnikow <github@paulmelnikow.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2020-04-07 01:54:03 +00:00
Caleb Cartwright
fbe865e149 feat: deprecate JitPackDownloads (#4873)
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2020-04-06 18:33:52 +00:00
dependabot-preview[bot]
da29c92910 Build(deps-dev): bump @typescript-eslint/eslint-plugin (#4857)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 2.25.0 to 2.26.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v2.26.0/packages/eslint-plugin)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
Co-authored-by: Paul Melnikow <github@paulmelnikow.com>
2020-04-06 17:19:30 +00:00
Marcin Mielnicki
aa185ea07c typo fix 2020-01-31 16:48:44 +01:00
Marcin Mielnicki
b3b772d95c typo fix 2020-01-24 20:50:03 +01:00
Marcin Mielnicki
670dc2bf77 Removed empty line added 2020-01-24 20:45:39 +01:00
Marcin Mielnicki
4b53ffbd3b Bytes type definition 2020-01-20 20:28:42 +01:00
Marcin Mielnicki
18ff7db947 checkCustomIntegrationConfiguration moved to config.js + tests 2020-01-20 20:15:38 +01:00
Marcin Mielnicki
cce0104ea1 Check there is no extra config 2020-01-19 20:33:04 +01:00
Marcin Mielnicki
56a30ef139 Typo fix 2020-01-18 19:58:12 +01:00
Marcin Mielnicki
46fa8adeb9 Use sazerac in config.spec.js 2020-01-12 17:34:58 +01:00
Marcin Mielnicki
f73f828aaf Merge the default configuration with a custom one 2020-01-12 17:20:15 +01:00
Marcin Mielnicki
9e7dfea103 function for configuration 2020-01-12 17:11:19 +01:00
Marcin Mielnicki
9f6f064193 Default values at the top 2020-01-11 18:23:16 +01:00
Marcin Mielnicki
d5812cbce8 Config for custom fetch limit 2020-01-10 21:10:46 +01:00
120 changed files with 2380 additions and 958 deletions

View File

@@ -33,6 +33,9 @@ update_configs:
- match:
dependency_name: babel-preset-gatsby
version_requirement: '>=0.3.0'
- match:
dependency_name: camelcase
version_requirement: '>=6.0.0'
- match:
dependency_name: chalk
version_requirement: '>=4.0.0'
@@ -42,6 +45,9 @@ update_configs:
- match:
dependency_name: decamelize
version_requirement: '>=4.0.0'
- match:
dependency_name: escape-string-regexp
version_requirement: '>=3.0.0'
- match:
dependency_name: eslint-plugin-jsdoc
version_requirement: '>=21.0.0'

View File

@@ -6,6 +6,15 @@ public:
metrics:
prometheus:
enabled: 'METRICS_PROMETHEUS_ENABLED'
endpointEnabled: 'METRICS_PROMETHEUS_ENDPOINT_ENABLED'
influx:
enabled: 'METRICS_INFLUX_ENABLED'
url: 'METRICS_INFLUX_URL'
timeoutMilliseconds: 'METRICS_INFLUX_TIMEOUT_MILLISECONDS'
intervalSeconds: 'METRICS_INFLUX_INTERVAL_SECONDS'
instanceIdFrom: 'METRICS_INFLUX_INSTANCE_ID_FROM'
instanceIdEnvVarName: 'METRICS_INFLUX_INSTANCE_ID_ENV_VAR_NAME'
envLabel: 'METRICS_INFLUX_ENV_LABEL'
ssl:
isSecure: 'HTTPS'
@@ -53,7 +62,9 @@ public:
rateLimit: 'RATE_LIMIT'
fetchLimit: 'FETCH_LIMIT'
integrations:
default:
fetchLimit: 'FETCH_LIMIT'
private:
azure_devops_token: 'AZURE_DEVOPS_TOKEN'
@@ -85,3 +96,5 @@ private:
twitch_client_id: 'TWITCH_CLIENT_ID'
twitch_client_secret: 'TWITCH_CLIENT_SECRET'
wheelmap_token: 'WHEELMAP_TOKEN'
influx_username: 'INFLUX_USERNAME'
influx_password: 'INFLUX_PASSWORD'

View File

@@ -5,7 +5,11 @@ public:
metrics:
prometheus:
enabled: false
endpointEnabled: false
influx:
enabled: false
timeoutMilliseconds: 1000
intervalSeconds: 15
ssl:
isSecure: false
@@ -30,6 +34,17 @@ public:
handleInternalErrors: true
fetchLimit: '10MB'
integrations:
default:
fetchLimit: '10MB'
DynamicJson:
fetchLimit: '2MB'
DynamicXml:
fetchLimit: '128KB'
DynamicYaml:
fetchLimit: '64KB'
private: {}

View File

@@ -2,6 +2,7 @@ public:
metrics:
prometheus:
enabled: true
endpointEnabled: true
ssl:
isSecure: true

View File

@@ -3,6 +3,7 @@
* @module
*/
const bytes = require('bytes')
// See available emoji at http://emoji.muan.co/
const emojic = require('emojic')
const Joi = require('@hapi/joi')
@@ -425,7 +426,8 @@ class BaseService {
{ camp, handleRequest, githubApiProvider, metricInstance },
serviceConfig
) {
const { cacheHeaders: cacheHeaderConfig, fetchLimitBytes } = serviceConfig
const { cacheHeaders: cacheHeaderConfig, fetchLimit } = serviceConfig
const fetchLimitBytes = bytes.parse(fetchLimit)
const { regex, captureNames } = prepareRoute(this.route)
const queryParams = getQueryParamNames(this.route)

38
core/server/config.js Normal file
View File

@@ -0,0 +1,38 @@
'use strict'
const deepmerge = require('deepmerge')
class RedundantCustomConfiguration extends Error {
constructor(message) {
super(message)
this.name = 'RedundantCustomConfiguration'
}
}
function merge(_default, custom) {
// Overwrites the existing array values completely rather than concatenating them
// recipe from https://github.com/TehShrike/deepmerge#arraymerge
const overwriteMerge = (destinationArray, sourceArray, options) => sourceArray
return deepmerge(_default, custom, {
arrayMerge: overwriteMerge,
})
}
function checkCustomIntegrationConfiguration(config, serviceClasses) {
const serviceNames = new Set(
serviceClasses.map(serviceClass => serviceClass.name)
)
const redundantConfigurations = Object.keys(config.public.integrations)
.filter(configName => configName !== 'default')
.filter(configName => !serviceNames.has(configName))
if (redundantConfigurations.length) {
throw new RedundantCustomConfiguration(
`Custom configurations found without a corresponding service: ${redundantConfigurations}`
)
}
}
module.exports = {
RedundantCustomConfiguration,
merge,
checkCustomIntegrationConfiguration,
}

View File

@@ -0,0 +1,61 @@
'use strict'
const { test, given } = require('sazerac')
const { expect } = require('chai')
const {
RedundantCustomConfiguration,
merge,
checkCustomIntegrationConfiguration,
} = require('./config')
describe('configuration', function() {
test(merge, function() {
given({ a: 2 }, {})
.describe('copies the default value')
.expect({ a: 2 })
given({ a: 2 }, { a: 3 })
.describe('overrides a primitive value')
.expect({ a: 3 })
given({ a: { a1: 1, a2: 2 } }, { a: { a1: 2 } })
.describe('merges objects')
.expect({ a: { a1: 2, a2: 2 } })
given({ a: { a1: 1, a2: 2 } }, { a: 3 })
.describe('overrides an object with a primitive')
.expect({ a: 3 })
given({ a: { a1: 1, a2: 2 } }, { a: {} })
.describe('does not override an object with an empty object')
.expect({ a: { a1: 1, a2: 2 } })
given({ a: [2, 3, 4] }, { a: [5, 6] })
.describe('overrides array')
.expect({ a: [5, 6] })
})
describe('checkCustomIntegrationConfiguration function', function() {
it('accepts the default configuration', function() {
const config = { public: { integrations: { default: {} } } }
const serviceClasses = [{ name: 'SomeService' }]
expect(() =>
checkCustomIntegrationConfiguration(config, serviceClasses)
).not.throw()
})
it('accepts a configuration for an existing service', function() {
const config = { public: { integrations: { SomeService: {} } } }
const serviceClasses = [{ name: 'SomeService' }]
expect(() =>
checkCustomIntegrationConfiguration(config, serviceClasses)
).not.throw()
})
it('throws an error if a custom config does not have a corresponding service', function() {
const config = { public: { integrations: { UnknownService: {} } } }
const serviceClasses = [{ name: 'KnownService' }]
expect(() =>
checkCustomIntegrationConfiguration(config, serviceClasses)
).to.throw(RedundantCustomConfiguration)
})
})
})

View File

@@ -1,21 +1,16 @@
'use strict'
const merge = require('deepmerge')
const config = require('config').util.toObject()
const portfinder = require('portfinder')
const Server = require('./server')
function createTestServer({ port }) {
const serverConfig = {
...config,
public: {
...config.public,
bind: {
...config.public.bind,
port,
},
},
async function createTestServer(customConfig = {}) {
const mergedConfig = merge(config, customConfig)
if (!mergedConfig.public.bind.port) {
mergedConfig.public.bind.port = await portfinder.getPortPromise()
}
return new Server(serverConfig)
return new Server(mergedConfig)
}
module.exports = {

View File

@@ -0,0 +1,86 @@
'use strict'
const os = require('os')
const { promisify } = require('util')
const { post } = require('request')
const postAsync = promisify(post)
const generateInstanceId = require('./instance-id-generator')
const { promClientJsonToInfluxV2 } = require('./metrics/format-converters')
const log = require('./log')
module.exports = class InfluxMetrics {
constructor(metricInstance, config) {
this._metricInstance = metricInstance
this._config = config
this._instanceId = this.getInstanceId()
}
async sendMetrics() {
const auth = {
user: this._config.username,
pass: this._config.password,
}
const request = {
uri: this._config.url,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: this.metrics(),
timeout: this._config.timeoutMillseconds,
auth,
}
let response
try {
response = await postAsync(request)
} catch (error) {
log.error(
new Error(`Cannot push metrics. Cause: ${error.name}: ${error.message}`)
)
}
if (response && response.statusCode >= 300) {
log.error(
new Error(
`Cannot push metrics. ${response.request.href} responded with status code ${response.statusCode}`
)
)
}
}
startPushingMetrics() {
this._intervalId = setInterval(
() => this.sendMetrics(),
this._config.intervalSeconds * 1000
)
}
metrics() {
return promClientJsonToInfluxV2(this._metricInstance.metrics(), {
env: this._config.envLabel,
application: 'shields',
instance: this._instanceId,
})
}
getInstanceId() {
const {
hostnameAliases = {},
instanceIdFrom,
instanceIdEnvVarName,
} = this._config
let instance
if (instanceIdFrom === 'env-var') {
instance = process.env[instanceIdEnvVarName]
} else if (instanceIdFrom === 'hostname') {
const hostname = os.hostname()
instance = hostnameAliases[hostname] || hostname
} else if (instanceIdFrom === 'random') {
instance = generateInstanceId()
}
return instance
}
stopPushingMetrics() {
if (this._intervalId) {
clearInterval(this._intervalId)
this._intervalId = undefined
}
}
}

View File

@@ -0,0 +1,177 @@
'use strict'
const os = require('os')
const nock = require('nock')
const sinon = require('sinon')
const { expect } = require('chai')
const log = require('./log')
const InfluxMetrics = require('./influx-metrics')
require('../register-chai-plugins.spec')
describe('Influx metrics', function() {
const metricInstance = {
metrics() {
return [
{
help: 'counter 1 help',
name: 'counter1',
type: 'counter',
values: [{ value: 11, labels: {} }],
aggregator: 'sum',
},
]
},
}
describe('"metrics" function', function() {
let osHostnameStub
afterEach(function() {
nock.enableNetConnect()
delete process.env.INSTANCE_ID
if (osHostnameStub) {
osHostnameStub.restore()
}
})
it('should use an environment variable value as an instance label', async function() {
process.env.INSTANCE_ID = 'instance3'
const influxMetrics = new InfluxMetrics(metricInstance, {
instanceIdFrom: 'env-var',
instanceIdEnvVarName: 'INSTANCE_ID',
})
expect(influxMetrics.metrics()).to.contain('instance=instance3')
})
it('should use a hostname as an instance label', async function() {
osHostnameStub = sinon.stub(os, 'hostname').returns('test-hostname')
const customConfig = {
instanceIdFrom: 'hostname',
}
const influxMetrics = new InfluxMetrics(metricInstance, customConfig)
expect(influxMetrics.metrics()).to.be.contain('instance=test-hostname')
})
it('should use a random string as an instance label', async function() {
const customConfig = {
instanceIdFrom: 'random',
}
const influxMetrics = new InfluxMetrics(metricInstance, customConfig)
expect(influxMetrics.metrics()).to.be.match(/instance=\w+ /)
})
it('should use a hostname alias as an instance label', async function() {
osHostnameStub = sinon.stub(os, 'hostname').returns('test-hostname')
const customConfig = {
instanceIdFrom: 'hostname',
hostnameAliases: { 'test-hostname': 'test-hostname-alias' },
}
const influxMetrics = new InfluxMetrics(metricInstance, customConfig)
expect(influxMetrics.metrics()).to.be.contain(
'instance=test-hostname-alias'
)
})
})
describe('startPushingMetrics', function() {
let influxMetrics, clock
beforeEach(function() {
clock = sinon.useFakeTimers()
})
afterEach(function() {
influxMetrics.stopPushingMetrics()
nock.cleanAll()
nock.enableNetConnect()
delete process.env.INSTANCE_ID
clock.restore()
})
it('should send metrics', async function() {
const scope = nock('http://shields-metrics.io/', {
reqheaders: {
'Content-Type': 'application/x-www-form-urlencoded',
},
})
.persist()
.post(
'/metrics',
'prometheus,application=shields,env=test-env,instance=instance2 counter1=11'
)
.basicAuth({ user: 'metrics-username', pass: 'metrics-password' })
.reply(200)
process.env.INSTANCE_ID = 'instance2'
influxMetrics = new InfluxMetrics(metricInstance, {
url: 'http://shields-metrics.io/metrics',
timeoutMillseconds: 100,
intervalSeconds: 0.001,
username: 'metrics-username',
password: 'metrics-password',
instanceIdFrom: 'env-var',
instanceIdEnvVarName: 'INSTANCE_ID',
envLabel: 'test-env',
})
influxMetrics.startPushingMetrics()
await clock.tickAsync(10)
expect(scope.isDone()).to.be.equal(
true,
`pending mocks: ${scope.pendingMocks()}`
)
})
})
describe('sendMetrics', function() {
beforeEach(function() {
sinon.spy(log, 'error')
})
afterEach(function() {
log.error.restore()
nock.cleanAll()
nock.enableNetConnect()
})
const influxMetrics = new InfluxMetrics(metricInstance, {
url: 'http://shields-metrics.io/metrics',
timeoutMillseconds: 50,
intervalSeconds: 0,
username: 'metrics-username',
password: 'metrics-password',
})
it('should log errors', async function() {
nock.disableNetConnect()
await influxMetrics.sendMetrics()
expect(log.error).to.be.calledWith(
sinon.match
.instanceOf(Error)
.and(
sinon.match.has(
'message',
'Cannot push metrics. Cause: NetConnectNotAllowedError: Nock: Disallowed net connect for "shields-metrics.io:80/metrics"'
)
)
)
})
it('should log error responses', async function() {
nock('http://shields-metrics.io/')
.persist()
.post('/metrics')
.reply(400)
await influxMetrics.sendMetrics()
expect(log.error).to.be.calledWith(
sinon.match
.instanceOf(Error)
.and(
sinon.match.has(
'message',
'Cannot push metrics. http://shields-metrics.io/metrics responded with status code 400'
)
)
)
})
})
})

View File

@@ -0,0 +1,10 @@
'use strict'
function generateInstanceId() {
// from https://gist.github.com/gordonbrander/2230317
return Math.random()
.toString(36)
.substr(2, 9)
}
module.exports = generateInstanceId

View File

@@ -0,0 +1,27 @@
'use strict'
const groupBy = require('lodash.groupby')
function promClientJsonToInfluxV2(metrics, extraLabels = {}) {
// TODO Replace with Array.prototype.flatMap() after migrating to Node.js >= 11
const flatMap = (f, arr) => arr.reduce((acc, x) => acc.concat(f(x)), [])
return flatMap(metric => {
const valuesByLabels = groupBy(metric.values, value =>
JSON.stringify(Object.entries(value.labels).sort())
)
return Object.values(valuesByLabels).map(metricsWithSameLabel => {
const labels = Object.entries(metricsWithSameLabel[0].labels)
.concat(Object.entries(extraLabels))
.sort((a, b) => a[0].localeCompare(b[0]))
.map(labelEntry => `${labelEntry[0]}=${labelEntry[1]}`)
.join(',')
const labelsFormatted = labels ? `,${labels}` : ''
const values = metricsWithSameLabel
.sort((a, b) => a.metricName.localeCompare(b.metricName))
.map(value => `${value.metricName || metric.name}=${value.value}`)
.join(',')
return `prometheus${labelsFormatted} ${values}`
})
}, metrics).join('\n')
}
module.exports = { promClientJsonToInfluxV2 }

View File

@@ -0,0 +1,217 @@
'use strict'
const { expect } = require('chai')
const prometheus = require('prom-client')
const { promClientJsonToInfluxV2 } = require('./format-converters')
describe('Metric format converters', function() {
describe('prom-client JSON to InfluxDB line protocol (version 2)', function() {
it('converts a counter', function() {
const json = [
{
help: 'counter 1 help',
name: 'counter1',
type: 'counter',
values: [{ value: 11, labels: {} }],
aggregator: 'sum',
},
]
const influx = promClientJsonToInfluxV2(json)
expect(influx).to.be.equal('prometheus counter1=11')
})
it('converts a counter (from prometheus registry)', function() {
const register = new prometheus.Registry()
const counter = new prometheus.Counter({
name: 'counter1',
help: 'counter 1 help',
registers: [register],
})
counter.inc(11)
const influx = promClientJsonToInfluxV2(register.getMetricsAsJSON())
expect(influx).to.be.equal('prometheus counter1=11')
})
it('converts a gauge', function() {
const json = [
{
help: 'gause 1 help',
name: 'gauge1',
type: 'gauge',
values: [{ value: 20, labels: {} }],
aggregator: 'sum',
},
]
const influx = promClientJsonToInfluxV2(json)
expect(influx).to.be.equal('prometheus gauge1=20')
})
it('converts a gauge (from prometheus registry)', function() {
const register = new prometheus.Registry()
const gauge = new prometheus.Gauge({
name: 'gauge1',
help: 'gauge 1 help',
registers: [register],
})
gauge.inc(20)
const influx = promClientJsonToInfluxV2(register.getMetricsAsJSON())
expect(influx).to.be.equal('prometheus gauge1=20')
})
const sortLines = text =>
text
.split('\n')
.sort()
.join('\n')
it('converts a histogram', function() {
const json = [
{
name: 'histogram1',
help: 'histogram 1 help',
type: 'histogram',
values: [
{ labels: { le: 5 }, value: 1, metricName: 'histogram1_bucket' },
{ labels: { le: 15 }, value: 2, metricName: 'histogram1_bucket' },
{ labels: { le: 50 }, value: 2, metricName: 'histogram1_bucket' },
{
labels: { le: '+Inf' },
value: 3,
metricName: 'histogram1_bucket',
},
{ labels: {}, value: 111, metricName: 'histogram1_sum' },
{ labels: {}, value: 3, metricName: 'histogram1_count' },
],
aggregator: 'sum',
},
]
const influx = promClientJsonToInfluxV2(json)
expect(sortLines(influx)).to.be.equal(
sortLines(`prometheus,le=+Inf histogram1_bucket=3
prometheus,le=50 histogram1_bucket=2
prometheus,le=15 histogram1_bucket=2
prometheus,le=5 histogram1_bucket=1
prometheus histogram1_count=3,histogram1_sum=111`)
)
})
it('converts a histogram (from prometheus registry)', function() {
const register = new prometheus.Registry()
const histogram = new prometheus.Histogram({
name: 'histogram1',
help: 'histogram 1 help',
buckets: [5, 15, 50],
registers: [register],
})
histogram.observe(100)
histogram.observe(10)
histogram.observe(1)
const influx = promClientJsonToInfluxV2(register.getMetricsAsJSON())
expect(sortLines(influx)).to.be.equal(
sortLines(`prometheus,le=+Inf histogram1_bucket=3
prometheus,le=50 histogram1_bucket=2
prometheus,le=15 histogram1_bucket=2
prometheus,le=5 histogram1_bucket=1
prometheus histogram1_count=3,histogram1_sum=111`)
)
})
it('converts a summary', function() {
const json = [
{
name: 'summary1',
help: 'summary 1 help',
type: 'summary',
values: [
{ labels: { quantile: 0.1 }, value: 1 },
{ labels: { quantile: 0.9 }, value: 100 },
{ labels: { quantile: 0.99 }, value: 100 },
{ metricName: 'summary1_sum', labels: {}, value: 111 },
{ metricName: 'summary1_count', labels: {}, value: 3 },
],
aggregator: 'sum',
},
]
const influx = promClientJsonToInfluxV2(json)
expect(sortLines(influx)).to.be.equal(
sortLines(`prometheus,quantile=0.99 summary1=100
prometheus,quantile=0.9 summary1=100
prometheus,quantile=0.1 summary1=1
prometheus summary1_count=3,summary1_sum=111`)
)
})
it('converts a summary (from prometheus registry)', function() {
const register = new prometheus.Registry()
const summary = new prometheus.Summary({
name: 'summary1',
help: 'summary 1 help',
percentiles: [0.1, 0.9, 0.99],
registers: [register],
})
summary.observe(100)
summary.observe(10)
summary.observe(1)
const influx = promClientJsonToInfluxV2(register.getMetricsAsJSON())
expect(sortLines(influx)).to.be.equal(
sortLines(`prometheus,quantile=0.99 summary1=100
prometheus,quantile=0.9 summary1=100
prometheus,quantile=0.1 summary1=1
prometheus summary1_count=3,summary1_sum=111`)
)
})
it('converts a counter and skip a timestamp', function() {
const json = [
{
help: 'counter 4 help',
name: 'counter4',
type: 'counter',
values: [{ value: 11, labels: {}, timestamp: 1581850552292 }],
aggregator: 'sum',
},
]
const influx = promClientJsonToInfluxV2(json)
expect(influx).to.be.equal('prometheus counter4=11')
})
it('converts a counter and adds extra labels', function() {
const json = [
{
help: 'counter 1 help',
name: 'counter1',
type: 'counter',
values: [{ value: 11, labels: {} }],
aggregator: 'sum',
},
]
const influx = promClientJsonToInfluxV2(json, {
instance: 'instance1',
env: 'production',
})
expect(influx).to.be.equal(
'prometheus,env=production,instance=instance1 counter1=11'
)
})
})
})

View File

@@ -68,11 +68,13 @@ module.exports = class PrometheusMetrics {
registers: [this.register],
}),
}
this.interval = prometheus.collectDefaultMetrics({
register: this.register,
})
}
async initialize(server) {
async registerMetricsEndpoint(server) {
const { register } = this
this.interval = prometheus.collectDefaultMetrics({ register })
server.route(/^\/metrics$/, (data, match, end, ask) => {
ask.res.setHeader('Content-Type', register.contentType)
@@ -88,6 +90,10 @@ module.exports = class PrometheusMetrics {
}
}
metrics() {
return this.register.getMetricsAsJSON()
}
/**
* @returns {object} `{ inc() {} }`.
*/

View File

@@ -7,26 +7,26 @@ const got = require('../got-test-client')
const Metrics = require('./prometheus-metrics')
describe('Prometheus metrics route', function() {
let port, baseUrl
let port, baseUrl, camp, metrics
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 (metrics) {
metrics.stop()
}
if (camp) {
await new Promise(resolve => camp.close(resolve))
camp = undefined
}
})
it('returns metrics', async function() {
new Metrics({ enabled: true }).initialize(camp)
it('returns default metrics', async function() {
metrics = new Metrics()
metrics.registerMetricsEndpoint(camp)
const { statusCode, body } = await got(`${baseUrl}/metrics`)

View File

@@ -6,7 +6,6 @@
const path = require('path')
const url = require('url')
const { URL } = url
const bytes = require('bytes')
const Camp = require('camp')
const originalJoi = require('@hapi/joi')
const makeBadge = require('../../gh-badges/lib/make-badge')
@@ -20,9 +19,11 @@ const {
} = require('../base-service/legacy-request-handler')
const { clearRegularUpdateCache } = require('../legacy/regular-update')
const { rasterRedirectUrl } = require('../badge-urls/make-badge-url')
const { merge, checkCustomIntegrationConfiguration } = require('./config')
const log = require('./log')
const sysMonitor = require('./monitor')
const PrometheusMetrics = require('./prometheus-metrics')
const InfluxMetrics = require('./influx-metrics')
const Joi = originalJoi
.extend(base => ({
@@ -58,6 +59,13 @@ const Joi = originalJoi
const optionalUrl = Joi.string().uri({ scheme: ['http', 'https'] })
const requiredUrl = optionalUrl.required()
const bytes = Joi.string().regex(/^[0-9]+(b|kb|mb|gb|tb)$/i)
const requireFields = {
required: schema => schema.required(),
}
const integrationSchema = Joi.object({
fetchLimit: bytes.alter(requireFields),
})
const origins = Joi.arrayFromString().items(Joi.string().origin())
const defaultService = Joi.object({ authorizedOrigins: origins }).default({
authorizedOrigins: [],
@@ -81,6 +89,33 @@ const publicConfigSchema = Joi.object({
metrics: {
prometheus: {
enabled: Joi.boolean().required(),
endpointEnabled: Joi.boolean().required(),
},
influx: {
enabled: Joi.boolean().required(),
url: Joi.string()
.uri()
.when('enabled', { is: true, then: Joi.required() }),
timeoutMilliseconds: Joi.number()
.integer()
.min(1)
.when('enabled', { is: true, then: Joi.required() }),
intervalSeconds: Joi.number()
.integer()
.min(1)
.when('enabled', { is: true, then: Joi.required() }),
instanceIdFrom: Joi.string()
.equal('hostname', 'env-var', 'random')
.when('enabled', { is: true, then: Joi.required() }),
instanceIdEnvVarName: Joi.string().when('instanceIdFrom', {
is: 'env-var',
then: Joi.required(),
}),
envLabel: Joi.string().when('enabled', {
is: true,
then: Joi.required(),
}),
hostnameAliases: Joi.object(),
},
},
ssl: {
@@ -130,7 +165,9 @@ const publicConfigSchema = Joi.object({
},
rateLimit: Joi.boolean().required(),
handleInternalErrors: Joi.boolean().required(),
fetchLimit: Joi.string().regex(/^[0-9]+(b|kb|mb|gb|tb)$/i),
integrations: Joi.object({
default: integrationSchema.tailor('required').required(),
}).pattern(Joi.string(), integrationSchema),
}).required()
const privateConfigSchema = Joi.object({
@@ -160,8 +197,13 @@ const privateConfigSchema = Joi.object({
twitch_client_id: Joi.string(),
twitch_client_secret: Joi.string(),
wheelmap_token: Joi.string(),
influx_username: Joi.string(),
influx_password: Joi.string(),
}).required()
const privateMetricsInfluxConfigSchema = privateConfigSchema.append({
influx_username: Joi.string().required(),
influx_password: Joi.string().required(),
})
/**
* The Server is based on the web framework Scoutcamp. It creates
* an http server, sets up helpers for token persistence and monitoring.
@@ -173,22 +215,25 @@ class Server {
* Badge Server Constructor
*
* @param {object} config Configuration object read from config yaml files
* by https://www.npmjs.com/package/config and validated against
* publicConfigSchema and privateConfigSchema
* by https://www.npmjs.com/package/config and validated against
* publicConfigSchema and privateConfigSchema
* @see https://github.com/badges/shields/blob/master/doc/production-hosting.md#configuration
* @see https://github.com/badges/shields/blob/master/doc/server-secrets.md
*/
constructor(config) {
const publicConfig = Joi.attempt(config.public, publicConfigSchema)
let privateConfig
try {
privateConfig = Joi.attempt(config.private, privateConfigSchema)
} catch (e) {
const badPaths = e.details.map(({ path }) => path)
throw Error(
`Private configuration is invalid. Check these paths: ${badPaths.join(
','
)}`
const privateConfig = this.validatePrivateConfig(
config.private,
privateConfigSchema
)
// We want to require an username and a password for the influx metrics
// only if the influx metrics are enabled. The private config schema
// and the public config schema are two separate schemas so we have to run
// validation manually.
if (publicConfig.metrics.influx && publicConfig.metrics.influx.enabled) {
this.validatePrivateConfig(
config.private,
privateMetricsInfluxConfigSchema
)
}
this.config = {
@@ -201,8 +246,31 @@ class Server {
service: publicConfig.services.github,
private: privateConfig,
})
if (publicConfig.metrics.prometheus.enabled) {
this.metricInstance = new PrometheusMetrics()
if (publicConfig.metrics.influx.enabled) {
this.influxMetrics = new InfluxMetrics(
this.metricInstance,
Object.assign({}, publicConfig.metrics.influx, {
username: privateConfig.influx_username,
password: privateConfig.influx_password,
})
)
}
}
}
validatePrivateConfig(privateConfig, privateConfigSchema) {
try {
return Joi.attempt(privateConfig, privateConfigSchema)
} catch (e) {
const badPaths = e.details.map(({ path }) => path)
throw Error(
`Private configuration is invalid. Check these paths: ${badPaths.join(
','
)}`
)
}
}
@@ -332,19 +400,25 @@ class Server {
const { config, camp, metricInstance } = this
const { apiProvider: githubApiProvider } = this.githubConstellation
loadServiceClasses().forEach(serviceClass =>
const serviceClasses = loadServiceClasses()
checkCustomIntegrationConfiguration(config, serviceClasses)
serviceClasses.forEach(serviceClass => {
const serviceConfig = merge(
config.public.integrations.default,
config.public.integrations[serviceClass.name] || {}
)
serviceClass.register(
{ camp, handleRequest, githubApiProvider, metricInstance },
{
handleInternalErrors: config.public.handleInternalErrors,
cacheHeaders: config.public.cacheHeaders,
fetchLimitBytes: bytes(config.public.fetchLimit),
rasterUrl: config.public.rasterUrl,
private: config.private,
public: config.public,
...serviceConfig,
}
)
)
})
}
/**
@@ -381,7 +455,12 @@ class Server {
const { githubConstellation } = this
githubConstellation.initialize(camp)
if (metricInstance) {
metricInstance.initialize(camp)
if (this.config.public.metrics.prometheus.endpointEnabled) {
metricInstance.registerMetricsEndpoint(camp)
}
if (this.influxMetrics) {
this.influxMetrics.startPushingMetrics()
}
}
const { apiProvider: githubApiProvider } = this.githubConstellation
@@ -425,6 +504,9 @@ class Server {
}
if (this.metricInstance) {
if (this.influxMetrics) {
this.influxMetrics.stopPushingMetrics()
}
this.metricInstance.stop()
}
}

View File

@@ -2,163 +2,333 @@
const { expect } = require('chai')
const isSvg = require('is-svg')
const portfinder = require('portfinder')
const config = require('config')
const got = require('../got-test-client')
const Server = require('./server')
const { createTestServer } = require('./in-process-server-test-helpers')
describe('The server', function() {
let server, baseUrl
before('Start the server', async function() {
// Fixes https://github.com/badges/shields/issues/2611
this.timeout(10000)
const port = await portfinder.getPortPromise()
server = createTestServer({ port })
baseUrl = server.baseUrl
await server.start()
})
after('Shut down the server', async function() {
if (server) {
await server.stop()
}
server = undefined
})
it('should allow strings for port', async function() {
// fixes #4391 - This allows the app to be run using iisnode, which uses a named pipe for the port.
const pipeServer = createTestServer({
port: '\\\\.\\pipe\\9c137306-7c4d-461e-b7cf-5213a3939ad6',
describe('running', function() {
let server, baseUrl
before('Start the server', async function() {
// Fixes https://github.com/badges/shields/issues/2611
this.timeout(10000)
server = await createTestServer()
baseUrl = server.baseUrl
await server.start()
})
after('Shut down the server', async function() {
if (server) {
await server.stop()
}
server = undefined
})
expect(pipeServer).to.not.be.undefined
})
it('should produce colorscheme badges', async function() {
const { statusCode, body } = await got(`${baseUrl}:fruit-apple-green.svg`)
expect(statusCode).to.equal(200)
expect(body)
.to.satisfy(isSvg)
.and.to.include('fruit')
.and.to.include('apple')
})
it('should allow strings for port', async function() {
// fixes #4391 - This allows the app to be run using iisnode, which uses a named pipe for the port.
const pipeServer = await createTestServer({
public: {
bind: {
port: '\\\\.\\pipe\\9c137306-7c4d-461e-b7cf-5213a3939ad6',
},
},
})
expect(pipeServer).to.not.be.undefined
})
it('should redirect colorscheme PNG badges as configured', async function() {
const { statusCode, headers } = await got(
`${baseUrl}:fruit-apple-green.png`,
{
it('should produce colorscheme badges', async function() {
const { statusCode, body } = await got(`${baseUrl}:fruit-apple-green.svg`)
expect(statusCode).to.equal(200)
expect(body)
.to.satisfy(isSvg)
.and.to.include('fruit')
.and.to.include('apple')
})
it('should redirect colorscheme PNG badges as configured', async function() {
const { statusCode, headers } = await got(
`${baseUrl}:fruit-apple-green.png`,
{
followRedirect: false,
}
)
expect(statusCode).to.equal(301)
expect(headers.location).to.equal(
'http://raster.example.test/:fruit-apple-green.png'
)
})
it('should redirect modern PNG badges as configured', async function() {
const { statusCode, headers } = await got(`${baseUrl}npm/v/express.png`, {
followRedirect: false,
}
)
expect(statusCode).to.equal(301)
expect(headers.location).to.equal(
'http://raster.example.test/:fruit-apple-green.png'
)
})
it('should redirect modern PNG badges as configured', async function() {
const { statusCode, headers } = await got(`${baseUrl}npm/v/express.png`, {
followRedirect: false,
})
expect(statusCode).to.equal(301)
expect(headers.location).to.equal(
'http://raster.example.test/npm/v/express.png'
)
})
expect(statusCode).to.equal(301)
expect(headers.location).to.equal(
'http://raster.example.test/npm/v/express.png'
)
})
it('should produce json badges', async function() {
const { statusCode, body, headers } = await got(
`${baseUrl}twitter/follow/_Pyves.json`
)
expect(statusCode).to.equal(200)
expect(headers['content-type']).to.equal('application/json')
expect(() => JSON.parse(body)).not.to.throw()
})
it('should produce json badges', async function() {
const { statusCode, body, headers } = await got(
`${baseUrl}twitter/follow/_Pyves.json`
)
expect(statusCode).to.equal(200)
expect(headers['content-type']).to.equal('application/json')
expect(() => JSON.parse(body)).not.to.throw()
})
it('should preserve label case', async function() {
const { statusCode, body } = await got(`${baseUrl}:fRuiT-apple-green.svg`)
expect(statusCode).to.equal(200)
expect(body)
.to.satisfy(isSvg)
.and.to.include('fRuiT')
})
it('should preserve label case', async function() {
const { statusCode, body } = await got(`${baseUrl}:fRuiT-apple-green.svg`)
expect(statusCode).to.equal(200)
expect(body)
.to.satisfy(isSvg)
.and.to.include('fRuiT')
})
// https://github.com/badges/shields/pull/1319
it('should not crash with a numeric logo', async function() {
const { statusCode, body } = await got(
`${baseUrl}:fruit-apple-green.svg?logo=1`
)
expect(statusCode).to.equal(200)
expect(body)
.to.satisfy(isSvg)
.and.to.include('fruit')
.and.to.include('apple')
})
// https://github.com/badges/shields/pull/1319
it('should not crash with a numeric logo', async function() {
const { statusCode, body } = await got(
`${baseUrl}:fruit-apple-green.svg?logo=1`
)
expect(statusCode).to.equal(200)
expect(body)
.to.satisfy(isSvg)
.and.to.include('fruit')
.and.to.include('apple')
})
it('should not crash with a numeric link', async function() {
const { statusCode, body } = await got(
`${baseUrl}:fruit-apple-green.svg?link=1`
)
expect(statusCode).to.equal(200)
expect(body)
.to.satisfy(isSvg)
.and.to.include('fruit')
.and.to.include('apple')
})
it('should not crash with a numeric link', async function() {
const { statusCode, body } = await got(
`${baseUrl}:fruit-apple-green.svg?link=1`
)
expect(statusCode).to.equal(200)
expect(body)
.to.satisfy(isSvg)
.and.to.include('fruit')
.and.to.include('apple')
})
it('should not crash with a boolean link', async function() {
const { statusCode, body } = await got(
`${baseUrl}:fruit-apple-green.svg?link=true`
)
expect(statusCode).to.equal(200)
expect(body)
.to.satisfy(isSvg)
.and.to.include('fruit')
.and.to.include('apple')
})
it('should not crash with a boolean link', async function() {
const { statusCode, body } = await got(
`${baseUrl}:fruit-apple-green.svg?link=true`
)
expect(statusCode).to.equal(200)
expect(body)
.to.satisfy(isSvg)
.and.to.include('fruit')
.and.to.include('apple')
})
it('should return the 404 badge for unknown badges', async function() {
const { statusCode, body } = await got(
`${baseUrl}this/is/not/a/badge.svg`,
{ throwHttpErrors: false }
)
expect(statusCode).to.equal(404)
expect(body)
.to.satisfy(isSvg)
.and.to.include('404')
.and.to.include('badge not found')
})
it('should return the 404 badge for unknown badges', async function() {
const { statusCode, body } = await got(
`${baseUrl}this/is/not/a/badge.svg`,
{
throwHttpErrors: false,
}
)
expect(statusCode).to.equal(404)
expect(body)
.to.satisfy(isSvg)
.and.to.include('404')
.and.to.include('badge not found')
})
it('should return the 404 badge page for rando links', async function() {
const { statusCode, body } = await got(
`${baseUrl}this/is/most/definitely/not/a/badge.js`,
{
it('should return the 404 badge page for rando links', async function() {
const { statusCode, body } = await got(
`${baseUrl}this/is/most/definitely/not/a/badge.js`,
{
throwHttpErrors: false,
}
)
expect(statusCode).to.equal(404)
expect(body)
.to.satisfy(isSvg)
.and.to.include('404')
.and.to.include('badge not found')
})
it('should redirect the root as configured', async function() {
const { statusCode, headers } = await got(baseUrl, {
followRedirect: false,
})
expect(statusCode).to.equal(302)
// This value is set in `config/test.yml`
expect(headers.location).to.equal('http://frontend.example.test')
})
it('should return the 410 badge for obsolete formats', async function() {
const { statusCode, body } = await got(`${baseUrl}npm/v/express.jpg`, {
throwHttpErrors: false,
})
// 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')
})
})
describe('configuration', function() {
let server
afterEach(async function() {
if (server) {
server.stop()
}
)
expect(statusCode).to.equal(404)
expect(body)
.to.satisfy(isSvg)
.and.to.include('404')
.and.to.include('badge not found')
})
it('should redirect the root as configured', async function() {
const { statusCode, headers } = await got(baseUrl, {
followRedirect: false,
})
expect(statusCode).to.equal(302)
// This value is set in `config/test.yml`
expect(headers.location).to.equal('http://frontend.example.test')
it('should allow to enable prometheus metrics', async function() {
// Fixes https://github.com/badges/shields/issues/2611
this.timeout(10000)
server = await createTestServer({
public: {
metrics: { prometheus: { enabled: true, endpointEnabled: true } },
},
})
await server.start()
const { statusCode } = await got(`${server.baseUrl}metrics`)
expect(statusCode).to.be.equal(200)
})
it('should allow to disable prometheus metrics', async function() {
// Fixes https://github.com/badges/shields/issues/2611
this.timeout(10000)
server = await createTestServer({
public: {
metrics: { prometheus: { enabled: true, endpointEnabled: false } },
},
})
await server.start()
const { statusCode } = await got(`${server.baseUrl}metrics`, {
throwHttpErrors: false,
})
expect(statusCode).to.be.equal(404)
})
})
it('should return the 410 badge for obsolete formats', async function() {
const { statusCode, body } = await got(`${baseUrl}npm/v/express.jpg`, {
throwHttpErrors: false,
describe('configuration validation', function() {
describe('influx', function() {
let customConfig
beforeEach(function() {
customConfig = config.util.toObject()
customConfig.public.metrics.influx = {
enabled: true,
url: 'http://localhost:8081/telegraf',
timeoutMilliseconds: 1000,
intervalSeconds: 2,
instanceIdFrom: 'random',
instanceIdEnvVarName: 'INSTANCE_ID',
hostnameAliases: { 'metrics-hostname': 'metrics-hostname-alias' },
envLabel: 'test-env',
}
customConfig.private = {
influx_username: 'telegraf',
influx_password: 'telegrafpass',
}
})
it('should not require influx configuration', function() {
delete customConfig.public.metrics.influx
expect(() => new Server(config.util.toObject())).to.not.throw()
})
it('should require url when influx configuration is enabled', function() {
delete customConfig.public.metrics.influx.url
expect(() => new Server(customConfig)).to.throw(
'"metrics.influx.url" is required'
)
})
it('should not require url when influx configuration is disabled', function() {
customConfig.public.metrics.influx.enabled = false
delete customConfig.public.metrics.influx.url
expect(() => new Server(customConfig)).to.not.throw()
})
it('should require timeoutMilliseconds when influx configuration is enabled', function() {
delete customConfig.public.metrics.influx.timeoutMilliseconds
expect(() => new Server(customConfig)).to.throw(
'"metrics.influx.timeoutMilliseconds" is required'
)
})
it('should require intervalSeconds when influx configuration is enabled', function() {
delete customConfig.public.metrics.influx.intervalSeconds
expect(() => new Server(customConfig)).to.throw(
'"metrics.influx.intervalSeconds" is required'
)
})
it('should require instanceIdFrom when influx configuration is enabled', function() {
delete customConfig.public.metrics.influx.instanceIdFrom
expect(() => new Server(customConfig)).to.throw(
'"metrics.influx.instanceIdFrom" is required'
)
})
it('should require instanceIdEnvVarName when instanceIdFrom is env-var', function() {
customConfig.public.metrics.influx.instanceIdFrom = 'env-var'
delete customConfig.public.metrics.influx.instanceIdEnvVarName
expect(() => new Server(customConfig)).to.throw(
'"metrics.influx.instanceIdEnvVarName" is required'
)
})
it('should allow instanceIdFrom = hostname', function() {
customConfig.public.metrics.influx.instanceIdFrom = 'hostname'
expect(() => new Server(customConfig)).to.not.throw()
})
it('should allow instanceIdFrom = env-var', function() {
customConfig.public.metrics.influx.instanceIdFrom = 'env-var'
expect(() => new Server(customConfig)).to.not.throw()
})
it('should allow instanceIdFrom = random', function() {
customConfig.public.metrics.influx.instanceIdFrom = 'random'
expect(() => new Server(customConfig)).to.not.throw()
})
it('should require envLabel when influx configuration is enabled', function() {
delete customConfig.public.metrics.influx.envLabel
expect(() => new Server(customConfig)).to.throw(
'"metrics.influx.envLabel" is required'
)
})
it('should not require hostnameAliases', function() {
delete customConfig.public.metrics.influx.hostnameAliases
expect(() => new Server(customConfig)).to.not.throw()
})
it('should allow empty hostnameAliases', function() {
customConfig.public.metrics.influx.hostnameAliases = {}
expect(() => new Server(customConfig)).to.not.throw()
})
it('should require username when influx configuration is enabled', function() {
delete customConfig.private.influx_username
expect(() => new Server(customConfig)).to.throw(
'Private configuration is invalid. Check these paths: influx_username'
)
})
it('should require password when influx configuration is enabled', function() {
delete customConfig.private.influx_password
expect(() => new Server(customConfig)).to.throw(
'Private configuration is invalid. Check these paths: influx_password'
)
})
it('should allow other private keys', function() {
customConfig.private.gh_token = 'my-token'
expect(() => new Server(customConfig)).to.not.throw()
})
})
// 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')
})
})

View File

@@ -73,8 +73,14 @@ if (process.env.TESTED_SERVER_URL) {
} else {
const port = 1111
baseUrl = 'http://localhost:1111'
before('Start running the server', function() {
server = createTestServer({ port })
before('Start running the server', async function() {
server = await createTestServer({
public: {
bind: {
port,
},
},
})
server.start()
})
after('Shut down the server', async function() {

View File

@@ -8,7 +8,7 @@ You will need Node 8 or later, which you can install using a
On Ubuntu / Debian:
```sh
curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -; sudo apt-get install -y nodejs
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -; sudo apt-get install -y nodejs
```
```sh

1192
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -29,11 +29,12 @@
"camp": "~17.2.4",
"chai-as-promised": "^7.1.1",
"chalk": "^3.0.0",
"check-node-version": "^4.0.2",
"check-node-version": "^4.0.3",
"chrome-web-store-item-property": "~1.2.0",
"config": "^3.3.1",
"cross-env": "^6.0.3",
"decamelize": "^3.2.0",
"deepmerge": "^4.2.2",
"dotenv": "^8.2.0",
"emojic": "^1.1.15",
"escape-string-regexp": "^2.0.0",
@@ -43,11 +44,12 @@
"glob": "^7.1.6",
"graphql": "^14.6.0",
"graphql-tag": "^2.10.3",
"ioredis": "4.16.0",
"ioredis": "4.16.2",
"joi-extension-semver": "4.0.0",
"js-yaml": "^3.13.1",
"jsonpath": "~1.0.2",
"lodash.countby": "^4.6.0",
"lodash.groupby": "^4.6.0",
"lodash.times": "^4.3.2",
"moment": "^2.24.0",
"node-env-flag": "^0.1.0",
@@ -56,10 +58,10 @@
"pretty-bytes": "^5.3.0",
"priorityqueuejs": "^1.0.0",
"prom-client": "^11.5.3",
"query-string": "^6.11.1",
"query-string": "^6.12.1",
"request": "~2.88.2",
"semver": "~7.1.3",
"simple-icons": "2.8.0",
"semver": "~7.3.2",
"simple-icons": "2.9.0",
"xmldom": "~0.2.1",
"xpath": "~0.0.27"
},
@@ -151,13 +153,13 @@
"@types/lodash.debounce": "^4.0.6",
"@types/lodash.groupby": "^4.6.6",
"@types/mocha": "^7.0.2",
"@types/node": "^13.11.0",
"@types/node": "^13.11.1",
"@types/react-helmet": "^5.0.15",
"@types/react-modal": "^3.10.5",
"@types/react-select": "^3.0.11",
"@types/styled-components": "4.1.8",
"@typescript-eslint/eslint-plugin": "^2.25.0",
"@typescript-eslint/parser": "^2.26.0",
"@typescript-eslint/eslint-plugin": "^2.28.0",
"@typescript-eslint/parser": "^2.28.0",
"babel-plugin-inline-react-svg": "^1.1.1",
"babel-plugin-istanbul": "^6.0.0",
"babel-preset-gatsby": "^0.2.36",
@@ -170,9 +172,10 @@
"child-process-promise": "^2.2.1",
"clipboard-copy": "^3.1.0",
"concurrently": "^5.1.0",
"cypress": "^4.3.0",
"danger": "^10.0.0",
"cypress": "^4.4.0",
"danger": "^10.1.1",
"danger-plugin-no-test-shortcuts": "^2.0.0",
"deepmerge": "^4.2.2",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2",
"eslint": "^6.8.0",
@@ -181,7 +184,7 @@
"eslint-config-standard-react": "^9.2.0",
"eslint-plugin-chai-friendly": "^0.5.0",
"eslint-plugin-cypress": "^2.10.3",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-jsdoc": "^20.4.0",
"eslint-plugin-mocha": "^6.3.0",
"eslint-plugin-no-extension-in-require": "^0.2.0",
@@ -189,7 +192,7 @@
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-react": "^7.19.0",
"eslint-plugin-react-hooks": "^2.5.1",
"eslint-plugin-sort-class-members": "^1.6.0",
"eslint-plugin-sort-class-members": "^1.7.0",
"eslint-plugin-standard": "^4.0.1",
"fetch-ponyfill": "^6.1.0",
"fs-readfile-promise": "^3.0.1",
@@ -208,11 +211,10 @@
"is-png": "^2.0.0",
"is-svg": "^4.2.1",
"js-yaml-loader": "^1.2.2",
"jsdoc": "^3.6.3",
"jsdoc": "^3.6.4",
"lint-staged": "^9.5.0",
"lodash.debounce": "^4.0.8",
"lodash.difference": "^4.5.0",
"lodash.groupby": "^4.6.0",
"minimist": "^1.2.5",
"mocha": "^6.2.3",
"mocha-env-reporter": "^4.0.0",
@@ -220,9 +222,9 @@
"mocha-yaml-loader": "^1.0.3",
"nock": "11.9.1",
"node-mocks-http": "^1.8.1",
"nodemon": "^2.0.2",
"nodemon": "^2.0.3",
"npm-run-all": "^4.1.5",
"nyc": "^15.0.0",
"nyc": "^15.0.1",
"opn-cli": "^5.0.0",
"portfinder": "^1.0.25",
"prettier": "1.19.1",
@@ -242,7 +244,7 @@
"sinon-chai": "^3.5.0",
"snap-shot-it": "^7.9.3",
"start-server-and-test": "1.10.7",
"styled-components": "^5.0.1",
"styled-components": "^5.1.0",
"tmp": "0.1.0",
"ts-mocha": "^7.0.0",
"typescript": "^3.8.3"

View File

@@ -2,8 +2,8 @@
const { metric } = require('../text-formatters')
const { downloadCount } = require('../color-formatters')
const { BaseAmoService, keywords } = require('./amo-base')
const { redirector } = require('..')
const { BaseAmoService, keywords } = require('./amo-base')
const documentation = `
<p>

View File

@@ -1,8 +1,8 @@
'use strict'
const { renderBuildStatusBadge } = require('../build-status')
const AppVeyorBase = require('./appveyor-base')
const { NotFound } = require('..')
const AppVeyorBase = require('./appveyor-base')
module.exports = class AppVeyorJobBuild extends AppVeyorBase {
static get route() {

View File

@@ -2,8 +2,8 @@
const { expect } = require('chai')
const { test, given } = require('sazerac')
const AppveyorJobBuild = require('./appveyor-job-build.service')
const { NotFound } = require('..')
const AppveyorJobBuild = require('./appveyor-job-build.service')
describe('AppveyorJobBuild', function() {
test(AppveyorJobBuild.prototype.transform, () => {

View File

@@ -2,8 +2,8 @@
const Joi = require('@hapi/joi')
const { renderBuildStatusBadge } = require('../build-status')
const { keywords, fetch } = require('./azure-devops-helpers')
const { BaseSvgScrapingService, NotFound } = require('..')
const { keywords, fetch } = require('./azure-devops-helpers')
const queryParamSchema = Joi.object({
stage: Joi.string(),

View File

@@ -1,8 +1,8 @@
'use strict'
const { renderBuildStatusBadge } = require('../build-status')
const { keywords, fetch } = require('./azure-devops-helpers')
const { BaseSvgScrapingService } = require('..')
const { keywords, fetch } = require('./azure-devops-helpers')
const documentation = `
<p>

View File

@@ -2,8 +2,8 @@
const Joi = require('@hapi/joi')
const { renderVersionBadge } = require('../version')
const BaseBowerService = require('./bower-base')
const { InvalidResponse, redirector } = require('..')
const BaseBowerService = require('./bower-base')
const queryParamSchema = Joi.object({
include_prereleases: Joi.equal(''),

View File

@@ -2,8 +2,8 @@
const { metric } = require('../text-formatters')
const { downloadCount } = require('../color-formatters')
const BaseChromeWebStoreService = require('./chrome-web-store-base')
const { redirector, NotFound } = require('..')
const BaseChromeWebStoreService = require('./chrome-web-store-base')
class ChromeWebStoreUsers extends BaseChromeWebStoreService {
static get category() {

View File

@@ -2,8 +2,8 @@
const Joi = require('@hapi/joi')
const { version: versionColor } = require('../color-formatters')
const { BaseClojarsService } = require('./clojars-base')
const { redirector } = require('..')
const { BaseClojarsService } = require('./clojars-base')
const queryParamSchema = Joi.object({
include_prereleases: Joi.equal(''),

View File

@@ -1,8 +1,8 @@
'use strict'
const Joi = require('@hapi/joi')
const { codacyGrade } = require('./codacy-helpers')
const { BaseSvgScrapingService } = require('..')
const { codacyGrade } = require('./codacy-helpers')
const schema = Joi.object({ message: codacyGrade }).required()

View File

@@ -3,8 +3,8 @@
const Joi = require('@hapi/joi')
const { colorScale, letterScore } = require('../color-formatters')
const { nonNegativeInteger } = require('../validators')
const { keywords, isLetterGrade, fetchRepo } = require('./codeclimate-common')
const { BaseJsonService, NotFound } = require('..')
const { keywords, isLetterGrade, fetchRepo } = require('./codeclimate-common')
const schema = Joi.object({
data: Joi.object({

View File

@@ -4,6 +4,9 @@ const Joi = require('@hapi/joi')
const { isIntegerPercentage } = require('../test-validators')
const t = (module.exports = require('../tester').createServiceTester())
// Examples for this service can be found through the explore page:
// https://codeclimate.com/explore
t.create('issues count')
.get('/issues/angular/angular.json')
.expectBadge({

View File

@@ -2,8 +2,8 @@
const Joi = require('@hapi/joi')
const { coveragePercentage, letterScore } = require('../color-formatters')
const { keywords, isLetterGrade, fetchRepo } = require('./codeclimate-common')
const { BaseJsonService, NotFound } = require('..')
const { keywords, isLetterGrade, fetchRepo } = require('./codeclimate-common')
const schema = Joi.object({
data: Joi.object({

View File

@@ -4,15 +4,18 @@ const Joi = require('@hapi/joi')
const { isIntegerPercentage } = require('../test-validators')
const t = (module.exports = require('../tester').createServiceTester())
// Examples for this service can be found through the explore page:
// https://codeclimate.com/explore
t.create('test coverage percentage')
.get('/coverage/jekyll/jekyll.json')
.get('/coverage/codeclimate/minidoc.json')
.expectBadge({
label: 'coverage',
message: isIntegerPercentage,
})
t.create('test coverage letter')
.get('/coverage-letter/jekyll/jekyll.json')
.get('/coverage-letter/codeclimate/minidoc.json')
.expectBadge({
label: 'coverage',
message: Joi.equal('A', 'B', 'C', 'D', 'E', 'F'),

View File

@@ -1,8 +1,8 @@
'use strict'
const Joi = require('@hapi/joi')
const { isValidGrade, gradeColor } = require('./codefactor-helpers')
const { BaseSvgScrapingService } = require('..')
const { isValidGrade, gradeColor } = require('./codefactor-helpers')
const schema = Joi.object({
message: isValidGrade,

View File

@@ -0,0 +1,59 @@
'use strict'
const Joi = require('@hapi/joi')
const { renderLicenseBadge } = require('../licenses')
const toArray = require('../../core/base-service/to-array')
const BaseCondaService = require('./conda-base')
const schema = Joi.object({
license: Joi.string().required(),
}).required()
module.exports = class CondaLicense extends BaseCondaService {
static get category() {
return 'license'
}
static get route() {
return {
base: 'conda',
pattern: 'l/:channel/:pkg',
}
}
static get examples() {
return [
{
title: 'Conda - License',
pattern: 'l/:channel/:package',
namedParams: {
channel: 'conda-forge',
package: 'setuptools',
},
staticPreview: this.render({
variant: 'l',
channel: 'conda-forge',
licenses: ['MIT'],
}),
},
]
}
static get defaultBadgeData() {
return { label: 'license' }
}
static render({ licenses }) {
return renderLicenseBadge({ licenses })
}
async handle({ channel, pkg }) {
const json = await this._requestJson({
schema,
url: `https://api.anaconda.org/package/${channel}/${pkg}`,
})
return this.constructor.render({
licenses: toArray(json.license),
})
}
}

View File

@@ -0,0 +1,11 @@
'use strict'
const t = (module.exports = require('../tester').createServiceTester())
t.create('license')
.get('/l/conda-forge/setuptools.json')
.expectBadge({ label: 'license', message: 'MIT', color: 'green' })
t.create('license (invalid)')
.get('/l/conda-forge/some-bogus-package-that-never-exists.json')
.expectBadge({ label: 'license', message: 'not found', color: 'red' })

View File

@@ -2,8 +2,8 @@
const { downloadCount: downloadCountColor } = require('../color-formatters')
const { metric } = require('../text-formatters')
const { BaseCratesService, keywords } = require('./crates-base')
const { InvalidParameter, NotFound } = require('..')
const { BaseCratesService, keywords } = require('./crates-base')
module.exports = class CratesDownloads extends BaseCratesService {
static get category() {

View File

@@ -1,8 +1,8 @@
'use strict'
const { renderVersionBadge } = require('../version')
const { BaseCratesService, keywords } = require('./crates-base')
const { InvalidResponse } = require('..')
const { BaseCratesService, keywords } = require('./crates-base')
module.exports = class CratesVersion extends BaseCratesService {
static get category() {

View File

@@ -2,8 +2,8 @@
const { test, given } = require('sazerac')
const { expect } = require('chai')
const CratesVersion = require('./crates-version.service')
const { InvalidResponse } = require('..')
const CratesVersion = require('./crates-version.service')
describe('CratesVersion', function() {
test(CratesVersion.prototype.transform, () => {

View File

@@ -1,12 +1,12 @@
'use strict'
const Joi = require('@hapi/joi')
const { BaseJsonService } = require('..')
const {
dockerBlue,
buildDockerUrl,
getDockerHubUser,
} = require('./docker-helpers')
const { BaseJsonService } = require('..')
const automatedBuildSchema = Joi.object({
is_automated: Joi.boolean().required(),

View File

@@ -2,12 +2,12 @@
const Joi = require('@hapi/joi')
const { anyInteger } = require('../validators')
const { BaseJsonService } = require('..')
const {
dockerBlue,
buildDockerUrl,
getDockerHubUser,
} = require('./docker-helpers')
const { BaseJsonService } = require('..')
const buildSchema = Joi.object({
results: Joi.array()

View File

@@ -1,8 +1,8 @@
'use strict'
const { BaseJsonService } = require('..')
const { dockerBlue, buildDockerUrl } = require('./docker-helpers')
const { fetchBuild } = require('./docker-cloud-common-fetch')
const { BaseJsonService } = require('..')
module.exports = class DockerCloudAutomatedBuild extends BaseJsonService {
static get category() {

View File

@@ -1,8 +1,8 @@
'use strict'
const { BaseJsonService } = require('..')
const { dockerBlue, buildDockerUrl } = require('./docker-helpers')
const { fetchBuild } = require('./docker-cloud-common-fetch')
const { BaseJsonService } = require('..')
module.exports = class DockerCloudBuild extends BaseJsonService {
static get category() {

View File

@@ -3,12 +3,12 @@
const Joi = require('@hapi/joi')
const { metric } = require('../text-formatters')
const { nonNegativeInteger } = require('../validators')
const { BaseJsonService } = require('..')
const {
dockerBlue,
buildDockerUrl,
getDockerHubUser,
} = require('./docker-helpers')
const { BaseJsonService } = require('..')
const pullsSchema = Joi.object({
pull_count: nonNegativeInteger,

View File

@@ -4,13 +4,12 @@ const Joi = require('@hapi/joi')
const prettyBytes = require('pretty-bytes')
const { nonNegativeInteger } = require('../validators')
const { latest } = require('../version')
const { BaseJsonService, NotFound } = require('..')
const {
buildDockerUrl,
getDockerHubUser,
getMultiPageData,
} = require('./docker-helpers')
const { NotFound } = require('..')
const { BaseJsonService } = require('..')
const buildSchema = Joi.object({
name: Joi.string().required(),

View File

@@ -2,12 +2,12 @@
const { metric } = require('../text-formatters')
const { nonNegativeInteger } = require('../validators')
const { BaseService } = require('..')
const {
dockerBlue,
buildDockerUrl,
getDockerHubUser,
} = require('./docker-helpers')
const { BaseService } = require('..')
module.exports = class DockerStars extends BaseService {
static get category() {

View File

@@ -3,14 +3,13 @@
const Joi = require('@hapi/joi')
const { nonNegativeInteger } = require('../validators')
const { latest, renderVersionBadge } = require('../version')
const { BaseJsonService, NotFound, InvalidResponse } = require('..')
const {
buildDockerUrl,
getDockerHubUser,
getMultiPageData,
getDigestSemVerMatches,
} = require('./docker-helpers')
const { NotFound, InvalidResponse } = require('..')
const { BaseJsonService } = require('..')
const buildSchema = Joi.object({
count: nonNegativeInteger.required(),
@@ -97,14 +96,15 @@ module.exports = class DockerVersion extends BaseJsonService {
if (version !== 'latest') {
return { version }
}
if (Object.keys(data.results[0].images).length === 0) {
const imageTag = data.results[0].images.find(
i => i.architecture === 'amd64'
) // Digest is the unique field that we utilise to match images
if (!imageTag) {
throw new InvalidResponse({
prettyMessage: 'digest not found for latest tag',
})
}
const { digest } = data.results[0].images.find(
i => i.architecture === 'amd64'
) // Digest is the unique field that we utilise to match images
const { digest } = imageTag
return { version: getDigestSemVerMatches({ data: pagedData, digest }) }
} else if (!tag && sort === 'semver') {
const matches = data.map(d => d.name)

View File

@@ -1,6 +1,8 @@
'use strict'
const { expect } = require('chai')
const { test, given } = require('sazerac')
const { InvalidResponse } = require('..')
const DockerVersion = require('./docker-version.service')
const {
versionDataNoTagDateSort,
@@ -47,4 +49,33 @@ describe('DockerVersion', function() {
version: '3.10.4',
})
})
it('throws InvalidResponse error with latest tag and no amd64 architecture digests', function() {
expect(() => {
DockerVersion.prototype.transform({
sort: 'date',
data: {
results: [
{
name: 'latest',
images: [
{
architecture: 'arm64',
digest:
'sha256:597bd5c319cc09d6bb295b4ef23cac50ec7c373fff5fe923cfd246ec09967b31',
},
{
architecture: 'arm',
digest:
'sha256:c5ea49127cd44d0f50eafda229a056bb83b6e691883c56fd863d42675fae3909',
},
],
},
],
},
})
})
.to.throw(InvalidResponse)
.with.property('prettyMessage', 'digest not found for latest tag')
})
})

View File

@@ -1,9 +1,9 @@
'use strict'
const { MetricNames } = require('../../core/base-service/metric-helper')
const { BaseJsonService } = require('..')
const { createRoute } = require('./dynamic-helpers')
const jsonPath = require('./json-path')
const { BaseJsonService } = require('..')
module.exports = class DynamicJson extends jsonPath(BaseJsonService) {
static get enabledMetrics() {

View File

@@ -4,8 +4,8 @@ const { DOMParser } = require('xmldom')
const xpath = require('xpath')
const { MetricNames } = require('../../core/base-service/metric-helper')
const { renderDynamicBadge, errorMessages } = require('../dynamic-common')
const { createRoute } = require('./dynamic-helpers')
const { BaseService, InvalidResponse, InvalidParameter } = require('..')
const { createRoute } = require('./dynamic-helpers')
// This service extends BaseService because it uses a different XML parser
// than BaseXmlService which can be used with xpath.

View File

@@ -4,8 +4,8 @@ const { expect } = require('chai')
const sinon = require('sinon')
const xpath = require('xpath')
const { test, given } = require('sazerac')
const DynamicXml = require('./dynamic-xml.service')
const { InvalidResponse } = require('..')
const DynamicXml = require('./dynamic-xml.service')
const exampleXml = `<?xml version="1.0"?>
<catalog>

View File

@@ -153,7 +153,7 @@ t.create('XPath parse error')
t.create('XML from url | invalid url')
.get(
'.json?url=https://github.com/badges/shields/raw/master/notafile.xml&query=//version'
'.json?url=https://raw.githubusercontent.com/badges/shields/master/notafile.xml&query=//version'
)
.expectBadge({
label: 'custom badge',

View File

@@ -1,9 +1,9 @@
'use strict'
const { MetricNames } = require('../../core/base-service/metric-helper')
const { BaseYamlService } = require('..')
const { createRoute } = require('./dynamic-helpers')
const jsonPath = require('./json-path')
const { BaseYamlService } = require('..')
module.exports = class DynamicYaml extends jsonPath(BaseYamlService) {
static get enabledMetrics() {

View File

@@ -2,9 +2,8 @@
const gql = require('graphql-tag')
const { mergeQueries } = require('../../core/base-service/graphql')
const { BaseGraphqlService, BaseJsonService } = require('..')
const { staticAuthConfigured } = require('./github-helpers')
const { BaseJsonService } = require('..')
const { BaseGraphqlService } = require('..')
function createRequestFetcher(context, config) {
const { sendAndCacheRequestWithCallbacks, githubApiProvider } = context

View File

@@ -1,9 +1,9 @@
'use strict'
const Joi = require('@hapi/joi')
const { NotFound, InvalidParameter } = require('..')
const { GithubAuthV3Service } = require('./github-auth-service')
const { documentation, errorMessagesFor } = require('./github-helpers')
const { NotFound, InvalidParameter } = require('..')
const schema = Joi.object({
// https://stackoverflow.com/a/23969867/893113

View File

@@ -1,8 +1,8 @@
'use strict'
const Joi = require('@hapi/joi')
const { errorMessagesFor } = require('./github-helpers')
const { InvalidResponse } = require('..')
const { errorMessagesFor } = require('./github-helpers')
const issueSchema = Joi.object({
head: Joi.object({

View File

@@ -2,8 +2,8 @@
const Joi = require('@hapi/joi')
const { latest } = require('../version')
const { errorMessagesFor } = require('./github-helpers')
const { NotFound } = require('..')
const { errorMessagesFor } = require('./github-helpers')
const releaseInfoSchema = Joi.object({
tag_name: Joi.string().required(),

View File

@@ -2,9 +2,9 @@
const gql = require('graphql-tag')
const Joi = require('@hapi/joi')
const { NotFound } = require('..')
const { GithubAuthV4Service } = require('./github-auth-service')
const { documentation, transformErrors } = require('./github-helpers')
const { NotFound } = require('..')
const greenStates = ['SUCCESS']
const redStates = ['ERROR', 'FAILURE']

View File

@@ -4,9 +4,9 @@ const Joi = require('@hapi/joi')
const { metric } = require('../text-formatters')
const { nonNegativeInteger } = require('../validators')
const { downloadCount: downloadCountColor } = require('../color-formatters')
const { NotFound } = require('..')
const { GithubAuthV3Service } = require('./github-auth-service')
const { documentation, errorMessagesFor } = require('./github-helpers')
const { NotFound } = require('..')
const releaseSchema = Joi.object({
assets: Joi.array()

View File

@@ -2,10 +2,10 @@
const Joi = require('@hapi/joi')
const { renderVersionBadge } = require('../version')
const { InvalidResponse } = require('..')
const { ConditionalGithubAuthV3Service } = require('./github-auth-service')
const { fetchRepoContent } = require('./github-common-fetch')
const { documentation } = require('./github-helpers')
const { InvalidResponse } = require('..')
const queryParamSchema = Joi.object({
filename: Joi.string(),

View File

@@ -4,6 +4,7 @@ const Joi = require('@hapi/joi')
const { nonNegativeInteger } = require('../validators')
const { formatDate, metric } = require('../text-formatters')
const { age } = require('../color-formatters')
const { InvalidResponse } = require('..')
const { GithubAuthV3Service } = require('./github-auth-service')
const {
documentation,
@@ -11,7 +12,6 @@ const {
stateColor,
commentsColor,
} = require('./github-helpers')
const { InvalidResponse } = require('..')
const commonSchemaFields = {
number: nonNegativeInteger,

View File

@@ -4,9 +4,9 @@ const { expect } = require('chai')
const { test, given } = require('sazerac')
const { age } = require('../color-formatters')
const { formatDate, metric } = require('../text-formatters')
const { InvalidResponse } = require('..')
const GithubIssueDetail = require('./github-issue-detail.service')
const { stateColor, commentsColor } = require('./github-helpers')
const { InvalidResponse } = require('..')
describe('GithubIssueDetail', function() {
test(GithubIssueDetail.render, () => {

View File

@@ -3,10 +3,10 @@
const { renderVersionBadge } = require('../version')
const { isLockfile, getDependencyVersion } = require('../pipenv-helpers')
const { addv } = require('../text-formatters')
const { NotFound } = require('..')
const { ConditionalGithubAuthV3Service } = require('./github-auth-service')
const { fetchJsonFromRepo } = require('./github-common-fetch')
const { documentation: githubDocumentation } = require('./github-helpers')
const { NotFound } = require('..')
const keywords = ['pipfile']

View File

@@ -2,13 +2,13 @@
const { addv } = require('../text-formatters')
const { version: versionColor } = require('../color-formatters')
const { redirector } = require('..')
const { GithubAuthV3Service } = require('./github-auth-service')
const {
fetchLatestRelease,
queryParamSchema,
} = require('./github-common-release')
const { documentation } = require('./github-helpers')
const { redirector } = require('..')
class GithubRelease extends GithubAuthV3Service {
static get category() {

View File

@@ -3,9 +3,9 @@
const Joi = require('@hapi/joi')
const prettyBytes = require('pretty-bytes')
const { nonNegativeInteger } = require('../validators')
const { NotFound } = require('..')
const { GithubAuthV3Service } = require('./github-auth-service')
const { documentation, errorMessagesFor } = require('./github-helpers')
const { NotFound } = require('..')
const schema = Joi.alternatives(
Joi.object({

View File

@@ -5,10 +5,10 @@ const Joi = require('@hapi/joi')
const { addv } = require('../text-formatters')
const { version: versionColor } = require('../color-formatters')
const { latest } = require('../version')
const { NotFound, redirector } = require('..')
const { GithubAuthV4Service } = require('./github-auth-service')
const { queryParamSchema } = require('./github-common-release')
const { documentation, transformErrors } = require('./github-helpers')
const { NotFound, redirector } = require('..')
const schema = Joi.object({
data: Joi.object({

View File

@@ -2,8 +2,8 @@
const Joi = require('@hapi/joi')
const { isBuildStatus, renderBuildStatusBadge } = require('../build-status')
const { documentation } = require('./github-helpers')
const { BaseSvgScrapingService } = require('..')
const { documentation } = require('./github-helpers')
const schema = Joi.object({
message: Joi.alternatives()

View File

@@ -1,7 +1,7 @@
'use strict'
const { buildRedirectUrl } = require('./jenkins-common')
const { redirector } = require('..')
const { buildRedirectUrl } = require('./jenkins-common')
const commonProps = {
category: 'build',

View File

@@ -1,7 +1,7 @@
'use strict'
const { buildRedirectUrl } = require('./jenkins-common')
const { redirector } = require('..')
const { buildRedirectUrl } = require('./jenkins-common')
const commonProps = {
category: 'coverage',

View File

@@ -1,7 +1,7 @@
'use strict'
const { buildRedirectUrl } = require('./jenkins-common')
const { redirector } = require('..')
const { buildRedirectUrl } = require('./jenkins-common')
const commonProps = {
category: 'build',

View File

@@ -7,13 +7,13 @@ const {
renderTestResultBadge,
} = require('../test-results')
const { optionalNonNegativeInteger } = require('../validators')
const { InvalidResponse } = require('..')
const JenkinsBase = require('./jenkins-base')
const {
buildTreeParamQueryString,
buildUrl,
queryParamSchema,
} = require('./jenkins-common')
const { InvalidResponse } = require('..')
// In the API response, the `actions` array can be empty, and when it is not empty it will contain a
// mix of objects. Some will be empty objects, and several will not have the test count properties.

View File

@@ -0,0 +1,86 @@
'use strict'
const Joi = require('@hapi/joi')
const { starRating } = require('../text-formatters')
const { colorScale } = require('../color-formatters')
const JetbrainsBase = require('./jetbrains-base')
const pluginRatingColor = colorScale([2, 3, 4])
const schema = Joi.object({
'plugin-repository': Joi.object({
category: Joi.object({
'idea-plugin': Joi.array()
.min(1)
.items(
Joi.object({
rating: Joi.string().required(),
})
)
.single()
.required(),
}),
}).required(),
}).required()
module.exports = class JetbrainsRating extends JetbrainsBase {
static get category() {
return 'rating'
}
static get route() {
return {
base: 'jetbrains/plugin/r',
pattern: ':format(rating|stars)/:pluginId',
}
}
static get examples() {
return [
{
title: 'JetBrains IntelliJ Plugins',
pattern: 'rating/:pluginId',
namedParams: {
pluginId: '11941-automatic-power-saver',
},
staticPreview: this.render({
rating: '4.5',
format: 'rating',
}),
},
{
title: 'JetBrains IntelliJ Plugins',
pattern: 'stars/:pluginId',
namedParams: {
pluginId: '11941-automatic-power-saver',
},
staticPreview: this.render({
rating: '4.5',
format: 'stars',
}),
},
]
}
static get defaultBadgeData() {
return { label: 'rating' }
}
static render({ rating, format }) {
const message =
format === 'rating'
? `${+parseFloat(rating).toFixed(1)}/5`
: starRating(rating)
return {
message,
color: pluginRatingColor(rating),
}
}
async handle({ format, pluginId }) {
const pluginData = await this.fetchPackageData({ pluginId, schema })
const pluginRating =
pluginData['plugin-repository'].category['idea-plugin'][0].rating
return this.constructor.render({ rating: pluginRating, format })
}
}

View File

@@ -0,0 +1,84 @@
'use strict'
const { withRegex, isStarRating } = require('../test-validators')
const t = (module.exports = require('../tester').createServiceTester())
const isRating = withRegex(/^(([0-4](.?([0-9]))?)|5)\/5$/)
t.create('rating number (user friendly plugin id)')
.get('/rating/11941-automatic-power-saver.json')
.expectBadge({ label: 'rating', message: isRating })
t.create('rating number (plugin id from plugin.xml)')
.get('/rating/com.chriscarini.jetbrains.jetbrains-auto-power-saver.json')
.expectBadge({ label: 'rating', message: isRating })
t.create('rating number (number as a plugin id)')
.get('/rating/11941.json')
.expectBadge({ label: 'rating', message: isRating })
t.create('rating number for unknown plugin')
.get('/rating/unknown-plugin.json')
.expectBadge({ label: 'rating', message: 'not found' })
t.create('rating stars (user friendly plugin id)')
.get('/stars/11941-automatic-power-saver.json')
.expectBadge({ label: 'rating', message: isStarRating })
t.create('rating stars (plugin id from plugin.xml)')
.get('/stars/com.chriscarini.jetbrains.jetbrains-auto-power-saver.json')
.expectBadge({ label: 'rating', message: isStarRating })
t.create('rating stars (number as a plugin id)')
.get('/stars/11941.json')
.expectBadge({ label: 'rating', message: isStarRating })
t.create('rating stars for unknown plugin')
.get('/stars/unknown-plugin.json')
.expectBadge({ label: 'rating', message: 'not found' })
t.create('rating number')
.get('/rating/11941.json')
.intercept(
nock =>
nock('https://plugins.jetbrains.com')
.get('/plugins/list?pluginId=11941')
.reply(
200,
`<?xml version="1.0" encoding="UTF-8"?>
<plugin-repository>
<category name="User Interface">
<idea-plugin downloads="1714" size="208537" date="1586449109000" updatedDate="1586449109000" url="">
<rating>4.5</rating>
</idea-plugin>
</category>
</plugin-repository>`
),
{
'Content-Type': 'text/xml;charset=UTF-8',
}
)
.expectBadge({ label: 'rating', message: '4.5/5' })
t.create('rating stars')
.get('/stars/11941.json')
.intercept(
nock =>
nock('https://plugins.jetbrains.com')
.get('/plugins/list?pluginId=11941')
.reply(
200,
`<?xml version="1.0" encoding="UTF-8"?>
<plugin-repository>
<category name="User Interface">
<idea-plugin downloads="1714" size="208537" date="1586449109000" updatedDate="1586449109000" url="">
<rating>4.5</rating>
</idea-plugin>
</category>
</plugin-repository>`
),
{
'Content-Type': 'text/xml;charset=UTF-8',
}
)
.expectBadge({ label: 'rating', message: '★★★★½' })

View File

@@ -2,8 +2,8 @@
const Joi = require('@hapi/joi')
const { optionalUrl } = require('../validators')
const { authConfig } = require('./jira-common')
const { BaseJsonService } = require('..')
const { authConfig } = require('./jira-common')
const queryParamSchema = Joi.object({
baseUrl: optionalUrl.required(),

View File

@@ -2,8 +2,8 @@
const Joi = require('@hapi/joi')
const { optionalUrl } = require('../validators')
const { authConfig } = require('./jira-common')
const { BaseJsonService } = require('..')
const { authConfig } = require('./jira-common')
const queryParamSchema = Joi.object({
baseUrl: optionalUrl.required(),

View File

@@ -1,83 +1,13 @@
'use strict'
const Joi = require('@hapi/joi')
const { nonNegativeInteger } = require('../validators')
const { downloadCount } = require('../color-formatters')
const { metric } = require('../text-formatters')
const { BaseJsonService } = require('..')
const { deprecatedService } = require('..')
const schema = Joi.object({
week: nonNegativeInteger,
month: nonNegativeInteger,
}).required()
const intervalMap = {
dw: {
api_field: 'week',
suffix: '/week',
module.exports = deprecatedService({
route: {
base: 'jitpack',
pattern: ':interval(dw|dm)/:various*',
},
dm: {
api_field: 'month',
suffix: '/month',
},
}
module.exports = class JitpackDownloads extends BaseJsonService {
static get category() {
return 'downloads'
}
static get route() {
return {
base: 'jitpack',
pattern:
':interval(dw|dm)/:vcs(github|bitbucket|gitlab|gitee)/:user/:repo',
}
}
static get examples() {
return [
{
title: 'JitPack - Downloads',
namedParams: {
interval: 'dm',
vcs: 'github',
user: 'jitpack',
repo: 'maven-simple',
},
staticPreview: JitpackDownloads.render({
downloads: 14000,
interval: 'dm',
}),
keywords: ['java', 'maven'],
},
]
}
static get defaultBadgeData() {
return { label: 'downloads' }
}
static render({ downloads, interval }) {
return {
message: `${metric(downloads)}${intervalMap[interval].suffix}`,
color: downloadCount(downloads),
}
}
async fetch({ vcs, user, repo }) {
return this._requestJson({
schema,
url: `https://jitpack.io/api/downloads/com.${vcs}.${user}/${repo}`,
errorMessages: { 401: 'project not found or private' },
})
}
async handle({ interval, vcs, user, repo }) {
const json = await this.fetch({ vcs, user, repo })
return this.constructor.render({
downloads: json[intervalMap[interval].api_field],
interval,
})
}
}
label: 'jitpack',
category: 'downloads',
dateAdded: new Date('2020-04-05'),
})

View File

@@ -1,22 +1,23 @@
'use strict'
const t = (module.exports = require('../tester').createServiceTester())
const { isMetricOverTimePeriod } = require('../test-validators')
const { ServiceTester } = require('../tester')
t.create('weekly (github)')
const t = (module.exports = new ServiceTester({
id: 'JitPackDownloads',
title: 'JitPackDownloads',
pathPrefix: '/jitpack',
}))
t.create('no longer available (dw)')
.get('/dw/github/jitpack/maven-simple.json')
.timeout(10000)
.expectBadge({ label: 'downloads', message: isMetricOverTimePeriod })
t.create('monthly (github)')
.get('/dm/github/dcendents/android-maven-gradle-plugin.json')
.timeout(10000)
.expectBadge({ label: 'downloads', message: isMetricOverTimePeriod })
t.create('unknown package (github)')
.get('/dw/github/some-bogus-user/super-fake-project.json')
.timeout(10000)
.expectBadge({
label: 'downloads',
message: '0/week',
label: 'jitpack',
message: 'no longer available',
})
t.create('no longer available (dm)')
.get('/dm/github/jitpack/maven-simple.json')
.expectBadge({
label: 'jitpack',
message: 'no longer available',
})

View File

@@ -1,7 +1,7 @@
'use strict'
const { renderCurrencyBadge, LiberapayBase } = require('./liberapay-base')
const { InvalidResponse } = require('..')
const { renderCurrencyBadge, LiberapayBase } = require('./liberapay-base')
module.exports = class LiberapayGives extends LiberapayBase {
static get route() {

View File

@@ -1,8 +1,8 @@
'use strict'
const { colorScale } = require('../color-formatters')
const { LiberapayBase } = require('./liberapay-base')
const { InvalidResponse } = require('..')
const { LiberapayBase } = require('./liberapay-base')
module.exports = class LiberapayGoal extends LiberapayBase {
static get route() {

View File

@@ -2,8 +2,8 @@
const { expect } = require('chai')
const { test, given } = require('sazerac')
const LiberapayGoal = require('./liberapay-goal.service')
const { InvalidResponse } = require('..')
const LiberapayGoal = require('./liberapay-goal.service')
describe('LiberapayGoal', function() {
test(LiberapayGoal.prototype.transform, () => {

View File

@@ -1,7 +1,7 @@
'use strict'
const { renderCurrencyBadge, LiberapayBase } = require('./liberapay-base')
const { InvalidResponse } = require('..')
const { renderCurrencyBadge, LiberapayBase } = require('./liberapay-base')
module.exports = class LiberapayReceives extends LiberapayBase {
static get route() {

View File

@@ -1,11 +1,11 @@
'use strict'
const Joi = require('@hapi/joi')
const { BaseJsonService } = require('..')
const {
transform,
renderDependenciesBadge,
} = require('./librariesio-dependencies-helpers')
const { BaseJsonService } = require('..')
const schema = Joi.object({
dependencies: Joi.array()

View File

@@ -1,8 +1,8 @@
'use strict'
const { metric } = require('../text-formatters')
const { fetchProject } = require('./librariesio-common')
const { BaseJsonService } = require('..')
const { fetchProject } = require('./librariesio-common')
// https://libraries.io/api#project-dependent-repositories
module.exports = class LibrariesIoDependentRepos extends BaseJsonService {

View File

@@ -1,8 +1,8 @@
'use strict'
const { metric } = require('../text-formatters')
const { fetchProject } = require('./librariesio-common')
const { BaseJsonService } = require('..')
const { fetchProject } = require('./librariesio-common')
// https://libraries.io/api#project-dependents
module.exports = class LibrariesIoDependents extends BaseJsonService {

View File

@@ -1,8 +1,8 @@
'use strict'
const { colorScale } = require('../color-formatters')
const { fetchProject } = require('./librariesio-common')
const { BaseJsonService } = require('..')
const { fetchProject } = require('./librariesio-common')
const sourceRankColor = colorScale([10, 15, 20, 25, 30])

View File

@@ -2,8 +2,8 @@
const Joi = require('@hapi/joi')
const { addv } = require('../text-formatters')
const { latestVersion } = require('./luarocks-version-helpers')
const { BaseJsonService, NotFound } = require('..')
const { latestVersion } = require('./luarocks-version-helpers')
const schema = Joi.object({
repository: Joi.object()

View File

@@ -1,8 +1,8 @@
'use strict'
const prettyBytes = require('pretty-bytes')
const BaseMicrobadgerService = require('./microbadger-base')
const { NotFound } = require('..')
const BaseMicrobadgerService = require('./microbadger-base')
const documentation = `
<p>

View File

@@ -7,8 +7,8 @@ const { optionalUrl } = require('../validators')
const {
optionalDottedVersionNClausesWithOptionalSuffix,
} = require('../validators')
const { isSnapshotVersion } = require('./nexus-version')
const { BaseJsonService, InvalidResponse, NotFound } = require('..')
const { isSnapshotVersion } = require('./nexus-version')
const nexus2SearchApiSchema = Joi.object({
data: Joi.array()

View File

@@ -3,8 +3,8 @@
const { expect } = require('chai')
const nock = require('nock')
const { cleanUpNockAfterEach, defaultContext } = require('../test-helpers')
const Nexus = require('./nexus.service')
const { InvalidResponse, NotFound } = require('..')
const Nexus = require('./nexus.service')
describe('Nexus', function() {
context('transform2()', function() {

View File

@@ -264,7 +264,7 @@ t.create('Nexus 3 - search snapshot version for artifact without snapshots')
t.create('Nexus 3 - repository version')
.get(
'/proxy-public-3rd-party-release/com.fasterxml.jackson.core/jackson-databind.json?server=https://nexus.pentaho.org&nexusVersion=3'
'/proxy-public-3rd-party-release/com.h2database/h2.json?server=https://nexus.pentaho.org&nexusVersion=3'
)
.expectBadge({
label: 'nexus',
@@ -276,7 +276,7 @@ t.create(
)
.timeout(15000)
.get(
'/proxy-public-3rd-party-release/com.fasterxml.jackson.core/jackson-databind.json?server=https://nexus.pentaho.org'
'/proxy-public-3rd-party-release/com.h2database/h2.json?server=https://nexus.pentaho.org'
)
.expectBadge({
label: 'nexus',

View File

@@ -2,8 +2,8 @@
const Joi = require('@hapi/joi')
const { renderVersionBadge } = require('../version')
const NpmBase = require('./npm-base')
const { NotFound } = require('..')
const NpmBase = require('./npm-base')
const keywords = ['node']

View File

@@ -2,12 +2,12 @@
const Joi = require('@hapi/joi')
const { nonNegativeInteger } = require('../validators')
const { BaseJsonService, BaseXmlService, NotFound, redirector } = require('..')
const {
renderVersionBadge,
renderDownloadBadge,
odataToObject,
} = require('./nuget-helpers')
const { BaseJsonService, BaseXmlService, NotFound, redirector } = require('..')
function createFilter({ packageName, includePrereleases }) {
const releaseTypeFilter = includePrereleases

View File

@@ -5,8 +5,8 @@ const Joi = require('@hapi/joi')
const semver = require('semver')
const { regularUpdate } = require('../../core/legacy/regular-update')
const RouteBuilder = require('../route-builder')
const { renderVersionBadge, renderDownloadBadge } = require('./nuget-helpers')
const { BaseJsonService, NotFound } = require('..')
const { renderVersionBadge, renderDownloadBadge } = require('./nuget-helpers')
/*
* Build the Shields service URL object for the given service configuration. Return

View File

@@ -0,0 +1,61 @@
'use strict'
const Joi = require('@hapi/joi')
const { metric } = require('../text-formatters')
const { floorCount } = require('../color-formatters')
const { BaseJsonService } = require('..')
const apiSchema = Joi.object({
total: Joi.number()
.positive()
.required(),
}).required()
module.exports = class OffsetEarthCarbonOffset extends BaseJsonService {
static get category() {
return 'other'
}
static get route() {
return {
base: 'offset-earth/carbon',
pattern: ':username',
}
}
static get examples() {
return [
{
title: 'Offset Earth (Carbon Offset)',
namedParams: { username: 'offsetearth' },
staticPreview: this.render({ count: 15.05 }),
},
]
}
static get defaultBadgeData() {
return { label: 'carbon offset' }
}
static render({ count }) {
const tonnes = metric(count)
return { message: `${tonnes} tonnes`, color: floorCount(count, 0.5, 1, 5) }
}
async fetch({ username }) {
const url = `https://public.offset.earth/users/${username}/carbon-offset`
return this._requestJson({
url,
schema: apiSchema,
errorMessages: {
404: 'username not found',
},
})
}
async handle({ username }) {
const { total } = await this.fetch({ username })
return this.constructor.render({ count: total })
}
}

View File

@@ -0,0 +1,19 @@
'use strict'
const t = (module.exports = require('../tester').createServiceTester())
const { withRegex } = require('../test-validators')
t.create('request for existing username')
.get('/offsetearth.json')
.expectBadge({
label: 'carbon offset',
message: withRegex(/[\d.]+ tonnes/),
})
t.create('invalid username')
.get('/non-existent-username.json')
.expectBadge({
label: 'carbon offset',
message: 'username not found',
color: 'red',
})

View File

@@ -0,0 +1,59 @@
'use strict'
const Joi = require('@hapi/joi')
const { metric } = require('../text-formatters')
const { floorCount } = require('../color-formatters')
const { nonNegativeInteger } = require('../validators')
const { BaseJsonService } = require('..')
const apiSchema = Joi.object({
total: nonNegativeInteger,
}).required()
module.exports = class OffsetEarthTrees extends BaseJsonService {
static get category() {
return 'other'
}
static get route() {
return {
base: 'offset-earth/trees',
pattern: ':username',
}
}
static get examples() {
return [
{
title: 'Offset Earth (Trees)',
namedParams: { username: 'offsetearth' },
staticPreview: this.render({ count: 250 }),
},
]
}
static get defaultBadgeData() {
return { label: 'trees' }
}
static render({ count }) {
return { message: metric(count), color: floorCount(count, 10, 50, 100) }
}
async fetch({ username }) {
const url = `https://public.offset.earth/users/${username}/trees`
return this._requestJson({
url,
schema: apiSchema,
errorMessages: {
404: 'username not found',
},
})
}
async handle({ username }) {
const { total } = await this.fetch({ username })
return this.constructor.render({ count: total })
}
}

View File

@@ -0,0 +1,28 @@
'use strict'
const t = (module.exports = require('../tester').createServiceTester())
const { isMetric } = require('../test-validators')
t.create('request for existing username')
.get('/offsetearth.json')
.expectBadge({
label: 'trees',
message: isMetric,
})
t.create('request for existing username')
.get('/offsetearth.json')
.intercept(nock =>
nock('https://public.offset.earth')
.get('/users/offsetearth/trees')
.reply(200, { total: 50 })
)
.expectBadge({
label: 'trees',
message: '50',
color: 'green',
})
t.create('invalid username')
.get('/non-existent-username.json')
.expectBadge({ label: 'trees', message: 'username not found', color: 'red' })

View File

@@ -3,12 +3,12 @@
const Joi = require('@hapi/joi')
const { renderLicenseBadge } = require('../licenses')
const { optionalUrl } = require('../validators')
const { NotFound } = require('..')
const {
keywords,
BasePackagistService,
customServerDocumentationFragment,
} = require('./packagist-base')
const { NotFound } = require('..')
const packageSchema = Joi.object()
.pattern(

View File

@@ -1,8 +1,8 @@
'use strict'
const { expect } = require('chai')
const PackagistLicense = require('./packagist-license.service')
const { NotFound } = require('..')
const PackagistLicense = require('./packagist-license.service')
describe('PackagistLicense', function() {
it('should throw NotFound when default branch is missing', function() {

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