Compare commits
104 Commits
server-202
...
server-202
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a0af349b5 | ||
|
|
cd91caf484 | ||
|
|
78f0e38d57 | ||
|
|
088fb17088 | ||
|
|
fb463e0bb1 | ||
|
|
c4ec53cac4 | ||
|
|
3c4d325e88 | ||
|
|
5705957cf0 | ||
|
|
3659599968 | ||
|
|
0fa54f5265 | ||
|
|
6bb495ce53 | ||
|
|
091ccfdbcd | ||
|
|
ec8d739099 | ||
|
|
303fb86200 | ||
|
|
174b1915c5 | ||
|
|
7f05d1aad9 | ||
|
|
daafaaa68a | ||
|
|
673688e4a2 | ||
|
|
1f59785e20 | ||
|
|
115d768b50 | ||
|
|
7b7c961781 | ||
|
|
95f21f0534 | ||
|
|
aeb1058c5b | ||
|
|
695c11c815 | ||
|
|
f252967d7d | ||
|
|
5ddf5399c1 | ||
|
|
1e168b71ef | ||
|
|
c2fe2ffa2e | ||
|
|
e5be1d6a1c | ||
|
|
9cfc47bba9 | ||
|
|
25d59e0809 | ||
|
|
6c0274336d | ||
|
|
3b9aa61d31 | ||
|
|
4d9247cdda | ||
|
|
4b4396254b | ||
|
|
25764ef120 | ||
|
|
28f0a44111 | ||
|
|
2e627b0218 | ||
|
|
a7368b8670 | ||
|
|
9d69da5e98 | ||
|
|
2e05bbf789 | ||
|
|
0e476d29b8 | ||
|
|
58cb398f40 | ||
|
|
abf36e4843 | ||
|
|
085e1a50b5 | ||
|
|
6c37ffb2cf | ||
|
|
4e389fa1bf | ||
|
|
34bc405f2e | ||
|
|
e325321546 | ||
|
|
af2838514e | ||
|
|
292e4e1483 | ||
|
|
ff14a116cc | ||
|
|
86b4a592d7 | ||
|
|
25d1f41824 | ||
|
|
7788f0510d | ||
|
|
19f8d6cb99 | ||
|
|
26b490b4a6 | ||
|
|
c364ba26ae | ||
|
|
2726c77b15 | ||
|
|
2da34547f1 | ||
|
|
e03d597c2c | ||
|
|
69d737cbdd | ||
|
|
1428ce5809 | ||
|
|
b510e6ec40 | ||
|
|
5b3630cad4 | ||
|
|
3c983c8ac0 | ||
|
|
026e675dcf | ||
|
|
b59e2bddf1 | ||
|
|
bcba591e8e | ||
|
|
2683e47506 | ||
|
|
d539e942fd | ||
|
|
e7046e210d | ||
|
|
b90d6aac72 | ||
|
|
e0bb07c0fc | ||
|
|
ac5fdbb8e0 | ||
|
|
4287b5ba67 | ||
|
|
9881ce5400 | ||
|
|
2407081f0f | ||
|
|
e8c42e9ec1 | ||
|
|
27a2deb7d2 | ||
|
|
7136943d6d | ||
|
|
2fb9889742 | ||
|
|
5edec9967f | ||
|
|
47d67b4b19 | ||
|
|
2afcdf4ebd | ||
|
|
91d85368bf | ||
|
|
1fe60b49e1 | ||
|
|
d6c3459803 | ||
|
|
0d4fcfe880 | ||
|
|
f67fe525c2 | ||
|
|
dad6ce554a | ||
|
|
0c73b35915 | ||
|
|
f1d151e963 | ||
|
|
a0149a8f8f | ||
|
|
e843d4eac1 | ||
|
|
830b5d8a1f | ||
|
|
8afb034a58 | ||
|
|
2f915a7b45 | ||
|
|
7dbfd0d049 | ||
|
|
493fdb76af | ||
|
|
abb1bbf8d4 | ||
|
|
a4911dac33 | ||
|
|
5e6583c530 | ||
|
|
bb1fda2aa7 |
@@ -1,110 +0,0 @@
|
||||
version: 2
|
||||
|
||||
services_steps: &services_steps
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: |
|
||||
npm ci
|
||||
environment:
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
|
||||
- run:
|
||||
name: Identify services tagged in the PR title
|
||||
command: npm run test:services:pr:prepare
|
||||
|
||||
- run:
|
||||
name: Run tests for tagged services
|
||||
environment:
|
||||
mocha_reporter: mocha-junit-reporter
|
||||
MOCHA_FILE: junit/services/results.xml
|
||||
command: RETRY_COUNT=3 npm run test:services:pr:run
|
||||
|
||||
- store_test_results:
|
||||
path: junit
|
||||
|
||||
jobs:
|
||||
danger:
|
||||
docker:
|
||||
- image: cimg/node:16.15
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: npm ci
|
||||
environment:
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
|
||||
- run:
|
||||
name: Danger
|
||||
when: always
|
||||
environment:
|
||||
# https://github.com/gatsbyjs/gatsby/pull/11555
|
||||
NODE_ENV: test
|
||||
command: npm run danger ci
|
||||
|
||||
services:
|
||||
docker:
|
||||
- image: cimg/node:16.15
|
||||
|
||||
<<: *services_steps
|
||||
|
||||
services@node-17:
|
||||
docker:
|
||||
- image: cimg/node:17.9
|
||||
environment:
|
||||
NPM_CONFIG_ENGINE_STRICT: 'false'
|
||||
|
||||
<<: *services_steps
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
|
||||
on-commit:
|
||||
jobs:
|
||||
- services:
|
||||
filters:
|
||||
branches:
|
||||
ignore:
|
||||
- master
|
||||
- gh-pages
|
||||
- services@node-17:
|
||||
filters:
|
||||
branches:
|
||||
ignore:
|
||||
- master
|
||||
- gh-pages
|
||||
- danger:
|
||||
filters:
|
||||
branches:
|
||||
ignore:
|
||||
- master
|
||||
- gh-pages
|
||||
- /dependabot\/.*/
|
||||
# on-commit-with-cache:
|
||||
# jobs:
|
||||
# - npm-install:
|
||||
# filters:
|
||||
# branches:
|
||||
# ignore: gh-pages
|
||||
# - services:
|
||||
# requires:
|
||||
# - npm-install
|
||||
# filters:
|
||||
# branches:
|
||||
# ignore: master
|
||||
# - services@node-latest:
|
||||
# requires:
|
||||
# - npm-install
|
||||
# filters:
|
||||
# branches:
|
||||
# ignore: master
|
||||
# - danger:
|
||||
# requires:
|
||||
# - npm-install
|
||||
# filters:
|
||||
# branches:
|
||||
# ignore: /dependabot\/.*/
|
||||
@@ -24,6 +24,7 @@ plugins:
|
||||
- chai-friendly
|
||||
- jsdoc
|
||||
- mocha
|
||||
- icedfrisby
|
||||
- no-extension-in-require
|
||||
- sort-class-members
|
||||
- import
|
||||
@@ -113,9 +114,16 @@ overrides:
|
||||
mocha: true
|
||||
rules:
|
||||
mocha/no-exclusive-tests: 'error'
|
||||
mocha/no-skipped-tests: 'error'
|
||||
mocha/no-mocha-arrows: 'error'
|
||||
mocha/prefer-arrow-callback: 'error'
|
||||
|
||||
- files:
|
||||
- 'services/**/*.tester.js'
|
||||
rules:
|
||||
icedfrisby/no-exclusive-tests: 'error'
|
||||
icedfrisby/no-skipped-tests: 'error'
|
||||
|
||||
rules:
|
||||
# Disable some rules from eslint:recommended.
|
||||
no-empty: ['error', { 'allowEmptyCatch': true }]
|
||||
|
||||
8
.github/actions/integration-tests/action.yml
vendored
8
.github/actions/integration-tests/action.yml
vendored
@@ -7,11 +7,19 @@ inputs:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Migrate DB
|
||||
if: always()
|
||||
run: npm run migrate up
|
||||
env:
|
||||
POSTGRES_URL: postgresql://postgres:postgres@localhost:5432/ci_test
|
||||
shell: bash
|
||||
|
||||
- name: Integration Tests
|
||||
if: always()
|
||||
run: npm run test:integration -- --reporter json --reporter-option 'output=reports/integration-tests.json'
|
||||
env:
|
||||
GH_TOKEN: '${{ inputs.github-token }}'
|
||||
POSTGRES_URL: postgresql://postgres:postgres@localhost:5432/ci_test
|
||||
shell: bash
|
||||
|
||||
- name: Write Markdown Summary
|
||||
|
||||
86
.github/actions/service-tests/action.yml
vendored
Normal file
86
.github/actions/service-tests/action.yml
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
name: 'Service tests'
|
||||
description: 'Run tests for selected services'
|
||||
inputs:
|
||||
github-token:
|
||||
description: 'The GITHUB_TOKEN secret'
|
||||
required: true
|
||||
librariesio-tokens:
|
||||
description: 'The SERVICETESTS_LIBRARIESIO_TOKENS secret'
|
||||
required: false
|
||||
default: ''
|
||||
obs-user:
|
||||
description: 'The SERVICETESTS_OBS_USER secret'
|
||||
required: false
|
||||
default: ''
|
||||
obs-pass:
|
||||
description: 'The SERVICETESTS_OBS_PASS secret'
|
||||
required: false
|
||||
default: ''
|
||||
sl-insight-user-uuid:
|
||||
description: 'The SERVICETESTS_SL_INSIGHT_USER_UUID secret'
|
||||
required: false
|
||||
default: ''
|
||||
sl-insight-api-token:
|
||||
description: 'The SERVICETESTS_SL_INSIGHT_API_TOKEN secret'
|
||||
required: false
|
||||
default: ''
|
||||
twitch-client-id:
|
||||
description: 'The SERVICETESTS_TWITCH_CLIENT_ID secret'
|
||||
required: false
|
||||
default: ''
|
||||
twitch-client-secret:
|
||||
description: 'The SERVICETESTS_TWITCH_CLIENT_SECRET secret'
|
||||
required: false
|
||||
default: ''
|
||||
wheelmap-token:
|
||||
description: 'The SERVICETESTS_WHEELMAP_TOKEN secret'
|
||||
required: false
|
||||
default: ''
|
||||
youtube-api-key:
|
||||
description: 'The SERVICETESTS_YOUTUBE_API_KEY secret'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Derive list of service tests to run
|
||||
# Note: In this step we are using an intermediate env var instead of
|
||||
# passing github.event.pull_request.title as an argument
|
||||
# to prevent a shell injection attack. Further reading:
|
||||
# https://securitylab.github.com/research/github-actions-untrusted-input/#exploitability-and-impact
|
||||
# https://securitylab.github.com/research/github-actions-untrusted-input/#remediation
|
||||
if: always()
|
||||
env:
|
||||
TITLE: ${{ github.event.pull_request.title }}
|
||||
run: npm run test:services:pr:prepare "$TITLE"
|
||||
shell: bash
|
||||
|
||||
- name: Run service tests
|
||||
if: always()
|
||||
run: npm run test:services:pr:run -- --reporter json --reporter-option 'output=reports/service-tests.json'
|
||||
shell: bash
|
||||
env:
|
||||
RETRY_COUNT: 3
|
||||
GH_TOKEN: '${{ inputs.github-token }}'
|
||||
LIBRARIESIO_TOKENS: '${{ inputs.librariesio-tokens }}'
|
||||
OBS_USER: '${{ inputs.obs-user }}'
|
||||
OBS_PASS: '${{ inputs.obs-pass }}'
|
||||
SL_INSIGHT_USER_UUID: '${{ inputs.sl-insight-user-uuid }}'
|
||||
SL_INSIGHT_API_TOKEN: '${{ inputs.sl-insight-api-token }}'
|
||||
TWITCH_CLIENT_ID: '${{ inputs.twitch-client-id }}'
|
||||
TWITCH_CLIENT_SECRET: '${{ inputs.twitch-client-secret }}'
|
||||
WHEELMAP_TOKEN: '${{ inputs.wheelmap-token }}'
|
||||
YOUTUBE_API_KEY: '${{ inputs.youtube-api-key }}'
|
||||
|
||||
- name: Write Markdown Summary
|
||||
if: always()
|
||||
run: |
|
||||
if test -f 'reports/service-tests.json'; then
|
||||
echo '# Services' >> $GITHUB_STEP_SUMMARY
|
||||
sed -e 's/^/- /' pull-request-services.log >> $GITHUB_STEP_SUMMARY
|
||||
node scripts/mocha2md.js Report reports/service-tests.json >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo 'No services found. Nothing to do.' >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
shell: bash
|
||||
4
.github/workflows/build-docker-image.yml
vendored
4
.github/workflows/build-docker-image.yml
vendored
@@ -11,12 +11,14 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
version: v0.9.1
|
||||
|
||||
- name: Set Git Short SHA
|
||||
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
|
||||
4
.github/workflows/create-release.yml
vendored
4
.github/workflows/create-release.yml
vendored
@@ -35,6 +35,8 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
version: v0.9.1
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
@@ -43,7 +45,7 @@ jobs:
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push snapshot release to DockerHub
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
|
||||
29
.github/workflows/danger.yml
vendored
Normal file
29
.github/workflows/danger.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Danger
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
|
||||
permissions:
|
||||
checks: write
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
|
||||
jobs:
|
||||
danger:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor != 'dependabot[bot]' && github.actor != 'repo-ranger[bot]'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Danger
|
||||
run: npm run danger ci
|
||||
env:
|
||||
# https://github.com/gatsbyjs/gatsby/pull/11555
|
||||
NODE_ENV: test
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
9
.github/workflows/deploy-docs.yml
vendored
9
.github/workflows/deploy-docs.yml
vendored
@@ -16,10 +16,13 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
npm ci
|
||||
npm run build-docs
|
||||
run: npm run build-docs
|
||||
|
||||
- name: Deploy
|
||||
uses: JamesIves/github-pages-deploy-action@v4
|
||||
|
||||
4
.github/workflows/publish-docker-next.yml
vendored
4
.github/workflows/publish-docker-next.yml
vendored
@@ -13,6 +13,8 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
version: v0.9.1
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
@@ -24,7 +26,7 @@ jobs:
|
||||
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
|
||||
13
.github/workflows/test-integration-17.yml
vendored
13
.github/workflows/test-integration-17.yml
vendored
@@ -23,6 +23,19 @@ jobs:
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 6379:6379
|
||||
postgres:
|
||||
image: postgres
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: ci_test
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
13
.github/workflows/test-integration.yml
vendored
13
.github/workflows/test-integration.yml
vendored
@@ -23,6 +23,19 @@ jobs:
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 6379:6379
|
||||
postgres:
|
||||
image: postgres
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: ci_test
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
40
.github/workflows/test-services-17.yml
vendored
Normal file
40
.github/workflows/test-services-17.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Services@node 17
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
|
||||
jobs:
|
||||
test-services-17:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: 17
|
||||
env:
|
||||
NPM_CONFIG_ENGINE_STRICT: 'false'
|
||||
|
||||
- name: Service tests (triggered from local branch)
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: ./.github/actions/service-tests
|
||||
with:
|
||||
github-token: '${{ secrets.GH_PAT }}'
|
||||
librariesio-tokens: '${{ secrets.SERVICETESTS_LIBRARIESIO_TOKENS }}'
|
||||
obs-user: '${{ secrets.SERVICETESTS_OBS_USER }}'
|
||||
obs-pass: '${{ secrets.SERVICETESTS_OBS_PASS }}'
|
||||
sl-insight-user-uuid: '${{ secrets.SERVICETESTS_SL_INSIGHT_USER_UUID }}'
|
||||
sl-insight-api-token: '${{ secrets.SERVICETESTS_SL_INSIGHT_API_TOKEN }}'
|
||||
twitch-client-id: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_ID }}'
|
||||
twitch-client-secret: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_SECRET }}'
|
||||
wheelmap-token: '${{ secrets.SERVICETESTS_WHEELMAP_TOKEN }}'
|
||||
youtube-api-key: '${{ secrets.SERVICETESTS_YOUTUBE_API_KEY }}'
|
||||
|
||||
- name: Service tests (triggered from fork)
|
||||
if: github.event.pull_request.head.repo.full_name != github.repository
|
||||
uses: ./.github/actions/service-tests
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
38
.github/workflows/test-services.yml
vendored
Normal file
38
.github/workflows/test-services.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Services
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
|
||||
jobs:
|
||||
test-services:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Service tests (triggered from local branch)
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: ./.github/actions/service-tests
|
||||
with:
|
||||
github-token: '${{ secrets.GH_PAT }}'
|
||||
librariesio-tokens: '${{ secrets.SERVICETESTS_LIBRARIESIO_TOKENS }}'
|
||||
obs-user: '${{ secrets.SERVICETESTS_OBS_USER }}'
|
||||
obs-pass: '${{ secrets.SERVICETESTS_OBS_PASS }}'
|
||||
sl-insight-user-uuid: '${{ secrets.SERVICETESTS_SL_INSIGHT_USER_UUID }}'
|
||||
sl-insight-api-token: '${{ secrets.SERVICETESTS_SL_INSIGHT_API_TOKEN }}'
|
||||
twitch-client-id: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_ID }}'
|
||||
twitch-client-secret: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_SECRET }}'
|
||||
wheelmap-token: '${{ secrets.SERVICETESTS_WHEELMAP_TOKEN }}'
|
||||
youtube-api-key: '${{ secrets.SERVICETESTS_YOUTUBE_API_KEY }}'
|
||||
|
||||
- name: Service tests (triggered from fork)
|
||||
if: github.event.pull_request.head.repo.full_name != github.repository
|
||||
uses: ./.github/actions/service-tests
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -117,3 +117,6 @@ service-definitions.yml
|
||||
|
||||
# Flamebearer
|
||||
flamegraph.html
|
||||
|
||||
# config file for node-pg-migrate
|
||||
migrations-config.json
|
||||
|
||||
20
CHANGELOG.md
20
CHANGELOG.md
@@ -4,6 +4,26 @@ Note: this changelog is for the shields.io server. The changelog for the badge-m
|
||||
|
||||
---
|
||||
|
||||
## server-2023-03-01
|
||||
|
||||
**Deprecation:** For users who need to maintain a Github Token pool, storage has been provided via the `RedisTokenPersistence` and `REDIS_URL` settings. As of this release, the `RedisTokenPersistence` backend is now deprecated and will be removed in a future release. If you are using this feature, you will need to migrate to using the `SQLTokenPersistence` backend for storage and provide a postgres connection string via the `POSTGRES_URL` setting. [#8922](https://github.com/badges/shields/issues/8922)
|
||||
|
||||
- fix: for crates.io versions, use max_stable_version if it exists [#8687](https://github.com/badges/shields/issues/8687)
|
||||
- don't autofocus search [#8927](https://github.com/badges/shields/issues/8927)
|
||||
- Add [Vcpkg] version service [#8923](https://github.com/badges/shields/issues/8923)
|
||||
- fix: Set uid/gid in docker image to 0 [#8908](https://github.com/badges/shields/issues/8908)
|
||||
- expose port 443 in Dockerfile [#8889](https://github.com/badges/shields/issues/8889)
|
||||
- Dependency updates
|
||||
|
||||
## server-2023-02-01
|
||||
|
||||
- replace [twitter] badge with static fallback [#8842](https://github.com/badges/shields/issues/8842)
|
||||
- Add various [Polymart] badges [#8811](https://github.com/badges/shields/issues/8811)
|
||||
- update [githubpipenv] tests/examples [#8797](https://github.com/badges/shields/issues/8797)
|
||||
- deprecate [apm] service [#8773](https://github.com/badges/shields/issues/8773)
|
||||
- deprecate lgtm [#8771](https://github.com/badges/shields/issues/8771)
|
||||
- Dependency updates
|
||||
|
||||
## server-2023-01-01
|
||||
|
||||
- Breaking change: Routes for GitHub workflows badge have changed. See https://github.com/badges/shields/issues/8671 for more details
|
||||
|
||||
@@ -30,8 +30,8 @@ LABEL fly.version=$version
|
||||
ENV NODE_ENV production
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY --from=Builder /usr/src/app /usr/src/app
|
||||
COPY --from=Builder --chown=0:0 /usr/src/app /usr/src/app
|
||||
|
||||
CMD node server
|
||||
|
||||
EXPOSE 80
|
||||
EXPOSE 80 443
|
||||
|
||||
@@ -19,9 +19,6 @@
|
||||
<a href="https://coveralls.io/github/badges/shields">
|
||||
<img src="https://img.shields.io/coveralls/github/badges/shields"
|
||||
alt="coverage"></a>
|
||||
<a href="https://lgtm.com/projects/g/badges/shields/alerts/">
|
||||
<img src="https://img.shields.io/lgtm/alerts/g/badges/shields"
|
||||
alt="Total alerts"/></a>
|
||||
<a href="https://discord.gg/HjJCwm5">
|
||||
<img src="https://img.shields.io/discord/308323056592486420?logo=discord"
|
||||
alt="chat on Discord"></a>
|
||||
|
||||
@@ -94,6 +94,7 @@ private:
|
||||
obs_user: 'OBS_USER'
|
||||
obs_pass: 'OBS_PASS'
|
||||
redis_url: 'REDIS_URL'
|
||||
postgres_url: 'POSTGRES_URL'
|
||||
sentry_dsn: 'SENTRY_DSN'
|
||||
sl_insight_userUuid: 'SL_INSIGHT_USER_UUID'
|
||||
sl_insight_apiToken: 'SL_INSIGHT_API_TOKEN'
|
||||
|
||||
@@ -27,11 +27,13 @@ class InvalidService extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
function getServicePaths(pattern) {
|
||||
return glob.sync(toUnixPath(path.join(serviceDir, '**', pattern)))
|
||||
}
|
||||
|
||||
async function loadServiceClasses(servicePaths) {
|
||||
if (!servicePaths) {
|
||||
servicePaths = glob.sync(
|
||||
toUnixPath(path.join(serviceDir, '**', '*.service.js'))
|
||||
)
|
||||
servicePaths = getServicePaths('*.service.js')
|
||||
}
|
||||
|
||||
const serviceClasses = []
|
||||
@@ -102,15 +104,16 @@ async function collectDefinitions() {
|
||||
|
||||
async function loadTesters() {
|
||||
return Promise.all(
|
||||
glob
|
||||
.sync(path.join(serviceDir, '**', '*.tester.js'))
|
||||
.map(async path => await import(`file://${path}`))
|
||||
getServicePaths('*.tester.js').map(
|
||||
async path => await import(`file://${path}`)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
InvalidService,
|
||||
loadServiceClasses,
|
||||
getServicePaths,
|
||||
checkNames,
|
||||
collectDefinitions,
|
||||
loadTesters,
|
||||
|
||||
@@ -2,7 +2,11 @@ import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import chai from 'chai'
|
||||
import chaiAsPromised from 'chai-as-promised'
|
||||
import { loadServiceClasses, InvalidService } from './loader.js'
|
||||
import {
|
||||
loadServiceClasses,
|
||||
getServicePaths,
|
||||
InvalidService,
|
||||
} from './loader.js'
|
||||
chai.use(chaiAsPromised)
|
||||
|
||||
const { expect } = chai
|
||||
@@ -65,3 +69,15 @@ describe('loadServiceClasses function', function () {
|
||||
).to.eventually.have.length(5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getServicePaths', function () {
|
||||
// these tests just make sure we discover a
|
||||
// plausibly large number of .service and .tester files
|
||||
it('finds a non-zero number of services in the project', function () {
|
||||
expect(getServicePaths('*.service.js')).to.have.length.above(400)
|
||||
})
|
||||
|
||||
it('finds a non-zero number of testers in the project', function () {
|
||||
expect(getServicePaths('*.tester.js')).to.have.length.above(400)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -14,14 +14,16 @@ let resourceCache = Object.create(null)
|
||||
/**
|
||||
* Make a HTTP request using an in-memory cache
|
||||
*
|
||||
* @param {object} attrs Refer to individual attrs
|
||||
* @param {string} attrs.url URL to request
|
||||
* @param {number} attrs.ttl Number of milliseconds to keep cached value for
|
||||
* @param {boolean} [attrs.json=true] True if we expect to parse the response as JSON
|
||||
* @param {Function} [attrs.scraper=buffer => buffer] Function to extract value from the response
|
||||
* @param {object} [attrs.options={}] Options to pass to got
|
||||
* @param {Function} [attrs.requestFetcher=fetch] Custom fetch function
|
||||
* @returns {*} Parsed response
|
||||
* @async
|
||||
* @param {object} attrs - Refer to individual attrs
|
||||
* @param {string} attrs.url - URL to request
|
||||
* @param {number} attrs.ttl - Number of milliseconds to keep cached value for
|
||||
* @param {boolean} [attrs.json=true] - True if we expect to parse the response as JSON
|
||||
* @param {Function} [attrs.scraper=buffer => buffer] - Function to extract value from the response
|
||||
* @param {object} [attrs.options={}] - Options to pass to got
|
||||
* @param {Function} [attrs.requestFetcher=fetch] - Custom fetch function
|
||||
* @throws {InvalidResponse} - Error if unable to parse response
|
||||
* @returns {Promise<*>} Promise that resolves to parsed response
|
||||
*/
|
||||
async function getCachedResource({
|
||||
url,
|
||||
|
||||
@@ -183,6 +183,7 @@ const privateConfigSchema = Joi.object({
|
||||
obs_user: Joi.string(),
|
||||
obs_pass: Joi.string(),
|
||||
redis_url: Joi.string().uri({ scheme: ['redis', 'rediss'] }),
|
||||
postgres_url: Joi.string().uri({ scheme: 'postgresql' }),
|
||||
sentry_dsn: Joi.string(),
|
||||
sl_insight_userUuid: Joi.string(),
|
||||
sl_insight_apiToken: Joi.string(),
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
/**
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { URL, format as urlFormat } from 'url'
|
||||
|
||||
function formatSlug(owner, repo, pullRequest) {
|
||||
return `${owner}/${repo}#${pullRequest}`
|
||||
}
|
||||
|
||||
function parseGithubPullRequestUrl(url, options = {}) {
|
||||
const { verifyBaseUrl } = options
|
||||
|
||||
const parsed = new URL(url)
|
||||
const components = parsed.pathname.substr(1).split('/')
|
||||
if (components[2] !== 'pull' || components.length !== 4) {
|
||||
throw Error(`Invalid GitHub pull request URL: ${url}`)
|
||||
}
|
||||
const [owner, repo, , pullRequest] = components
|
||||
|
||||
parsed.pathname = ''
|
||||
const baseUrl = urlFormat(parsed, {
|
||||
auth: false,
|
||||
fragment: false,
|
||||
search: false,
|
||||
}).replace(/\/$/, '')
|
||||
|
||||
if (verifyBaseUrl && baseUrl !== verifyBaseUrl) {
|
||||
throw Error(`Expected base URL to be ${verifyBaseUrl} but got ${baseUrl}`)
|
||||
}
|
||||
|
||||
return {
|
||||
baseUrl,
|
||||
owner,
|
||||
repo,
|
||||
pullRequest: +pullRequest,
|
||||
slug: formatSlug(owner, repo, pullRequest),
|
||||
}
|
||||
}
|
||||
|
||||
function parseGithubRepoSlug(slug) {
|
||||
const components = slug.split('/')
|
||||
if (components.length !== 2) {
|
||||
throw Error(`Invalid GitHub repo slug: ${slug}`)
|
||||
}
|
||||
const [owner, repo] = components
|
||||
return { owner, repo }
|
||||
}
|
||||
|
||||
function _inferPullRequestFromTravisEnv(env) {
|
||||
const { owner, repo } = parseGithubRepoSlug(env.TRAVIS_REPO_SLUG)
|
||||
const pullRequest = +env.TRAVIS_PULL_REQUEST
|
||||
return {
|
||||
owner,
|
||||
repo,
|
||||
pullRequest,
|
||||
slug: formatSlug(owner, repo, pullRequest),
|
||||
}
|
||||
}
|
||||
|
||||
function _inferPullRequestFromCircleEnv(env) {
|
||||
return parseGithubPullRequestUrl(
|
||||
env.CI_PULL_REQUEST || env.CIRCLE_PULL_REQUEST
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* When called inside a CI build, infer the details
|
||||
* of a pull request from the environment variables.
|
||||
*
|
||||
* @param {object} [env=process.env] Environment variables
|
||||
* @returns {module:core/service-test-runner/infer-pull-request~PullRequest}
|
||||
* Pull Request
|
||||
*/
|
||||
function inferPullRequest(env = process.env) {
|
||||
if (env.TRAVIS) {
|
||||
return _inferPullRequestFromTravisEnv(env)
|
||||
} else if (env.CIRCLECI) {
|
||||
return _inferPullRequestFromCircleEnv(env)
|
||||
} else if (env.CI) {
|
||||
throw Error(
|
||||
'Unsupported CI system. Unable to obtain pull request information from the environment.'
|
||||
)
|
||||
} else {
|
||||
throw Error(
|
||||
'Unable to obtain pull request information from the environment. Is this running in CI?'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull Request
|
||||
*
|
||||
* @typedef PullRequest
|
||||
* @property {string} pr.baseUrl (returned for travis CI only)
|
||||
* @property {string} owner
|
||||
* @property {string} repo
|
||||
* @property {string} pullRequest PR/issue number
|
||||
* @property {string} slug owner/repo/#pullRequest
|
||||
*/
|
||||
|
||||
export { parseGithubPullRequestUrl, parseGithubRepoSlug, inferPullRequest }
|
||||
@@ -1,48 +0,0 @@
|
||||
import { test, given, forCases } from 'sazerac'
|
||||
import {
|
||||
parseGithubPullRequestUrl,
|
||||
inferPullRequest,
|
||||
} from './infer-pull-request.js'
|
||||
|
||||
describe('Pull request inference', function () {
|
||||
test(parseGithubPullRequestUrl, () => {
|
||||
forCases([
|
||||
given('https://github.com/badges/shields/pull/1234'),
|
||||
given('https://github.com/badges/shields/pull/1234', {
|
||||
verifyBaseUrl: 'https://github.com',
|
||||
}),
|
||||
]).expect({
|
||||
baseUrl: 'https://github.com',
|
||||
owner: 'badges',
|
||||
repo: 'shields',
|
||||
pullRequest: 1234,
|
||||
slug: 'badges/shields#1234',
|
||||
})
|
||||
|
||||
given('https://github.com/badges/shields/pull/1234', {
|
||||
verifyBaseUrl: 'https://example.com',
|
||||
}).expectError(
|
||||
'Expected base URL to be https://example.com but got https://github.com'
|
||||
)
|
||||
})
|
||||
|
||||
test(inferPullRequest, () => {
|
||||
const expected = {
|
||||
owner: 'badges',
|
||||
repo: 'shields',
|
||||
pullRequest: 1234,
|
||||
slug: 'badges/shields#1234',
|
||||
}
|
||||
|
||||
given({
|
||||
CIRCLECI: '1',
|
||||
CI_PULL_REQUEST: 'https://github.com/badges/shields/pull/1234',
|
||||
}).expect(Object.assign({ baseUrl: 'https://github.com' }, expected))
|
||||
|
||||
given({
|
||||
TRAVIS: '1',
|
||||
TRAVIS_REPO_SLUG: 'badges/shields',
|
||||
TRAVIS_PULL_REQUEST: '1234',
|
||||
}).expect(expected)
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
// Infer the current PR from the Travis environment, and look for bracketed,
|
||||
// space-separated service names in the pull request title.
|
||||
// Derive a list of service tests to run based on
|
||||
// space-separated service names in the PR title.
|
||||
//
|
||||
// Output the list of services.
|
||||
//
|
||||
@@ -8,54 +8,26 @@
|
||||
// Output:
|
||||
// travis
|
||||
// sonar
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// TRAVIS=1 TRAVIS_REPO_SLUG=badges/shields TRAVIS_PULL_REQUEST=1108 npm run test:services:pr:prepare
|
||||
|
||||
import got from 'got'
|
||||
import { inferPullRequest } from './infer-pull-request.js'
|
||||
import servicesForTitle from './services-for-title.js'
|
||||
|
||||
async function getTitle(owner, repo, pullRequest) {
|
||||
const {
|
||||
body: { title },
|
||||
} = await got(
|
||||
`https://api.github.com/repos/${owner}/${repo}/pulls/${pullRequest}`,
|
||||
{
|
||||
headers: {
|
||||
'User-Agent': 'badges/shields',
|
||||
Authorization: `token ${process.env.GITHUB_TOKEN}`,
|
||||
},
|
||||
responseType: 'json',
|
||||
}
|
||||
)
|
||||
return title
|
||||
let title
|
||||
|
||||
try {
|
||||
if (process.argv.length < 3) {
|
||||
throw new Error()
|
||||
}
|
||||
title = process.argv[2]
|
||||
} catch (e) {
|
||||
console.error('Error processing arguments')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { owner, repo, pullRequest, slug } = inferPullRequest()
|
||||
console.error(`PR: ${slug}`)
|
||||
|
||||
const title = await getTitle(owner, repo, pullRequest)
|
||||
|
||||
console.error(`Title: ${title}\n`)
|
||||
const services = servicesForTitle(title)
|
||||
if (services.length === 0) {
|
||||
console.error('No services found. Nothing to do.')
|
||||
} else {
|
||||
console.error(
|
||||
`Services: (${services.length} found) ${services.join(', ')}\n`
|
||||
)
|
||||
console.log(services.join('\n'))
|
||||
}
|
||||
console.error(`Title: ${title}\n`)
|
||||
const services = servicesForTitle(title)
|
||||
if (services.length === 0) {
|
||||
console.error('No services found. Nothing to do.')
|
||||
} else {
|
||||
console.error(`Services: (${services.length} found) ${services.join(', ')}\n`)
|
||||
console.log(services.join('\n'))
|
||||
}
|
||||
|
||||
;(async () => {
|
||||
try {
|
||||
await main()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
process.exit(1)
|
||||
}
|
||||
})()
|
||||
|
||||
@@ -84,7 +84,6 @@ describe('Redis token persistence', function () {
|
||||
const toRemove = expected.pop()
|
||||
|
||||
await persistence.initialize()
|
||||
|
||||
await persistence.noteTokenRemoved(toRemove)
|
||||
|
||||
const savedTokens = await redis.smembers(key)
|
||||
|
||||
103
core/token-pooling/sql-token-persistence.integration.js
Normal file
103
core/token-pooling/sql-token-persistence.integration.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import pg from 'pg'
|
||||
import { expect } from 'chai'
|
||||
import configModule from 'config'
|
||||
import SqlTokenPersistence from './sql-token-persistence.js'
|
||||
|
||||
const config = configModule.util.toObject()
|
||||
const postgresUrl = config?.private?.postgres_url
|
||||
const tableName = 'token_persistence_integration_test'
|
||||
|
||||
describe('SQL token persistence', function () {
|
||||
let pool
|
||||
let persistence
|
||||
|
||||
before('Mock db connection and load app', async function () {
|
||||
// Create a new pool with a connection limit of 1
|
||||
pool = new pg.Pool({
|
||||
connectionString: postgresUrl,
|
||||
|
||||
// Reuse the connection to make sure we always hit the same pg_temp schema
|
||||
max: 1,
|
||||
|
||||
// Disable auto-disconnection of idle clients to make sure we always hit the same pg_temp schema
|
||||
idleTimeoutMillis: 0,
|
||||
})
|
||||
persistence = new SqlTokenPersistence({
|
||||
url: postgresUrl,
|
||||
table: tableName,
|
||||
})
|
||||
})
|
||||
after(async function () {
|
||||
if (persistence) {
|
||||
await persistence.stop()
|
||||
persistence = undefined
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach('Create temporary table', async function () {
|
||||
await pool.query(
|
||||
`CREATE TEMPORARY TABLE ${tableName} (LIKE github_user_tokens INCLUDING ALL);`
|
||||
)
|
||||
})
|
||||
afterEach('Drop temporary table', async function () {
|
||||
await pool.query(`DROP TABLE IF EXISTS pg_temp.${tableName};`)
|
||||
})
|
||||
|
||||
context('when the key does not exist', function () {
|
||||
it('does nothing', async function () {
|
||||
const tokens = await persistence.initialize(pool)
|
||||
expect(tokens).to.deep.equal([])
|
||||
})
|
||||
})
|
||||
|
||||
context('when the key exists', function () {
|
||||
const initialTokens = ['a', 'b', 'c'].map(char => char.repeat(40))
|
||||
|
||||
beforeEach(async function () {
|
||||
initialTokens.forEach(async token => {
|
||||
await pool.query(
|
||||
`INSERT INTO pg_temp.${tableName} (token) VALUES ($1::text);`,
|
||||
[token]
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('loads the contents', async function () {
|
||||
const tokens = await persistence.initialize(pool)
|
||||
expect(tokens.sort()).to.deep.equal(initialTokens)
|
||||
})
|
||||
|
||||
context('when tokens are added', function () {
|
||||
it('saves the change', async function () {
|
||||
const newToken = 'e'.repeat(40)
|
||||
const expected = initialTokens.slice()
|
||||
expected.push(newToken)
|
||||
|
||||
await persistence.initialize(pool)
|
||||
await persistence.noteTokenAdded(newToken)
|
||||
|
||||
const result = await pool.query(
|
||||
`SELECT token FROM pg_temp.${tableName};`
|
||||
)
|
||||
const savedTokens = result.rows.map(row => row.token)
|
||||
expect(savedTokens.sort()).to.deep.equal(expected)
|
||||
})
|
||||
})
|
||||
|
||||
context('when tokens are removed', function () {
|
||||
it('saves the change', async function () {
|
||||
const expected = Array.from(initialTokens)
|
||||
const toRemove = expected.pop()
|
||||
|
||||
await persistence.initialize(pool)
|
||||
await persistence.noteTokenRemoved(toRemove)
|
||||
|
||||
const result = await pool.query(
|
||||
`SELECT token FROM pg_temp.${tableName};`
|
||||
)
|
||||
const savedTokens = result.rows.map(row => row.token)
|
||||
expect(savedTokens.sort()).to.deep.equal(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
55
core/token-pooling/sql-token-persistence.js
Normal file
55
core/token-pooling/sql-token-persistence.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import pg from 'pg'
|
||||
import log from '../server/log.js'
|
||||
|
||||
export default class SqlTokenPersistence {
|
||||
constructor({ url, table }) {
|
||||
this.url = url
|
||||
this.table = table
|
||||
this.noteTokenAdded = this.noteTokenAdded.bind(this)
|
||||
this.noteTokenRemoved = this.noteTokenRemoved.bind(this)
|
||||
}
|
||||
|
||||
async initialize(pool) {
|
||||
if (pool) {
|
||||
this.pool = pool
|
||||
} else {
|
||||
this.pool = new pg.Pool({ connectionString: this.url })
|
||||
}
|
||||
const result = await this.pool.query(`SELECT token FROM ${this.table};`)
|
||||
return result.rows.map(row => row.token)
|
||||
}
|
||||
|
||||
async stop() {
|
||||
await this.pool.end()
|
||||
}
|
||||
|
||||
async onTokenAdded(token) {
|
||||
return await this.pool.query(
|
||||
`INSERT INTO ${this.table} (token) VALUES ($1::text) ON CONFLICT (token) DO NOTHING;`,
|
||||
[token]
|
||||
)
|
||||
}
|
||||
|
||||
async onTokenRemoved(token) {
|
||||
return await this.pool.query(
|
||||
`DELETE FROM ${this.table} WHERE token=$1::text;`,
|
||||
[token]
|
||||
)
|
||||
}
|
||||
|
||||
async noteTokenAdded(token) {
|
||||
try {
|
||||
await this.onTokenAdded(token)
|
||||
} catch (e) {
|
||||
log.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
async noteTokenRemoved(token) {
|
||||
try {
|
||||
await this.onTokenRemoved(token)
|
||||
} catch (e) {
|
||||
log.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@
|
||||
// DANGER_GITHUB_API_TOKEN=your-github-api-token npm run danger -- pr https://github.com/badges/shields/pull/2665
|
||||
|
||||
const { danger, fail, message, warn } = require('danger')
|
||||
const { default: noTestShortcuts } = require('danger-plugin-no-test-shortcuts')
|
||||
const { fileMatch } = danger.git
|
||||
|
||||
const documentation = fileMatch(
|
||||
@@ -173,11 +172,3 @@ affectedServices.forEach(service => {
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Prevent merging exclusive services tests.
|
||||
noTestShortcuts({
|
||||
testFilePredicate: filePath => filePath.endsWith('.tester.js'),
|
||||
patterns: {
|
||||
only: ['only()'],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -125,11 +125,17 @@ Because of GitHub rate limits, you will need to provide a token, or else badges
|
||||
will stop working once you hit 60 requests per hour, the
|
||||
[unauthenticated rate limit][github rate limit].
|
||||
|
||||
You can [create a personal access token][personal access tokens] through the
|
||||
You can [create a personal access token][personal access tokens] (PATs) through the
|
||||
GitHub website. When you create the token, you can choose to give read access
|
||||
to your repositories. If you do that, your self-hosted Shields installation
|
||||
will have access to your private repositories.
|
||||
|
||||
For most users we recommend using a classic PAT as opposed to a [fine-grained PAT][fine-grained pat].
|
||||
It is possible to request a fairly large subset of the GitHub badge suite using a
|
||||
fine-grained PAT for authentication but there are also some badges that won't work.
|
||||
This is because some of our badges make use of GitHub's v4 GraphQL API and the
|
||||
GraphQL API only supports authentication with a classic PAT.
|
||||
|
||||
When a `gh_token` is specified, it is used in place of the Shields token
|
||||
rotation logic.
|
||||
|
||||
@@ -139,6 +145,7 @@ token, though it's not required.
|
||||
|
||||
[github rate limit]: https://developer.github.com/v3/#rate-limiting
|
||||
[personal access tokens]: https://github.com/settings/tokens
|
||||
[fine-grained pat]: https://github.blog/2022-10-18-introducing-fine-grained-personal-access-tokens-for-github/
|
||||
|
||||
- `GH_CLIENT_ID` (yml: `private.gh_client_id`)
|
||||
- `GH_CLIENT_SECRET` (yml: `private.gh_client_secret`)
|
||||
|
||||
@@ -27,7 +27,6 @@ export default function Search({
|
||||
<form action="javascript:void 0" autoComplete="off">
|
||||
<BlockInput
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
onChange={onQueryChanged}
|
||||
placeholder="search"
|
||||
/>
|
||||
|
||||
@@ -106,9 +106,6 @@ export default function SponsorsPage(): JSX.Element {
|
||||
<li>
|
||||
<a href="https://github.com/">GitHub</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://lgtm.com/">LGTM</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://uptimerobot.com/">Uptime Robot</a>
|
||||
</li>
|
||||
|
||||
14
migrations/1676731511125_initialize.cjs
Normal file
14
migrations/1676731511125_initialize.cjs
Normal file
@@ -0,0 +1,14 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
exports.shorthands = undefined
|
||||
|
||||
exports.up = pgm => {
|
||||
pgm.createTable('github_user_tokens', {
|
||||
id: 'id',
|
||||
token: { type: 'varchar(1000)', notNull: true, unique: true },
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = pgm => {
|
||||
pgm.dropTable('github_user_tokens')
|
||||
}
|
||||
2884
package-lock.json
generated
2884
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
69
package.json
69
package.json
@@ -24,29 +24,29 @@
|
||||
"@fontsource/lato": "^4.5.10",
|
||||
"@fontsource/lekton": "^4.5.11",
|
||||
"@renovate/pep440": "^1.0.0",
|
||||
"@renovatebot/ruby-semver": "^1.1.7",
|
||||
"@sentry/node": "^7.28.1",
|
||||
"@shields_io/camp": "^18.1.1",
|
||||
"@renovatebot/ruby-semver": "^2.1.8",
|
||||
"@sentry/node": "^7.38.0",
|
||||
"@shields_io/camp": "^18.1.2",
|
||||
"badge-maker": "file:badge-maker",
|
||||
"bytes": "^3.1.2",
|
||||
"camelcase": "^7.0.1",
|
||||
"chalk": "^5.2.0",
|
||||
"check-node-version": "^4.2.1",
|
||||
"cloudflare-middleware": "^1.0.4",
|
||||
"config": "^3.3.8",
|
||||
"config": "^3.3.9",
|
||||
"cross-env": "^7.0.3",
|
||||
"dayjs": "^1.11.7",
|
||||
"decamelize": "^3.2.0",
|
||||
"emojic": "^1.1.17",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"fast-xml-parser": "^4.0.12",
|
||||
"glob": "^8.0.3",
|
||||
"fast-xml-parser": "^4.1.2",
|
||||
"glob": "^8.1.0",
|
||||
"global-agent": "^3.0.0",
|
||||
"got": "^12.5.3",
|
||||
"graphql": "^15.6.1",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"ioredis": "5.2.4",
|
||||
"joi": "17.7.0",
|
||||
"ioredis": "5.3.1",
|
||||
"joi": "17.8.3",
|
||||
"joi-extension-semver": "5.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonpath": "~1.1.1",
|
||||
@@ -54,15 +54,17 @@
|
||||
"lodash.groupby": "^4.6.0",
|
||||
"lodash.times": "^4.3.2",
|
||||
"node-env-flag": "^0.1.0",
|
||||
"node-pg-migrate": "^6.2.2",
|
||||
"parse-link-header": "^2.0.0",
|
||||
"path-to-regexp": "^6.2.1",
|
||||
"pretty-bytes": "^6.0.0",
|
||||
"pg": "^8.9.0",
|
||||
"pretty-bytes": "^6.1.0",
|
||||
"priorityqueuejs": "^2.0.0",
|
||||
"prom-client": "^14.1.0",
|
||||
"prom-client": "^14.1.1",
|
||||
"qs": "^6.11.0",
|
||||
"query-string": "^8.1.0",
|
||||
"semver": "~7.3.8",
|
||||
"simple-icons": "8.2.0",
|
||||
"simple-icons": "8.5.0",
|
||||
"webextension-store-meta": "^1.0.5",
|
||||
"xmldom": "~0.6.0",
|
||||
"xpath": "~0.0.32"
|
||||
@@ -117,7 +119,8 @@
|
||||
"e2e": "start-server-and-test start http://localhost:3000 test:e2e",
|
||||
"e2e-on-build": "cross-env CYPRESS_baseUrl=http://localhost:8080 start-server-and-test start:server:e2e-on-build http://localhost:8080 test:e2e",
|
||||
"badge": "cross-env NODE_CONFIG_ENV=test TRACE_SERVICES=true node scripts/badge-cli.js",
|
||||
"build-docs": "rimraf api-docs/ && jsdoc --pedantic -c ./jsdoc.json . && echo 'contributing.shields.io' > api-docs/CNAME"
|
||||
"build-docs": "rimraf api-docs/ && jsdoc --pedantic -c ./jsdoc.json . && echo 'contributing.shields.io' > api-docs/CNAME",
|
||||
"migrate": "node scripts/write-migrations-config.js > migrations-config.json && node-pg-migrate --config-file=migrations-config.json"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*.@(js|ts|tsx)": [
|
||||
@@ -142,9 +145,9 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.7",
|
||||
"@babel/core": "^7.21.0",
|
||||
"@babel/polyfill": "^7.12.1",
|
||||
"@babel/register": "7.18.9",
|
||||
"@babel/register": "7.21.0",
|
||||
"@istanbuljs/schema": "^0.1.3",
|
||||
"@mapbox/react-click-to-select": "^2.2.1",
|
||||
"@types/chai": "^4.3.4",
|
||||
@@ -156,11 +159,11 @@
|
||||
"@types/react-modal": "^3.13.1",
|
||||
"@types/react-select": "^4.0.17",
|
||||
"@types/styled-components": "5.1.26",
|
||||
"@typescript-eslint/eslint-plugin": "^5.47.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.53.0",
|
||||
"@typescript-eslint/parser": "^5.46.0",
|
||||
"babel-plugin-inline-react-svg": "^2.0.1",
|
||||
"babel-plugin-inline-react-svg": "^2.0.2",
|
||||
"babel-preset-gatsby": "^2.22.0",
|
||||
"c8": "^7.12.0",
|
||||
"c8": "^7.13.0",
|
||||
"caller": "^1.1.0",
|
||||
"chai": "^4.3.7",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
@@ -169,25 +172,25 @@
|
||||
"child-process-promise": "^2.2.1",
|
||||
"clipboard-copy": "^4.0.1",
|
||||
"concurrently": "^7.6.0",
|
||||
"cypress": "^12.2.0",
|
||||
"cypress": "^12.6.0",
|
||||
"cypress-wait-for-stable-dom": "^0.1.0",
|
||||
"danger": "^11.2.0",
|
||||
"danger-plugin-no-test-shortcuts": "^2.0.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"danger": "^11.2.3",
|
||||
"deepmerge": "^4.3.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-config-standard": "^16.0.3",
|
||||
"eslint-config-standard-jsx": "^10.0.0",
|
||||
"eslint-config-standard-react": "^11.0.1",
|
||||
"eslint-plugin-chai-friendly": "^0.7.2",
|
||||
"eslint-plugin-cypress": "^2.12.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsdoc": "^39.6.4",
|
||||
"eslint-plugin-icedfrisby": "^0.1.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jsdoc": "^40.0.0",
|
||||
"eslint-plugin-mocha": "^10.1.0",
|
||||
"eslint-plugin-no-extension-in-require": "^0.2.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^5.2.0",
|
||||
"eslint-plugin-react": "^7.31.11",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-sort-class-members": "^1.16.0",
|
||||
"fetch-ponyfill": "^7.1.0",
|
||||
@@ -204,22 +207,22 @@
|
||||
"icedfrisby-nock": "^2.1.0",
|
||||
"is-svg": "^4.3.2",
|
||||
"js-yaml-loader": "^1.2.2",
|
||||
"jsdoc": "^4.0.0",
|
||||
"lint-staged": "^13.1.0",
|
||||
"jsdoc": "^4.0.2",
|
||||
"lint-staged": "^13.1.2",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.difference": "^4.5.0",
|
||||
"minimist": "^1.2.7",
|
||||
"minimist": "^1.2.8",
|
||||
"mocha": "^10.2.0",
|
||||
"mocha-env-reporter": "^4.0.0",
|
||||
"mocha-junit-reporter": "^2.2.0",
|
||||
"mocha-yaml-loader": "^1.0.3",
|
||||
"nock": "13.2.9",
|
||||
"nock": "13.3.0",
|
||||
"node-mocks-http": "^1.12.1",
|
||||
"nodemon": "^2.0.20",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"open-cli": "^7.1.0",
|
||||
"portfinder": "^1.0.32",
|
||||
"prettier": "2.8.1",
|
||||
"prettier": "2.8.4",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-error-overlay": "^6.0.11",
|
||||
@@ -229,17 +232,17 @@
|
||||
"react-select": "^4.3.1",
|
||||
"read-all-stdin-sync": "^1.0.5",
|
||||
"redis-server": "^1.2.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"rimraf": "^4.1.2",
|
||||
"sazerac": "^2.0.0",
|
||||
"simple-git-hooks": "^2.8.1",
|
||||
"sinon": "^15.0.1",
|
||||
"sinon-chai": "^3.7.0",
|
||||
"snap-shot-it": "^7.9.10",
|
||||
"start-server-and-test": "1.15.2",
|
||||
"start-server-and-test": "1.15.4",
|
||||
"styled-components": "^5.3.6",
|
||||
"ts-mocha": "^10.0.0",
|
||||
"tsd": "^0.25.0",
|
||||
"typescript": "^4.9.4",
|
||||
"typescript": "^4.9.5",
|
||||
"url": "^0.11.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -22,6 +22,10 @@ if (data.stats.passes > 0) {
|
||||
if (data.stats.failures > 0) {
|
||||
process.stdout.write(`✖ ${data.stats.failures} failed\n\n`)
|
||||
}
|
||||
if (data.stats.pending > 0) {
|
||||
process.stdout.write(`● ${data.stats.pending} pending\n\n`)
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
if (data.stats.failures > 0) {
|
||||
for (const test of data.tests) {
|
||||
|
||||
11
scripts/write-migrations-config.js
Normal file
11
scripts/write-migrations-config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import configModule from 'config'
|
||||
const config = configModule.util.toObject()
|
||||
|
||||
const postgresUrl = config?.private?.postgres_url
|
||||
|
||||
if (!postgresUrl) {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
process.stdout.write(JSON.stringify({ url: postgresUrl }))
|
||||
process.exit(0)
|
||||
@@ -42,6 +42,15 @@ if (fs.existsSync('.env')) {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (config.private.redis_url != null) {
|
||||
console.warn(
|
||||
'RedisTokenPersistence is deprecated for token pooling and will be removed in a future release. Migrate to SqlTokenPersistence'
|
||||
)
|
||||
console.warn(
|
||||
'See https://github.com/badges/shields/blob/master/CHANGELOG.md#server-2023-03-01 for more info'
|
||||
)
|
||||
}
|
||||
|
||||
const legacySecretsPath = path.join(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'private',
|
||||
|
||||
@@ -1,117 +1,33 @@
|
||||
import Joi from 'joi'
|
||||
import { renderLicenseBadge } from '../licenses.js'
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
import { renderDownloadsBadge } from '../downloads.js'
|
||||
import { nonNegativeInteger } from '../validators.js'
|
||||
import { BaseJsonService, InvalidResponse } from '../index.js'
|
||||
import { deprecatedService } from '../index.js'
|
||||
|
||||
const keywords = ['atom']
|
||||
|
||||
const schema = Joi.object({
|
||||
downloads: nonNegativeInteger,
|
||||
releases: Joi.object({
|
||||
latest: Joi.string().required(),
|
||||
}),
|
||||
metadata: Joi.object({
|
||||
license: Joi.string().required(),
|
||||
}),
|
||||
const APMDownloads = deprecatedService({
|
||||
category: 'downloads',
|
||||
route: {
|
||||
base: 'apm/dm',
|
||||
pattern: ':various*',
|
||||
},
|
||||
label: 'downloads',
|
||||
dateAdded: new Date('2023-01-04'),
|
||||
})
|
||||
|
||||
class BaseAPMService extends BaseJsonService {
|
||||
static defaultBadgeData = { label: 'apm' }
|
||||
const APMVersion = deprecatedService({
|
||||
category: 'version',
|
||||
route: {
|
||||
base: 'apm/v',
|
||||
pattern: ':various*',
|
||||
},
|
||||
label: 'apm',
|
||||
dateAdded: new Date('2023-01-04'),
|
||||
})
|
||||
|
||||
async fetch({ packageName }) {
|
||||
return this._requestJson({
|
||||
schema,
|
||||
url: `https://atom.io/api/packages/${packageName}`,
|
||||
errorMessages: { 404: 'package not found' },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class APMDownloads extends BaseAPMService {
|
||||
static category = 'downloads'
|
||||
static route = { base: 'apm/dm', pattern: ':packageName' }
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'APM',
|
||||
namedParams: { packageName: 'vim-mode' },
|
||||
staticPreview: this.render({ downloads: '60043' }),
|
||||
keywords,
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = { label: 'downloads' }
|
||||
|
||||
static render({ downloads }) {
|
||||
return renderDownloadsBadge({ downloads, colorOverride: 'green' })
|
||||
}
|
||||
|
||||
async handle({ packageName }) {
|
||||
const json = await this.fetch({ packageName })
|
||||
return this.constructor.render({ downloads: json.downloads })
|
||||
}
|
||||
}
|
||||
|
||||
class APMVersion extends BaseAPMService {
|
||||
static category = 'version'
|
||||
static route = { base: 'apm/v', pattern: ':packageName' }
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'APM',
|
||||
namedParams: { packageName: 'vim-mode' },
|
||||
staticPreview: this.render({ version: '0.6.0' }),
|
||||
keywords,
|
||||
},
|
||||
]
|
||||
|
||||
static render({ version }) {
|
||||
return renderVersionBadge({ version })
|
||||
}
|
||||
|
||||
async handle({ packageName }) {
|
||||
const json = await this.fetch({ packageName })
|
||||
|
||||
const version = json.releases.latest
|
||||
if (!version)
|
||||
throw new InvalidResponse({
|
||||
underlyingError: new Error('version is invalid'),
|
||||
})
|
||||
return this.constructor.render({ version })
|
||||
}
|
||||
}
|
||||
|
||||
class APMLicense extends BaseAPMService {
|
||||
static category = 'license'
|
||||
static route = { base: 'apm/l', pattern: ':packageName' }
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'APM',
|
||||
namedParams: { packageName: 'vim-mode' },
|
||||
staticPreview: this.render({ license: 'MIT' }),
|
||||
keywords,
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = { label: 'license' }
|
||||
|
||||
static render({ license }) {
|
||||
return renderLicenseBadge({ license })
|
||||
}
|
||||
|
||||
async handle({ packageName }) {
|
||||
const json = await this.fetch({ packageName })
|
||||
|
||||
const license = json.metadata.license
|
||||
if (!license)
|
||||
throw new InvalidResponse({
|
||||
underlyingError: new Error('licence is invalid'),
|
||||
})
|
||||
return this.constructor.render({ license })
|
||||
}
|
||||
}
|
||||
const APMLicense = deprecatedService({
|
||||
category: 'license',
|
||||
route: {
|
||||
base: 'apm/l',
|
||||
pattern: ':various*',
|
||||
},
|
||||
label: 'license',
|
||||
dateAdded: new Date('2023-01-04'),
|
||||
})
|
||||
|
||||
export { APMDownloads, APMVersion, APMLicense }
|
||||
|
||||
@@ -1,57 +1,19 @@
|
||||
import { ServiceTester } from '../tester.js'
|
||||
import { invalidJSON } from '../response-fixtures.js'
|
||||
import { isMetric, isVPlusTripleDottedVersion } from '../test-validators.js'
|
||||
|
||||
export const t = new ServiceTester({
|
||||
id: 'apm',
|
||||
title: 'Atom Package Manager',
|
||||
pathPrefix: '/apm',
|
||||
})
|
||||
|
||||
t.create('Downloads')
|
||||
.get('/dm/vim-mode.json')
|
||||
.expectBadge({ label: 'downloads', message: isMetric })
|
||||
.expectBadge({ label: 'downloads', message: 'no longer available' })
|
||||
|
||||
t.create('Version')
|
||||
.get('/v/vim-mode.json')
|
||||
.expectBadge({ label: 'apm', message: isVPlusTripleDottedVersion })
|
||||
.expectBadge({ label: 'apm', message: 'no longer available' })
|
||||
|
||||
t.create('License')
|
||||
.get('/l/vim-mode.json')
|
||||
.expectBadge({ label: 'license', message: 'MIT' })
|
||||
|
||||
t.create('Downloads | Package not found')
|
||||
.get('/dm/notapackage.json')
|
||||
.expectBadge({ label: 'downloads', message: 'package not found' })
|
||||
|
||||
t.create('Version | Package not found')
|
||||
.get('/v/notapackage.json')
|
||||
.expectBadge({ label: 'apm', message: 'package not found' })
|
||||
|
||||
t.create('License | Package not found')
|
||||
.get('/l/notapackage.json')
|
||||
.expectBadge({ label: 'license', message: 'package not found' })
|
||||
|
||||
t.create('Invalid version')
|
||||
.get('/dm/vim-mode.json')
|
||||
.intercept(nock =>
|
||||
nock('https://atom.io')
|
||||
.get('/api/packages/vim-mode')
|
||||
.reply(200, '{"releases":{}}')
|
||||
)
|
||||
.expectBadge({ label: 'downloads', message: 'invalid response data' })
|
||||
|
||||
t.create('Invalid License')
|
||||
.get('/l/vim-mode.json')
|
||||
.intercept(nock =>
|
||||
nock('https://atom.io')
|
||||
.get('/api/packages/vim-mode')
|
||||
.reply(200, '{"metadata":{}}')
|
||||
)
|
||||
.expectBadge({ label: 'license', message: 'invalid response data' })
|
||||
|
||||
t.create('Unexpected response')
|
||||
.get('/dm/vim-mode.json')
|
||||
.intercept(nock =>
|
||||
nock('https://atom.io').get('/api/packages/vim-mode').reply(invalidJSON)
|
||||
)
|
||||
.expectBadge({ label: 'downloads', message: 'unparseable json response' })
|
||||
.expectBadge({ label: 'license', message: 'no longer available' })
|
||||
|
||||
@@ -19,12 +19,14 @@ export default class CratesVersion extends BaseCratesService {
|
||||
if (json.errors) {
|
||||
throw new InvalidResponse({ prettyMessage: json.errors[0].detail })
|
||||
}
|
||||
return { version: json.version ? json.version.num : json.crate.max_version }
|
||||
return json.crate.max_stable_version
|
||||
? json.crate.max_stable_version
|
||||
: json.crate.max_version
|
||||
}
|
||||
|
||||
async handle({ crate }) {
|
||||
const json = await this.fetch({ crate })
|
||||
const { version } = this.transform(json)
|
||||
const version = this.transform(json)
|
||||
return renderVersionBadge({ version })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ import CratesVersion from './crates-version.service.js'
|
||||
|
||||
describe('CratesVersion', function () {
|
||||
test(CratesVersion.prototype.transform, () => {
|
||||
given({ version: { num: '1.0.0' } }).expect({ version: '1.0.0' })
|
||||
given({ crate: { max_version: '1.1.0' } }).expect({ version: '1.1.0' })
|
||||
given({ crate: { max_version: '1.1.0' } }).expect('1.1.0')
|
||||
given({
|
||||
crate: { max_stable_version: '1.1.0', max_version: '1.9.0-alpha' },
|
||||
}).expect('1.1.0')
|
||||
})
|
||||
|
||||
it('throws InvalidResponse on error response', function () {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ServiceTester } from '../../tester.js'
|
||||
|
||||
export const t = new ServiceTester({
|
||||
id: 'GithubGistLastCommitRedirect',
|
||||
id: 'GistLastCommitRedirect',
|
||||
title: 'Github Gist Last Commit Redirect',
|
||||
pathPrefix: '/github-gist',
|
||||
})
|
||||
|
||||
@@ -8,7 +8,7 @@ const schema = Joi.object({
|
||||
updated_at: Joi.string().required(),
|
||||
}).required()
|
||||
|
||||
export default class GithubGistLastCommit extends GithubAuthV3Service {
|
||||
export default class GistLastCommit extends GithubAuthV3Service {
|
||||
static category = 'activity'
|
||||
static route = { base: 'github/gist/last-commit', pattern: ':gistId' }
|
||||
static examples = [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ServiceTester } from '../../tester.js'
|
||||
export const t = new ServiceTester({
|
||||
id: 'GithubGistStarsRedirect',
|
||||
id: 'GistStarsRedirect',
|
||||
title: 'Github Gist Stars Redirect',
|
||||
pathPrefix: '/github',
|
||||
})
|
||||
|
||||
@@ -24,7 +24,7 @@ const documentation = `${commonDocumentation}
|
||||
<p>This badge shows the number of stargazers for a gist. Gist id is accepted as input and 'gist not found' is returned if the gist is not found for the given gist id.
|
||||
</p>`
|
||||
|
||||
export default class GithubGistStars extends GithubAuthV4Service {
|
||||
export default class GistStars extends GithubAuthV4Service {
|
||||
static category = 'social'
|
||||
|
||||
static route = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AuthHelper } from '../../core/base-service/auth-helper.js'
|
||||
import RedisTokenPersistence from '../../core/token-pooling/redis-token-persistence.js'
|
||||
import SqlTokenPersistence from '../../core/token-pooling/sql-token-persistence.js'
|
||||
import log from '../../core/server/log.js'
|
||||
import GithubApiProvider from './github-api-provider.js'
|
||||
import { setRoutes as setAcceptorRoutes } from './auth/acceptor.js'
|
||||
@@ -23,8 +24,18 @@ class GithubConstellation {
|
||||
this._debugEnabled = config.service.debug.enabled
|
||||
this._debugIntervalSeconds = config.service.debug.intervalSeconds
|
||||
|
||||
const { redis_url: redisUrl, gh_token: globalToken } = config.private
|
||||
if (redisUrl) {
|
||||
const {
|
||||
postgres_url: pgUrl,
|
||||
redis_url: redisUrl,
|
||||
gh_token: globalToken,
|
||||
} = config.private
|
||||
if (pgUrl) {
|
||||
log.log('Token persistence configured with dbUrl')
|
||||
this.persistence = new SqlTokenPersistence({
|
||||
url: pgUrl,
|
||||
table: 'github_user_tokens',
|
||||
})
|
||||
} else if (redisUrl) {
|
||||
log.log('Token persistence configured with redisUrl')
|
||||
this.persistence = new RedisTokenPersistence({
|
||||
url: redisUrl,
|
||||
|
||||
@@ -18,7 +18,6 @@ const documentation = `
|
||||
badge can be added to the project readme to encourage potential
|
||||
contributors to review the suggested issues and to celebrate the
|
||||
contributions that have already been made.
|
||||
|
||||
The badge displays three pieces of information:
|
||||
<ul>
|
||||
<li>
|
||||
@@ -33,7 +32,6 @@ const documentation = `
|
||||
</li>
|
||||
<li>The number of days left of October.</li>
|
||||
</ul>
|
||||
|
||||
</p>
|
||||
|
||||
${githubDocumentation}
|
||||
|
||||
@@ -66,9 +66,9 @@ class GithubPipenvLockedPythonVersion extends ConditionalGithubAuthV3Service {
|
||||
namedParams: {
|
||||
user: 'metabolize',
|
||||
repo: 'rq-dashboard-on-heroku',
|
||||
branch: 'master',
|
||||
branch: 'main',
|
||||
},
|
||||
staticPreview: this.render({ version: '3.7', branch: 'master' }),
|
||||
staticPreview: this.render({ version: '3.7', branch: 'main' }),
|
||||
documentation,
|
||||
keywords,
|
||||
},
|
||||
@@ -135,7 +135,7 @@ class GithubPipenvLockedDependencyVersion extends ConditionalGithubAuthV3Service
|
||||
repo: 'rq-dashboard-on-heroku',
|
||||
kind: 'dev',
|
||||
packageName: 'black',
|
||||
branch: 'master',
|
||||
branch: 'main',
|
||||
},
|
||||
staticPreview: this.render({ dependency: 'black', version: '19.3b0' }),
|
||||
documentation,
|
||||
|
||||
@@ -47,7 +47,7 @@ t.create('Locked version of default dependency')
|
||||
|
||||
t.create('Locked version of default dependency (branch)')
|
||||
.get(
|
||||
'/locked/dependency-version/metabolize/rq-dashboard-on-heroku/rq-dashboard/master.json'
|
||||
'/locked/dependency-version/metabolize/rq-dashboard-on-heroku/rq-dashboard/main.json'
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'rq-dashboard',
|
||||
@@ -65,7 +65,7 @@ t.create('Locked version of dev dependency')
|
||||
|
||||
t.create('Locked version of dev dependency (branch)')
|
||||
.get(
|
||||
'/locked/dependency-version/metabolize/rq-dashboard-on-heroku/dev/black/master.json'
|
||||
'/locked/dependency-version/metabolize/rq-dashboard-on-heroku/dev/black/main.json'
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'black',
|
||||
|
||||
@@ -1,45 +1,11 @@
|
||||
import { metric } from '../text-formatters.js'
|
||||
import LgtmBaseService from './lgtm-base.js'
|
||||
import { deprecatedService } from '../index.js'
|
||||
|
||||
export default class LgtmAlerts extends LgtmBaseService {
|
||||
static route = {
|
||||
export default deprecatedService({
|
||||
category: 'analysis',
|
||||
route: {
|
||||
base: 'lgtm/alerts',
|
||||
pattern: this.pattern,
|
||||
}
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'LGTM Alerts',
|
||||
namedParams: {
|
||||
host: 'github',
|
||||
user: 'apache',
|
||||
repo: 'cloudstack',
|
||||
},
|
||||
staticPreview: this.render({ alerts: 2488 }),
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'lgtm alerts',
|
||||
}
|
||||
|
||||
static getColor({ alerts }) {
|
||||
let color = 'yellow'
|
||||
if (alerts === 0) {
|
||||
color = 'brightgreen'
|
||||
}
|
||||
return color
|
||||
}
|
||||
|
||||
static render({ alerts }) {
|
||||
return {
|
||||
message: metric(alerts),
|
||||
color: this.getColor({ alerts }),
|
||||
}
|
||||
}
|
||||
|
||||
async handle({ host, user, repo }) {
|
||||
const { alerts } = await this.fetch({ host, user, repo })
|
||||
return this.constructor.render({ alerts })
|
||||
}
|
||||
}
|
||||
pattern: ':various*',
|
||||
},
|
||||
label: 'lgtm alerts',
|
||||
dateAdded: new Date('2023-01-03'),
|
||||
})
|
||||
|
||||
@@ -1,61 +1,11 @@
|
||||
import Joi from 'joi'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
import { data } from './lgtm-test-helpers.js'
|
||||
export const t = await createServiceTester()
|
||||
import { ServiceTester } from '../tester.js'
|
||||
|
||||
t.create('alerts: total alerts for a project')
|
||||
export const t = new ServiceTester({
|
||||
id: 'lgtmAlerts',
|
||||
title: 'LgtmAlerts',
|
||||
pathPrefix: '/lgtm/alerts',
|
||||
})
|
||||
|
||||
t.create('Lgtm')
|
||||
.get('/github/apache/cloudstack.json')
|
||||
.expectBadge({
|
||||
label: 'lgtm alerts',
|
||||
message: Joi.string().regex(/^[0-9kM.]+$/),
|
||||
})
|
||||
|
||||
t.create('alerts: missing project')
|
||||
.get('/github/some-org/this-project-doesnt-exist.json')
|
||||
.expectBadge({
|
||||
label: 'lgtm alerts',
|
||||
message: 'project not found',
|
||||
})
|
||||
|
||||
t.create('alerts: no alerts')
|
||||
.get('/github/apache/cloudstack.json')
|
||||
.intercept(nock =>
|
||||
nock('https://lgtm.com')
|
||||
.get('/api/v0.1/project/g/apache/cloudstack/details')
|
||||
.reply(200, { alerts: 0, languages: data.languages })
|
||||
)
|
||||
.expectBadge({ label: 'lgtm alerts', message: '0' })
|
||||
|
||||
t.create('alerts: single alert')
|
||||
.get('/github/apache/cloudstack.json')
|
||||
.intercept(nock =>
|
||||
nock('https://lgtm.com')
|
||||
.get('/api/v0.1/project/g/apache/cloudstack/details')
|
||||
.reply(200, { alerts: 1, languages: data.languages })
|
||||
)
|
||||
.expectBadge({ label: 'lgtm alerts', message: '1' })
|
||||
|
||||
t.create('alerts: multiple alerts')
|
||||
.get('/github/apache/cloudstack.json')
|
||||
.intercept(nock =>
|
||||
nock('https://lgtm.com')
|
||||
.get('/api/v0.1/project/g/apache/cloudstack/details')
|
||||
.reply(200, { alerts: 123, languages: data.languages })
|
||||
)
|
||||
.expectBadge({ label: 'lgtm alerts', message: '123' })
|
||||
|
||||
t.create('alerts: json missing alerts')
|
||||
.get('/github/apache/cloudstack.json')
|
||||
.intercept(nock =>
|
||||
nock('https://lgtm.com')
|
||||
.get('/api/v0.1/project/g/apache/cloudstack/details')
|
||||
.reply(200, {})
|
||||
)
|
||||
.expectBadge({ label: 'lgtm alerts', message: 'invalid response data' })
|
||||
|
||||
t.create('alerts: total alerts for a project with a github mapped host')
|
||||
.get('/github/apache/cloudstack.json')
|
||||
.expectBadge({
|
||||
label: 'lgtm alerts',
|
||||
message: Joi.string().regex(/^[0-9kM.]+$/),
|
||||
})
|
||||
.expectBadge({ label: 'lgtm alerts', message: 'no longer available' })
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import Joi from 'joi'
|
||||
import { BaseJsonService } from '../index.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
alerts: Joi.number().required(),
|
||||
|
||||
languages: Joi.array()
|
||||
.items(
|
||||
Joi.object({
|
||||
lang: Joi.string().required(),
|
||||
grade: Joi.string(),
|
||||
})
|
||||
)
|
||||
.required(),
|
||||
}).required()
|
||||
|
||||
const hostMappings = {
|
||||
github: 'g',
|
||||
bitbucket: 'b',
|
||||
gitlab: 'gl',
|
||||
}
|
||||
|
||||
export default class LgtmBaseService extends BaseJsonService {
|
||||
static category = 'analysis'
|
||||
|
||||
static defaultBadgeData = { label: 'lgtm' }
|
||||
|
||||
static pattern = `:host(${Object.keys(hostMappings).join('|')})/:user/:repo`
|
||||
|
||||
async fetch({ host, user, repo }) {
|
||||
const mappedHost = hostMappings[host]
|
||||
const url = `https://lgtm.com/api/v0.1/project/${mappedHost}/${user}/${repo}/details`
|
||||
|
||||
return this._requestJson({
|
||||
schema,
|
||||
url,
|
||||
errorMessages: {
|
||||
404: 'project not found',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,88 +1,11 @@
|
||||
import LgtmBaseService from './lgtm-base.js'
|
||||
import { deprecatedService } from '../index.js'
|
||||
|
||||
export default class LgtmGrade extends LgtmBaseService {
|
||||
static route = {
|
||||
export default deprecatedService({
|
||||
category: 'analysis',
|
||||
route: {
|
||||
base: 'lgtm/grade',
|
||||
pattern: `:language/${this.pattern}`,
|
||||
}
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'LGTM Grade',
|
||||
namedParams: {
|
||||
language: 'java',
|
||||
host: 'github',
|
||||
user: 'apache',
|
||||
repo: 'cloudstack',
|
||||
},
|
||||
staticPreview: this.render({
|
||||
language: 'java',
|
||||
data: {
|
||||
languages: [
|
||||
{
|
||||
lang: 'java',
|
||||
grade: 'C',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
},
|
||||
]
|
||||
|
||||
static getLabel({ language }) {
|
||||
const languageLabel = (() => {
|
||||
switch (language) {
|
||||
case 'cpp':
|
||||
return 'c/c++'
|
||||
case 'csharp':
|
||||
return 'c#'
|
||||
// Javascript analysis on LGTM also includes TypeScript
|
||||
case 'javascript':
|
||||
return 'js/ts'
|
||||
default:
|
||||
return language
|
||||
}
|
||||
})()
|
||||
return languageLabel
|
||||
}
|
||||
|
||||
static getGradeAndColor({ language, data }) {
|
||||
let grade = 'no language data'
|
||||
let color = 'red'
|
||||
|
||||
for (const languageData of data.languages) {
|
||||
if (languageData.lang === language && 'grade' in languageData) {
|
||||
// Pretty label for the language
|
||||
grade = languageData.grade
|
||||
// Pick colour based on grade
|
||||
if (languageData.grade === 'A+') {
|
||||
color = 'brightgreen'
|
||||
} else if (languageData.grade === 'A') {
|
||||
color = 'green'
|
||||
} else if (languageData.grade === 'B') {
|
||||
color = 'yellowgreen'
|
||||
} else if (languageData.grade === 'C') {
|
||||
color = 'yellow'
|
||||
} else if (languageData.grade === 'D') {
|
||||
color = 'orange'
|
||||
}
|
||||
}
|
||||
}
|
||||
return { grade, color }
|
||||
}
|
||||
|
||||
static render({ language, data }) {
|
||||
const { grade, color } = this.getGradeAndColor({ language, data })
|
||||
|
||||
return {
|
||||
label: `code quality: ${this.getLabel({ language })}`,
|
||||
message: grade,
|
||||
color,
|
||||
}
|
||||
}
|
||||
|
||||
async handle({ language, host, user, repo }) {
|
||||
const data = await this.fetch({ host, user, repo })
|
||||
return this.constructor.render({ language, data })
|
||||
}
|
||||
}
|
||||
pattern: ':various*',
|
||||
},
|
||||
label: 'lgtm grade',
|
||||
dateAdded: new Date('2023-01-03'),
|
||||
})
|
||||
|
||||
@@ -1,106 +1,11 @@
|
||||
import Joi from 'joi'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
import { data } from './lgtm-test-helpers.js'
|
||||
export const t = await createServiceTester()
|
||||
import { ServiceTester } from '../tester.js'
|
||||
|
||||
t.create('grade: missing project')
|
||||
.get('/java/github/some-org/this-project-doesnt-exist.json')
|
||||
.expectBadge({
|
||||
label: 'lgtm',
|
||||
message: 'project not found',
|
||||
})
|
||||
export const t = new ServiceTester({
|
||||
id: 'lgtmGrade',
|
||||
title: 'LgtmGrade',
|
||||
pathPrefix: '/lgtm/grade',
|
||||
})
|
||||
|
||||
t.create('grade: json missing languages')
|
||||
.get('/java/github/apache/cloudstack.json')
|
||||
.intercept(nock =>
|
||||
nock('https://lgtm.com')
|
||||
.get('/api/v0.1/project/g/apache/cloudstack/details')
|
||||
.reply(200, {})
|
||||
)
|
||||
.expectBadge({ label: 'lgtm', message: 'invalid response data' })
|
||||
|
||||
t.create('grade: grade for a project (java)')
|
||||
.get('/java/github/apache/cloudstack.json')
|
||||
.expectBadge({
|
||||
label: 'code quality: java',
|
||||
message: Joi.string().regex(/^(?:A\+)|A|B|C|D|E$/),
|
||||
})
|
||||
|
||||
t.create('grade: grade for missing language')
|
||||
.get('/foo/github/apache/cloudstack.json')
|
||||
.expectBadge({
|
||||
label: 'code quality: foo',
|
||||
message: 'no language data',
|
||||
})
|
||||
|
||||
t.create('grade: grade for a project with a mapped host')
|
||||
.get('/java/github/apache/cloudstack.json')
|
||||
.expectBadge({
|
||||
label: 'code quality: java',
|
||||
message: Joi.string().regex(/^(?:A\+)|A|B|C|D|E$/),
|
||||
})
|
||||
|
||||
// Test display of languages
|
||||
|
||||
t.create('grade: cpp')
|
||||
.get('/cpp/github/apache/cloudstack.json')
|
||||
.intercept(nock =>
|
||||
nock('https://lgtm.com')
|
||||
.get('/api/v0.1/project/g/apache/cloudstack/details')
|
||||
.reply(200, data)
|
||||
)
|
||||
.expectBadge({ label: 'code quality: c/c++', message: 'A+' })
|
||||
|
||||
t.create('grade: javascript')
|
||||
.get('/javascript/github/apache/cloudstack.json')
|
||||
.intercept(nock =>
|
||||
nock('https://lgtm.com')
|
||||
.get('/api/v0.1/project/g/apache/cloudstack/details')
|
||||
.reply(200, data)
|
||||
)
|
||||
.expectBadge({ label: 'code quality: js/ts', message: 'A' })
|
||||
|
||||
t.create('grade: java')
|
||||
.get('/java/github/apache/cloudstack.json')
|
||||
.intercept(nock =>
|
||||
nock('https://lgtm.com')
|
||||
.get('/api/v0.1/project/g/apache/cloudstack/details')
|
||||
.reply(200, data)
|
||||
)
|
||||
.expectBadge({ label: 'code quality: java', message: 'B' })
|
||||
|
||||
t.create('grade: python')
|
||||
.get('/python/github/apache/cloudstack.json')
|
||||
.intercept(nock =>
|
||||
nock('https://lgtm.com')
|
||||
.get('/api/v0.1/project/g/apache/cloudstack/details')
|
||||
.reply(200, data)
|
||||
)
|
||||
.expectBadge({ label: 'code quality: python', message: 'C' })
|
||||
|
||||
t.create('grade: csharp')
|
||||
.get('/csharp/github/apache/cloudstack.json')
|
||||
.intercept(nock =>
|
||||
nock('https://lgtm.com')
|
||||
.get('/api/v0.1/project/g/apache/cloudstack/details')
|
||||
.reply(200, data)
|
||||
)
|
||||
.expectBadge({ label: 'code quality: c#', message: 'D' })
|
||||
|
||||
t.create('grade: other')
|
||||
.get('/other/github/apache/cloudstack.json')
|
||||
.intercept(nock =>
|
||||
nock('https://lgtm.com')
|
||||
.get('/api/v0.1/project/g/apache/cloudstack/details')
|
||||
.reply(200, data)
|
||||
)
|
||||
.expectBadge({ label: 'code quality: other', message: 'E' })
|
||||
|
||||
t.create('grade: foo (no grade for valid language)')
|
||||
.get('/foo/github/apache/cloudstack.json')
|
||||
.intercept(nock =>
|
||||
nock('https://lgtm.com')
|
||||
.get('/api/v0.1/project/g/apache/cloudstack/details')
|
||||
.reply(200, data)
|
||||
)
|
||||
.expectBadge({ label: 'code quality: foo', message: 'no language data' })
|
||||
t.create('Lgtm')
|
||||
.get('/github/apache/cloudstack.json')
|
||||
.expectBadge({ label: 'lgtm grade', message: 'no longer available' })
|
||||
|
||||
@@ -7,9 +7,9 @@ export const t = new ServiceTester({
|
||||
})
|
||||
|
||||
t.create('alerts')
|
||||
.get('/alerts/g/badges/shields.svg')
|
||||
.expectRedirect('/lgtm/alerts/github/badges/shields.svg')
|
||||
.get('/alerts/g/badges/shields.json')
|
||||
.expectBadge({ label: 'lgtm alerts', message: 'no longer available' })
|
||||
|
||||
t.create('grade')
|
||||
.get('/grade/java/g/apache/cloudstack.svg')
|
||||
.expectRedirect('/lgtm/grade/java/github/apache/cloudstack.svg')
|
||||
.get('/grade/java/g/apache/cloudstack.json')
|
||||
.expectBadge({ label: 'lgtm grade', message: 'no longer available' })
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
const data = {
|
||||
alerts: 0,
|
||||
languages: [
|
||||
{ lang: 'cpp', grade: 'A+' },
|
||||
{ lang: 'javascript', grade: 'A' },
|
||||
{ lang: 'java', grade: 'B' },
|
||||
{ lang: 'python', grade: 'C' },
|
||||
{ lang: 'csharp', grade: 'D' },
|
||||
{ lang: 'other', grade: 'E' },
|
||||
{ lang: 'foo' },
|
||||
],
|
||||
}
|
||||
|
||||
export { data }
|
||||
@@ -14,7 +14,7 @@ const queryParamSchema = Joi.object({
|
||||
|
||||
const documentation = `
|
||||
<p>To find your user id, you can use <a link target="_blank" href="https://prouser123.me/misc/mastodon-userid-lookup.html">this tool</a>.</p><br>
|
||||
<p>Alternatively you can make a request to <code><br>https://your.mastodon.server/.well-known/webfinger?resource=acct:{user}@{domain}</br></code></p>
|
||||
<p>Alternatively you can make a request to <code>https://your.mastodon.server/.well-known/webfinger?resource=acct:{user}@{domain}</code></p>
|
||||
<p>Failing that, you can also visit your profile page, where your user ID will be in the header in a tag like this: <code><link href='https://your.mastodon.server/api/salmon/{your-user-id}' rel='salmon'></code></p>
|
||||
`
|
||||
|
||||
|
||||
@@ -29,9 +29,9 @@ const matrixStateSchema = Joi.array()
|
||||
const documentation = `
|
||||
<p>
|
||||
In order for this badge to work, the host of your room must allow guest accounts or dummy accounts to register, and the room must be world readable (chat history visible to anyone).
|
||||
</br>
|
||||
<br>
|
||||
The following steps will show you how to setup the badge URL using the Element Matrix client.
|
||||
</br>
|
||||
<br>
|
||||
<ul>
|
||||
<li>Select the desired room inside the Element client</li>
|
||||
<li>Click on the room settings button (gear icon) located near the top right of the client</li>
|
||||
@@ -41,11 +41,11 @@ const documentation = `
|
||||
<li>Remove the starting hash character (<code>#</code>)</li>
|
||||
<li>The final badge URL should look something like this <code>/matrix/twim:matrix.org.svg</code></li>
|
||||
</ul>
|
||||
</br>
|
||||
<br>
|
||||
Some Matrix homeservers don't hold a server name matching where they live (e.g. if the homeserver <code>example.com</code> that created the room alias <code>#mysuperroom:example.com</code> lives at <code>matrix.example.com</code>).
|
||||
</br>
|
||||
<br>
|
||||
If that is the case of the homeserver that created the room alias used for generating the badge, you will need to add the server's FQDN (fully qualified domain name) as a query parameter.
|
||||
</br>
|
||||
<br>
|
||||
The final badge URL should then look something like this <code>/matrix/mysuperroom:example.com.svg?server_fqdn=matrix.example.com</code>.
|
||||
</p>
|
||||
`
|
||||
|
||||
@@ -31,11 +31,11 @@ const documentation = `
|
||||
is a set of tools to analyze your website
|
||||
and inform you if you are utilizing the many available methods to secure it.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
By default the scan result is hidden from the public result list.
|
||||
You can activate the publication of the scan result
|
||||
by setting the <code>publish</code> parameter.
|
||||
<p>
|
||||
</p>
|
||||
<p>
|
||||
The badge returns a cached site result if the site has been scanned anytime in the previous 24 hours.
|
||||
If you need to force invalidating the cache,
|
||||
|
||||
@@ -81,8 +81,7 @@ export default class Nexus extends BaseJsonService {
|
||||
staticPreview: this.render({
|
||||
version: '3.9',
|
||||
}),
|
||||
documentation: `
|
||||
<p>
|
||||
documentation: `<p>
|
||||
Specifying 'nexusVersion=3' when targeting Nexus 3 servers will speed up the badge rendering.
|
||||
Note that you can use this query parameter with any Nexus badge type (Releases, Snapshots, or Repository).
|
||||
</p>
|
||||
@@ -132,8 +131,7 @@ export default class Nexus extends BaseJsonService {
|
||||
staticPreview: this.render({
|
||||
version: '7.0.1-SNAPSHOT',
|
||||
}),
|
||||
documentation: `
|
||||
<p>
|
||||
documentation: `<p>
|
||||
Note that you can use query options with any Nexus badge type (Releases, Snapshots, or Repository).
|
||||
</p>
|
||||
<p>
|
||||
@@ -144,7 +142,7 @@ export default class Nexus extends BaseJsonService {
|
||||
<ul>
|
||||
<li><a href="https://nexus.pentaho.org/swagger-ui/#/search/search">All Nexus 3 badges</a></li>
|
||||
<li><a href="https://repository.sonatype.org/nexus-restlet1x-plugin/default/docs/path__artifact_maven_resolve.html">Nexus 2 Releases and Snapshots badges</a></li>
|
||||
<li><a href=https://repository.sonatype.org/nexus-indexer-lucene-plugin/default/docs/path__lucene_search.html">Nexus 2 Repository badges</a></li>
|
||||
<li><a href="https://repository.sonatype.org/nexus-indexer-lucene-plugin/default/docs/path__lucene_search.html">Nexus 2 Repository badges</a></li>
|
||||
</ul>
|
||||
</p>
|
||||
`,
|
||||
|
||||
@@ -34,7 +34,7 @@ const resourceSchema = Joi.object({
|
||||
|
||||
const documentation = `
|
||||
<p>Your Plugin ID is the name of your plugin in lowercase, without any spaces or dashes.</p>
|
||||
<p>Example: <code>https://ore.spongepowered.org/Erigitic/Total-Economy</code> - Here the Plugin ID is <code>totaleconomy<code/>.`
|
||||
<p>Example: <code>https://ore.spongepowered.org/Erigitic/Total-Economy</code> - Here the Plugin ID is <code>totaleconomy</code>.</p>`
|
||||
|
||||
const keywords = ['sponge', 'spongemc', 'spongepowered']
|
||||
|
||||
|
||||
@@ -164,14 +164,12 @@ class BasePackagistService extends BaseJsonService {
|
||||
return versions.filter(version => version.version === release)[0]
|
||||
}
|
||||
}
|
||||
const customServerDocumentationFragment = `
|
||||
<p>
|
||||
const customServerDocumentationFragment = `<p>
|
||||
Note that only network-accessible packagist.org and other self-hosted Packagist instances are supported.
|
||||
</p>
|
||||
`
|
||||
|
||||
const cacheDocumentationFragment = `
|
||||
<p>
|
||||
const cacheDocumentationFragment = `<p>
|
||||
Displayed data may be slightly outdated.
|
||||
Due to performance reasons, data fetched from packagist JSON API is cached for twelve hours on packagist infrastructure.
|
||||
For more information please refer to <a target="_blank" href="https://packagist.org/apidoc#get-package-data">official packagist documentation</a>.
|
||||
|
||||
@@ -2,14 +2,23 @@
|
||||
* Utilities relating to PHP version numbers. This compares version numbers
|
||||
* using the algorithm followed by Composer (see
|
||||
* https://getcomposer.org/doc/04-schema.md#version).
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { fetch } from '../core/base-service/got.js'
|
||||
import { getCachedResource } from '../core/base-service/resource-cache.js'
|
||||
import { listCompare } from './version.js'
|
||||
import { omitv } from './text-formatters.js'
|
||||
|
||||
// Return a negative value if v1 < v2,
|
||||
// zero if v1 = v2, a positive value otherwise.
|
||||
/**
|
||||
* Return a negative value if v1 < v2,
|
||||
* zero if v1 = v2, a positive value otherwise.
|
||||
*
|
||||
* @param {string} v1 - First version for comparison
|
||||
* @param {string} v2 - Second version for comparison
|
||||
* @returns {number} Comparison result (-1, 0 or 1)
|
||||
*/
|
||||
function asciiVersionCompare(v1, v2) {
|
||||
if (v1 < v2) {
|
||||
return -1
|
||||
@@ -20,9 +29,14 @@ function asciiVersionCompare(v1, v2) {
|
||||
}
|
||||
}
|
||||
|
||||
// Take a version without the starting v.
|
||||
// eg, '1.0.x-beta'
|
||||
// Return { numbers: [1,0,something big], modifier: 2, modifierCount: 1 }
|
||||
/**
|
||||
* Take a version without the starting v.
|
||||
* eg, '1.0.x-beta'
|
||||
* Return { numbers: [1,0,something big], modifier: 2, modifierCount: 1 }
|
||||
*
|
||||
* @param {string} version - Version number string
|
||||
* @returns {object} Object containing version details
|
||||
*/
|
||||
function numberedVersionData(version) {
|
||||
// A version has a numbered part and a modifier part
|
||||
// (eg, 1.0.0-patch, 2.0.x-dev).
|
||||
@@ -96,7 +110,12 @@ function numberedVersionData(version) {
|
||||
}
|
||||
}
|
||||
|
||||
// Try to convert to a list of numbers.
|
||||
/**
|
||||
* Try to convert to a list of numbers.
|
||||
*
|
||||
* @param {string} s - Version number string
|
||||
* @returns {number} Version number interger
|
||||
*/
|
||||
function toNum(s) {
|
||||
let n = +s
|
||||
if (Number.isNaN(n)) {
|
||||
@@ -113,12 +132,15 @@ function numberedVersionData(version) {
|
||||
}
|
||||
}
|
||||
|
||||
// Return a negative value if v1 < v2,
|
||||
// zero if v1 = v2,
|
||||
// a positive value otherwise.
|
||||
//
|
||||
// See https://getcomposer.org/doc/04-schema.md#version
|
||||
// and https://github.com/badges/shields/issues/319#issuecomment-74411045
|
||||
/**
|
||||
* Compares two versions and return an interger based on the result.
|
||||
* See https://getcomposer.org/doc/04-schema.md#version
|
||||
* and https://github.com/badges/shields/issues/319#issuecomment-74411045
|
||||
*
|
||||
* @param {string} v1 - First version
|
||||
* @param {string} v2 - Second version
|
||||
* @returns {number} Negative value if v1 < v2, zero if v1 = v2, else a positive value
|
||||
*/
|
||||
function compare(v1, v2) {
|
||||
// Omit the starting `v`.
|
||||
const rawv1 = omitv(v1)
|
||||
@@ -154,6 +176,12 @@ function compare(v1, v2) {
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the latest version from a list of versions.
|
||||
*
|
||||
* @param {string[]} versions - List of versions
|
||||
* @returns {string} Latest version
|
||||
*/
|
||||
function latest(versions) {
|
||||
let latest = versions[0]
|
||||
for (let i = 1; i < versions.length; i++) {
|
||||
@@ -164,6 +192,12 @@ function latest(versions) {
|
||||
return latest
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a version is stable or not.
|
||||
*
|
||||
* @param {string} version - Version number
|
||||
* @returns {boolean} true if version is stable, else false
|
||||
*/
|
||||
function isStable(version) {
|
||||
const rawVersion = omitv(version)
|
||||
let versionData
|
||||
@@ -176,6 +210,12 @@ function isStable(version) {
|
||||
return versionData.modifier === 3 || versionData.modifier === 4
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a version is valid and returns the minor version.
|
||||
*
|
||||
* @param {string} version - Version number
|
||||
* @returns {string} Minor version
|
||||
*/
|
||||
function minorVersion(version) {
|
||||
const result = version.match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/)
|
||||
|
||||
@@ -186,6 +226,13 @@ function minorVersion(version) {
|
||||
return `${result[1]}.${result[2] ? result[2] : '0'}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces the list of php versions that intersect with release versions to a version range (for eg. '5.4 - 7.1', '>= 5.5').
|
||||
*
|
||||
* @param {string[]} versions - List of php versions
|
||||
* @param {string[]} phpReleases - List of php release versions
|
||||
* @returns {string[]} Reduced Version Range (for eg. ['5.4 - 7.1'], ['>= 5.5'])
|
||||
*/
|
||||
function versionReduction(versions, phpReleases) {
|
||||
if (!versions.length) {
|
||||
return []
|
||||
@@ -216,6 +263,13 @@ function versionReduction(versions, phpReleases) {
|
||||
return versions
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the PHP release versions from cache if exists, else fetch from the souce url and save in cache.
|
||||
*
|
||||
* @async
|
||||
* @param {object} githubApiProvider - Github API provider
|
||||
* @returns {Promise<*>} Promise that resolves to parsed response
|
||||
*/
|
||||
async function getPhpReleases(githubApiProvider) {
|
||||
return getCachedResource({
|
||||
url: '/repos/php/php-src/git/refs/tags',
|
||||
|
||||
@@ -1,11 +1,28 @@
|
||||
/**
|
||||
* Common functions and utilities for tasks related to pipenv
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import Joi from 'joi'
|
||||
import { InvalidParameter } from './index.js'
|
||||
|
||||
/**
|
||||
* Joi schema for validating dependency.
|
||||
*
|
||||
* @type {Joi}
|
||||
*/
|
||||
const isDependency = Joi.object({
|
||||
version: Joi.string(),
|
||||
ref: Joi.string(),
|
||||
}).required()
|
||||
|
||||
/**
|
||||
* Joi schema for validating lock file object.
|
||||
* Checks if the lock file object has required properties and the properties are valid.
|
||||
*
|
||||
* @type {Joi}
|
||||
*/
|
||||
const isLockfile = Joi.object({
|
||||
_meta: Joi.object({
|
||||
requires: Joi.object({
|
||||
@@ -16,6 +33,18 @@ const isLockfile = Joi.object({
|
||||
develop: Joi.object().pattern(Joi.string(), isDependency),
|
||||
}).required()
|
||||
|
||||
/**
|
||||
* Determines the dependency version based on the dependency type.
|
||||
*
|
||||
* @param {object} attrs - Refer to individual attributes
|
||||
* @param {string} attrs.kind - Wanted dependency type ('dev' or 'default'), defaults to 'default'
|
||||
* @param {string} attrs.wantedDependency - Name of the wanted dependency
|
||||
* @param {object} attrs.lockfileData - Object containing lock file data
|
||||
* @throws {Error} - Error if unknown dependency type provided
|
||||
* @throws {InvalidParameter} - Error if wanted dependency is not present in lock file data
|
||||
* @throws {InvalidParameter} - Error if version or ref is not present for the wanted dependency
|
||||
* @returns {object} Object containing wanted dependency version or ref
|
||||
*/
|
||||
function getDependencyVersion({
|
||||
kind = 'default',
|
||||
wantedDependency,
|
||||
|
||||
51
services/polymart/polymart-base.js
Normal file
51
services/polymart/polymart-base.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import Joi from 'joi'
|
||||
import { BaseJsonService } from '../index.js'
|
||||
|
||||
const resourceSchema = Joi.object({
|
||||
response: Joi.object({
|
||||
resource: Joi.object({
|
||||
price: Joi.number().required(),
|
||||
downloads: Joi.string().required(),
|
||||
reviews: Joi.object({
|
||||
count: Joi.number().required(),
|
||||
stars: Joi.number().required(),
|
||||
}).required(),
|
||||
updates: Joi.object({
|
||||
latest: Joi.object({
|
||||
version: Joi.string().required(),
|
||||
}).required(),
|
||||
}).required(),
|
||||
}).required(),
|
||||
}).required(),
|
||||
}).required()
|
||||
|
||||
const notFoundResourceSchema = Joi.object({
|
||||
response: Joi.object({
|
||||
success: Joi.boolean().required(),
|
||||
errors: Joi.object().required(),
|
||||
}).required(),
|
||||
})
|
||||
|
||||
const resourceFoundOrNotSchema = Joi.alternatives(
|
||||
resourceSchema,
|
||||
notFoundResourceSchema
|
||||
)
|
||||
|
||||
const documentation = `
|
||||
<p>You can find your resource ID in the url for your resource page.</p>
|
||||
<p>Example: <code>https://polymart.org/resource/polymart-plugin.323</code> - Here the Resource ID is 323.</p>`
|
||||
|
||||
class BasePolymartService extends BaseJsonService {
|
||||
async fetch({
|
||||
resourceId,
|
||||
schema = resourceFoundOrNotSchema,
|
||||
url = `https://api.polymart.org/v1/getResourceInfo/?resource_id=${resourceId}`,
|
||||
}) {
|
||||
return this._requestJson({
|
||||
schema,
|
||||
url,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export { documentation, BasePolymartService }
|
||||
35
services/polymart/polymart-downloads.service.js
Normal file
35
services/polymart/polymart-downloads.service.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { NotFound } from '../../core/base-service/errors.js'
|
||||
import { renderDownloadsBadge } from '../downloads.js'
|
||||
import { BasePolymartService, documentation } from './polymart-base.js'
|
||||
|
||||
export default class PolymartDownloads extends BasePolymartService {
|
||||
static category = 'downloads'
|
||||
|
||||
static route = {
|
||||
base: 'polymart/downloads',
|
||||
pattern: ':resourceId',
|
||||
}
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'Polymart Downloads',
|
||||
namedParams: {
|
||||
resourceId: '323',
|
||||
},
|
||||
staticPreview: renderDownloadsBadge({ downloads: 655 }),
|
||||
documentation,
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'downloads',
|
||||
}
|
||||
|
||||
async handle({ resourceId }) {
|
||||
const { response } = await this.fetch({ resourceId })
|
||||
if (!response.resource) {
|
||||
throw new NotFound()
|
||||
}
|
||||
return renderDownloadsBadge({ downloads: response.resource.downloads })
|
||||
}
|
||||
}
|
||||
13
services/polymart/polymart-downloads.tester.js
Normal file
13
services/polymart/polymart-downloads.tester.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { isMetric } from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('Polymart Plugin (id 323)').get('/323.json').expectBadge({
|
||||
label: 'downloads',
|
||||
message: isMetric,
|
||||
})
|
||||
|
||||
t.create('Invalid Resource (id 0)').get('/0.json').expectBadge({
|
||||
label: 'downloads',
|
||||
message: 'not found',
|
||||
})
|
||||
38
services/polymart/polymart-latest-version.service.js
Normal file
38
services/polymart/polymart-latest-version.service.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { NotFound } from '../../core/base-service/errors.js'
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
import { BasePolymartService, documentation } from './polymart-base.js'
|
||||
export default class PolymartLatestVersion extends BasePolymartService {
|
||||
static category = 'version'
|
||||
|
||||
static route = {
|
||||
base: 'polymart/version',
|
||||
pattern: ':resourceId',
|
||||
}
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'Polymart Version',
|
||||
namedParams: {
|
||||
resourceId: '323',
|
||||
},
|
||||
staticPreview: renderVersionBadge({
|
||||
version: 'v1.2.9',
|
||||
}),
|
||||
documentation,
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'polymart',
|
||||
}
|
||||
|
||||
async handle({ resourceId }) {
|
||||
const { response } = await this.fetch({ resourceId })
|
||||
if (!response.resource) {
|
||||
throw new NotFound()
|
||||
}
|
||||
return renderVersionBadge({
|
||||
version: response.resource.updates.latest.version,
|
||||
})
|
||||
}
|
||||
}
|
||||
13
services/polymart/polymart-latest-version.tester.js
Normal file
13
services/polymart/polymart-latest-version.tester.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { isVPlusDottedVersionNClauses } from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('Polymart Plugin (id 323)').get('/323.json').expectBadge({
|
||||
label: 'polymart',
|
||||
message: isVPlusDottedVersionNClauses,
|
||||
})
|
||||
|
||||
t.create('Invalid Resource (id 0)').get('/0.json').expectBadge({
|
||||
label: 'polymart',
|
||||
message: 'not found',
|
||||
})
|
||||
65
services/polymart/polymart-rating.service.js
Normal file
65
services/polymart/polymart-rating.service.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import { starRating, metric } from '../text-formatters.js'
|
||||
import { floorCount } from '../color-formatters.js'
|
||||
import { NotFound } from '../../core/base-service/errors.js'
|
||||
import { BasePolymartService, documentation } from './polymart-base.js'
|
||||
|
||||
export default class PolymartRatings extends BasePolymartService {
|
||||
static category = 'rating'
|
||||
|
||||
static route = {
|
||||
base: 'polymart',
|
||||
pattern: ':format(rating|stars)/:resourceId',
|
||||
}
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'Polymart Stars',
|
||||
pattern: 'stars/:resourceId',
|
||||
namedParams: {
|
||||
resourceId: '323',
|
||||
},
|
||||
staticPreview: this.render({
|
||||
format: 'stars',
|
||||
total: 14,
|
||||
average: 5,
|
||||
}),
|
||||
documentation,
|
||||
},
|
||||
{
|
||||
title: 'Polymart Rating',
|
||||
pattern: 'rating/:resourceId',
|
||||
namedParams: {
|
||||
resourceId: '323',
|
||||
},
|
||||
staticPreview: this.render({ total: 14, average: 5 }),
|
||||
documentation,
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'rating',
|
||||
}
|
||||
|
||||
static render({ format, total, average }) {
|
||||
const message =
|
||||
format === 'stars'
|
||||
? starRating(average)
|
||||
: `${average}/5 (${metric(total)})`
|
||||
return {
|
||||
message,
|
||||
color: floorCount(average, 2, 3, 4),
|
||||
}
|
||||
}
|
||||
|
||||
async handle({ format, resourceId }) {
|
||||
const { response } = await this.fetch({ resourceId })
|
||||
if (!response.resource) {
|
||||
throw new NotFound()
|
||||
}
|
||||
return this.constructor.render({
|
||||
format,
|
||||
total: response.resource.reviews.count,
|
||||
average: response.resource.reviews.stars.toFixed(2),
|
||||
})
|
||||
}
|
||||
}
|
||||
27
services/polymart/polymart-rating.tester.js
Normal file
27
services/polymart/polymart-rating.tester.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { isStarRating, withRegex } from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('Stars - Polymart Plugin (id 323)')
|
||||
.get('/stars/323.json')
|
||||
.expectBadge({
|
||||
label: 'rating',
|
||||
message: isStarRating,
|
||||
})
|
||||
|
||||
t.create('Stars - Invalid Resource (id 0)').get('/stars/0.json').expectBadge({
|
||||
label: 'rating',
|
||||
message: 'not found',
|
||||
})
|
||||
|
||||
t.create('Rating - Polymart Plugin (id 323)')
|
||||
.get('/rating/323.json')
|
||||
.expectBadge({
|
||||
label: 'rating',
|
||||
message: withRegex(/^(\d*\.\d+)(\/5 \()(\d+)(\))$/),
|
||||
})
|
||||
|
||||
t.create('Rating - Invalid Resource (id 0)').get('/rating/0.json').expectBadge({
|
||||
label: 'rating',
|
||||
message: 'not found',
|
||||
})
|
||||
@@ -13,9 +13,9 @@ const documentation = `
|
||||
provide an easy mechanism to analyze HTTP response headers and
|
||||
give information on how to deploy missing headers.
|
||||
</p>
|
||||
</p>
|
||||
The scan result will be hidden from the public result list and follow redirects will be on too.
|
||||
<p>
|
||||
The scan result will be hidden from the public result list and follow redirects will be on too.
|
||||
</p>
|
||||
`
|
||||
|
||||
export default class SecurityHeaders extends BaseService {
|
||||
|
||||
@@ -25,8 +25,7 @@ export default class SnykVulnerabilityGitHub extends SynkVulnerabilityBase {
|
||||
manifestFilePath: 'badge-maker/package.json',
|
||||
},
|
||||
staticPreview: this.render({ vulnerabilities: '0' }),
|
||||
documentation: `
|
||||
<p>
|
||||
documentation: `<p>
|
||||
Provide the path to your target manifest file relative to the base of your repository.
|
||||
Snyk does not support using a specific branch for this, so do not include "blob" nor a branch name.
|
||||
</p>
|
||||
|
||||
@@ -18,7 +18,7 @@ const documentation = `
|
||||
</p>
|
||||
<img
|
||||
src="https://user-images.githubusercontent.com/7288322/46567027-27c83400-c987-11e8-9850-ab67d987202f.png"
|
||||
alt="Right-Click and 'Copy Page URL'">
|
||||
alt="Right-Click and 'Copy Page URL'" />
|
||||
`
|
||||
|
||||
const steamCollectionSchema = Joi.object({
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Joi from 'joi'
|
||||
import { metric } from '../text-formatters.js'
|
||||
import { optionalUrl } from '../validators.js'
|
||||
import { BaseService, BaseJsonService, NotFound } from '../index.js'
|
||||
import { BaseService, BaseJsonService } from '../index.js'
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
url: optionalUrl.required(),
|
||||
@@ -33,6 +32,8 @@ class TwitterUrl extends BaseService {
|
||||
},
|
||||
]
|
||||
|
||||
static _cacheLength = 86400
|
||||
|
||||
static defaultBadgeData = {
|
||||
namedLogo: 'twitter',
|
||||
}
|
||||
@@ -51,8 +52,19 @@ class TwitterUrl extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
const schema = Joi.any()
|
||||
/*
|
||||
This badge is unusual.
|
||||
|
||||
We don't usually host badges that don't show any dynamic information.
|
||||
Also when an upstream API is removed, we usually deprecate/remove badges
|
||||
according to the process in
|
||||
https://github.com/badges/shields/blob/master/doc/deprecating-badges.md
|
||||
|
||||
In the case of twitter, we decided to provide a static fallback instead
|
||||
due to how widely used the badge was. See
|
||||
https://github.com/badges/shields/issues/8837
|
||||
for related discussion.
|
||||
*/
|
||||
class TwitterFollow extends BaseJsonService {
|
||||
static category = 'social'
|
||||
|
||||
@@ -65,51 +77,40 @@ class TwitterFollow extends BaseJsonService {
|
||||
{
|
||||
title: 'Twitter Follow',
|
||||
namedParams: {
|
||||
user: 'espadrine',
|
||||
user: 'shields_io',
|
||||
},
|
||||
queryParams: { label: 'Follow' },
|
||||
// hard code the static preview
|
||||
// because link[] is not allowed in examples
|
||||
staticPreview: {
|
||||
label: 'Follow',
|
||||
message: '393',
|
||||
label: 'Follow @shields_io',
|
||||
message: '',
|
||||
style: 'social',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
static _cacheLength = 86400
|
||||
|
||||
static defaultBadgeData = {
|
||||
namedLogo: 'twitter',
|
||||
}
|
||||
|
||||
static render({ user, followers }) {
|
||||
static render({ user }) {
|
||||
return {
|
||||
label: `follow @${user}`,
|
||||
message: metric(followers),
|
||||
message: '',
|
||||
style: 'social',
|
||||
link: [
|
||||
`https://twitter.com/intent/follow?screen_name=${encodeURIComponent(
|
||||
user
|
||||
)}`,
|
||||
`https://twitter.com/${encodeURIComponent(user)}/followers`,
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
async fetch({ user }) {
|
||||
return this._requestJson({
|
||||
schema,
|
||||
url: 'http://cdn.syndication.twimg.com/widgets/followbutton/info.json',
|
||||
options: { searchParams: { screen_names: user } },
|
||||
})
|
||||
}
|
||||
|
||||
async handle({ user }) {
|
||||
const data = await this.fetch({ user })
|
||||
if (!Array.isArray(data) || data.length === 0) {
|
||||
throw new NotFound({ prettyMessage: 'invalid user' })
|
||||
}
|
||||
return this.constructor.render({ user, followers: data[0].followers_count })
|
||||
return this.constructor.render({ user })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { isMetric } from '../test-validators.js'
|
||||
import { ServiceTester } from '../tester.js'
|
||||
|
||||
export const t = new ServiceTester({
|
||||
@@ -10,25 +9,8 @@ t.create('Followers')
|
||||
.get('/follow/shields_io.json')
|
||||
.expectBadge({
|
||||
label: 'follow @shields_io',
|
||||
message: isMetric,
|
||||
link: [
|
||||
'https://twitter.com/intent/follow?screen_name=shields_io',
|
||||
'https://twitter.com/shields_io/followers',
|
||||
],
|
||||
})
|
||||
|
||||
t.create('Invalid Username Specified (non-existent user)')
|
||||
.get('/follow/invalidusernamethatshouldnotexist.json?label=Follow')
|
||||
.expectBadge({
|
||||
label: 'Follow',
|
||||
message: 'invalid user',
|
||||
})
|
||||
|
||||
t.create('Invalid Username Specified (only spaces)')
|
||||
.get('/follow/%20%20.json?label=Follow')
|
||||
.expectBadge({
|
||||
label: 'Follow',
|
||||
message: 'invalid user',
|
||||
message: '',
|
||||
link: ['https://twitter.com/intent/follow?screen_name=shields_io'],
|
||||
})
|
||||
|
||||
t.create('URL')
|
||||
|
||||
49
services/vcpkg/vcpkg-version.service.js
Normal file
49
services/vcpkg/vcpkg-version.service.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import Joi from 'joi'
|
||||
import { ConditionalGithubAuthV3Service } from '../github/github-auth-service.js'
|
||||
import { fetchJsonFromRepo } from '../github/github-common-fetch.js'
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
import { NotFound } from '../index.js'
|
||||
|
||||
const vcpkgManifestSchema = Joi.object({
|
||||
version: Joi.string().required(),
|
||||
}).required()
|
||||
|
||||
export default class VcpkgVersion extends ConditionalGithubAuthV3Service {
|
||||
static category = 'version'
|
||||
|
||||
static route = { base: 'vcpkg/v', pattern: ':portName' }
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'Vcpkg',
|
||||
namedParams: { portName: 'entt' },
|
||||
staticPreview: this.render({ version: '3.11.1' }),
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = { label: 'vcpkg' }
|
||||
|
||||
static render({ version }) {
|
||||
return renderVersionBadge({ version })
|
||||
}
|
||||
|
||||
async handle({ portName }) {
|
||||
try {
|
||||
const { version } = await fetchJsonFromRepo(this, {
|
||||
schema: vcpkgManifestSchema,
|
||||
user: 'microsoft',
|
||||
repo: 'vcpkg',
|
||||
branch: 'master',
|
||||
filename: `ports/${portName}/vcpkg.json`,
|
||||
})
|
||||
return this.constructor.render({ version })
|
||||
} catch (error) {
|
||||
if (error instanceof NotFound) {
|
||||
throw new NotFound({
|
||||
prettyMessage: 'port not found',
|
||||
})
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
16
services/vcpkg/vcpkg-version.tester.js
Normal file
16
services/vcpkg/vcpkg-version.tester.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { isSemver } from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('gets the port version of entt')
|
||||
.get('/entt.json')
|
||||
.expectBadge({ label: 'vcpkg', message: isSemver })
|
||||
|
||||
t.create('returns not found for invalid port')
|
||||
.get('/this-port-does-not-exist.json')
|
||||
.expectBadge({
|
||||
label: 'vcpkg',
|
||||
color: 'red',
|
||||
message: 'port not found',
|
||||
})
|
||||
@@ -102,11 +102,10 @@ const documentation = `
|
||||
This badge relies on the <a target="_blank" href="https://validator.nu/">https://validator.nu/</a> service to perform the validation.
|
||||
Please refer to <a target="_blank" href="https://about.validator.nu/">https://about.validator.nu/</a> for the full documentation and Terms of service.
|
||||
The following are required from the consumer for the badge to function.
|
||||
|
||||
<ul class="note">
|
||||
<li>
|
||||
Path:
|
||||
<ul>
|
||||
<ul>
|
||||
<li>
|
||||
parser: The parser that is used for validation. This is a passthru value to the service
|
||||
<ul>
|
||||
@@ -115,8 +114,8 @@ const documentation = `
|
||||
<li>xml <i>(XML; don’t load external entities)</i></li>
|
||||
<li>xmldtd <i>(XML; load external entities)</i></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
Query string:
|
||||
@@ -140,7 +139,7 @@ const documentation = `
|
||||
<li>SVG 1.1, URL, XHTML, MathML 3.0</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
@@ -13,7 +13,6 @@ const documentation = `
|
||||
<li><code>ParserFunctions</code></li>
|
||||
<li><code>parserFunctions</code></li>
|
||||
</ul>
|
||||
|
||||
However, the following are invalid:
|
||||
<ul>
|
||||
<li><code>parserfunctions</code></li>
|
||||
|
||||
Reference in New Issue
Block a user