Compare commits
101 Commits
server-202
...
services-g
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0ef5046d0 | ||
|
|
f60c2058fa | ||
|
|
39e4d9bcbc | ||
|
|
2bf863fb09 | ||
|
|
ed277d4e79 | ||
|
|
fdc81c2e3a | ||
|
|
6ef9dcaba5 | ||
|
|
541cb9acf2 | ||
|
|
2492ff79f7 | ||
|
|
0c73b35915 | ||
|
|
f1d151e963 | ||
|
|
a0149a8f8f | ||
|
|
e843d4eac1 | ||
|
|
830b5d8a1f | ||
|
|
8afb034a58 | ||
|
|
2f915a7b45 | ||
|
|
7dbfd0d049 | ||
|
|
493fdb76af | ||
|
|
abb1bbf8d4 | ||
|
|
a4911dac33 | ||
|
|
5e6583c530 | ||
|
|
bb1fda2aa7 | ||
|
|
606ea0ad54 | ||
|
|
00e37a6888 | ||
|
|
c3d08f7f8b | ||
|
|
8be87554d6 | ||
|
|
df4c6d14dd | ||
|
|
5830810451 | ||
|
|
ac2ae0c180 | ||
|
|
efd707eb65 | ||
|
|
f67251a9ca | ||
|
|
00f35c67e8 | ||
|
|
7a38cfe099 | ||
|
|
b32dc3e894 | ||
|
|
e8798a437a | ||
|
|
80ffafc422 | ||
|
|
8808e0dd7b | ||
|
|
146598f5f4 | ||
|
|
41c53c73c2 | ||
|
|
dd3e2df00a | ||
|
|
271547d2c6 | ||
|
|
300871ac65 | ||
|
|
af25802b68 | ||
|
|
5caab6724f | ||
|
|
73b8e78143 | ||
|
|
022fe54141 | ||
|
|
06cdf34c3d | ||
|
|
6049ef64c8 | ||
|
|
a276770cdb | ||
|
|
61dd6c0443 | ||
|
|
2d254acd20 | ||
|
|
2e18afc062 | ||
|
|
ddd25e0d47 | ||
|
|
7d2d930486 | ||
|
|
c26b90f4e7 | ||
|
|
eef6057c78 | ||
|
|
a67dda19bc | ||
|
|
e1622e800c | ||
|
|
5e78eccf56 | ||
|
|
7207df085d | ||
|
|
367b83b0b6 | ||
|
|
908cc8de93 | ||
|
|
4d785cd459 | ||
|
|
8dcd0d1ffd | ||
|
|
8754ac3798 | ||
|
|
93016b04ff | ||
|
|
b6be37d277 | ||
|
|
4da9e7d58f | ||
|
|
affae9c521 | ||
|
|
73bbe80eaa | ||
|
|
9cd16402db | ||
|
|
c49ee023dd | ||
|
|
e2fcd1787b | ||
|
|
c008dcfbb6 | ||
|
|
d769de309e | ||
|
|
478c0545ac | ||
|
|
ac1a4d4587 | ||
|
|
882fb9d267 | ||
|
|
2cdd89e8a2 | ||
|
|
eefb16a6ec | ||
|
|
53c5cfa94d | ||
|
|
9c692cd53a | ||
|
|
7410bf2e97 | ||
|
|
a83cfa4fb6 | ||
|
|
4d64969738 | ||
|
|
ade213c6d3 | ||
|
|
1135fba9f6 | ||
|
|
0234fb077f | ||
|
|
ed86c1de21 | ||
|
|
a47f770c82 | ||
|
|
3176d6f7f3 | ||
|
|
0763d8ec66 | ||
|
|
3fcb959ed2 | ||
|
|
f562dfe868 | ||
|
|
22aee48544 | ||
|
|
098a24cae5 | ||
|
|
a2c8ed27b8 | ||
|
|
2dccd3d040 | ||
|
|
b8ce38a041 | ||
|
|
0784a14153 | ||
|
|
afc7b283bc |
@@ -1,155 +1,3 @@
|
||||
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
|
||||
|
||||
frontend:
|
||||
docker:
|
||||
- image: cimg/node:16.15
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: |
|
||||
npm ci
|
||||
environment:
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
|
||||
- run:
|
||||
name: Prepare frontend tests
|
||||
command: npm run defs && npm run features
|
||||
|
||||
- run:
|
||||
name: Check types
|
||||
command: npm run check-types:frontend
|
||||
|
||||
- run:
|
||||
name: Frontend unit tests
|
||||
environment:
|
||||
mocha_reporter: mocha-junit-reporter
|
||||
MOCHA_FILE: junit/frontend/results.xml
|
||||
when: always
|
||||
command: npm run test:frontend
|
||||
|
||||
- store_test_results:
|
||||
path: junit
|
||||
|
||||
- run:
|
||||
name: Frontend build completes successfully
|
||||
when: always
|
||||
command: npm run build
|
||||
|
||||
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:
|
||||
- frontend:
|
||||
filters:
|
||||
branches:
|
||||
ignore: gh-pages
|
||||
- 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
|
||||
# - frontend:
|
||||
# requires:
|
||||
# - npm-install
|
||||
# - 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\/.*/
|
||||
# Do nothing
|
||||
# TODO: disable Circle
|
||||
|
||||
37
.github/ISSUE_TEMPLATE/3_Badge_request.md
vendored
37
.github/ISSUE_TEMPLATE/3_Badge_request.md
vendored
@@ -1,37 +0,0 @@
|
||||
---
|
||||
name: 💡 Badge Request
|
||||
about: Ideas for new badges
|
||||
labels: 'service-badge'
|
||||
---
|
||||
|
||||
:clipboard: **Description**
|
||||
|
||||
<!--
|
||||
A clear and concise description of the new badge.
|
||||
|
||||
- Which service is this badge for e.g: GitHub, Travis CI
|
||||
- What sort of information should this badge show?
|
||||
Provide an example in plain text e.g: "version | v1.01" or as a static badge
|
||||
(static badge generator can be found at https://shields.io)
|
||||
-->
|
||||
|
||||
:link: **Data**
|
||||
|
||||
<!--
|
||||
Where can we get the data from?
|
||||
|
||||
- Is there a public API?
|
||||
- Does the API require an API key?
|
||||
- Link to the API documentation.
|
||||
-->
|
||||
|
||||
:microphone: **Motivation**
|
||||
|
||||
<!--
|
||||
Please explain why this feature should be implemented and how it would be used.
|
||||
|
||||
- What is the specific use case?
|
||||
-->
|
||||
|
||||
<!-- Love Shields? Please consider donating $10 to sustain our activities:
|
||||
👉 https://opencollective.com/shields -->
|
||||
62
.github/ISSUE_TEMPLATE/3_Badge_request.yml
vendored
Normal file
62
.github/ISSUE_TEMPLATE/3_Badge_request.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
name: '💡 Badge Request'
|
||||
description: Ideas for new badges
|
||||
labels: ['service-badge']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
## Ideas for new badges
|
||||
|
||||
|
||||
This issue template is for suggesting new badges which
|
||||
**fetch and display data from an upstream service**.
|
||||
If your suggestion is for a static badge
|
||||
(which shows the same information every time it is requested), it is
|
||||
[already possible to make these](https://github.com/badges/shields/blob/master/doc/static-badges.md).
|
||||
We don't add specific routes for badges which only show static information.
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: '📋 Description'
|
||||
description: |
|
||||
A clear and concise description of the new badge.
|
||||
|
||||
- Which service is this badge for e.g: GitHub, Travis CI
|
||||
- What sort of information should this badge show?
|
||||
Provide an example in plain text e.g: "version | v1.01" or as a static badge
|
||||
(static badge generator can be found at https://shields.io/#your-badge )
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: data
|
||||
attributes:
|
||||
label: '🔗 Data'
|
||||
description: |
|
||||
Where can we get the data from?
|
||||
|
||||
Please consider and cover details like:
|
||||
- Is there a public API?
|
||||
- Does the API require authentication or an API key?
|
||||
If so, please review our documentation on [Badges Requiring Authentication](https://github.com/badges/shields/blob/master/doc/authentication.md)
|
||||
- Link to the API documentation.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: motivation
|
||||
attributes:
|
||||
label: '🎤 Motivation'
|
||||
description: |
|
||||
Please explain why this feature should be implemented and how it would be used.
|
||||
|
||||
- What is the specific use case?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## :heart: Love Shields?
|
||||
Please consider donating $10 to sustain our activities: [https://opencollective.com/shields](https://opencollective.com/shields)
|
||||
2
.github/actions/close-bot/action.yml
vendored
2
.github/actions/close-bot/action.yml
vendored
@@ -8,5 +8,5 @@ inputs:
|
||||
description: 'The GITHUB_TOKEN secret'
|
||||
required: true
|
||||
runs:
|
||||
using: 'node12'
|
||||
using: 'node16'
|
||||
main: 'index.js'
|
||||
|
||||
31
.github/actions/frontend-tests/action.yml
vendored
Normal file
31
.github/actions/frontend-tests/action.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: 'Frontend tests'
|
||||
description: 'Run frontend tests and check types'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Prepare frontend tests
|
||||
if: always()
|
||||
run: npm run defs && npm run features
|
||||
shell: bash
|
||||
|
||||
- name: Tests
|
||||
if: always()
|
||||
run: npm run test:frontend -- --reporter json --reporter-option 'output=reports/frontend-tests.json'
|
||||
shell: bash
|
||||
|
||||
- name: Type Checks
|
||||
if: always()
|
||||
run: |
|
||||
set -o pipefail
|
||||
npm run check-types:frontend 2>&1 | tee reports/frontend-types.txt
|
||||
shell: bash
|
||||
|
||||
- name: Write Markdown Summary
|
||||
if: always()
|
||||
run: |
|
||||
node scripts/mocha2md.js 'Frontend Tests' reports/frontend-tests.json >> $GITHUB_STEP_SUMMARY
|
||||
echo '# Frontend Types' >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
cat reports/frontend-types.txt >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
shell: bash
|
||||
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
|
||||
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]'
|
||||
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 }}'
|
||||
26
.github/workflows/test-frontend.yml
vendored
Normal file
26
.github/workflows/test-frontend.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Frontend
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
- 'dependabot/**'
|
||||
|
||||
jobs:
|
||||
test-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Frontend tests
|
||||
uses: ./.github/actions/frontend-tests
|
||||
|
||||
- name: Frontend build
|
||||
run: npm run build
|
||||
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 }}'
|
||||
33
.github/workflows/update-github-api.yml
vendored
Normal file
33
.github/workflows/update-github-api.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Update GitHub API Version
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 7 * * 6'
|
||||
# At 07:00 on Saturday
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update-github-api:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Check for new GitHub API version
|
||||
run: node scripts/update-github-api.js
|
||||
|
||||
- name: Create Pull Request if config has changed
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
commit-message: Update GitHub API Version
|
||||
title: Update [GitHub] API Version
|
||||
branch-suffix: random
|
||||
15
CHANGELOG.md
15
CHANGELOG.md
@@ -4,6 +4,21 @@ Note: this changelog is for the shields.io server. The changelog for the badge-m
|
||||
|
||||
---
|
||||
|
||||
## server-2023-01-01
|
||||
|
||||
- Breaking change: Routes for GitHub workflows badge have changed. See https://github.com/badges/shields/issues/8671 for more details
|
||||
- Behaviour change: In this release we fixed a long standing bug. GitHub badges were previously not reading the base URL from the `config.service.baseUri`.
|
||||
This release fixes that bug, bringing the code into line with the documented behaviour. This should not cause a behaviour change for most users,
|
||||
but users who had previously set a value in `config.service.baseUri` which was previously ignored could see this now have an effect.
|
||||
Users who configure their instance using env vars rather than yaml should see no change.
|
||||
- Send `X-GitHub-Api-Version` when calling [GitHub] v3 API [#8669](https://github.com/badges/shields/issues/8669)
|
||||
- add [VpmVersion] badge [#8755](https://github.com/badges/shields/issues/8755)
|
||||
- Add [modrinth] game versions [#8673](https://github.com/badges/shields/issues/8673)
|
||||
- fix debug logging of undefined query params [#8540](https://github.com/badges/shields/issues/8540), [#8757](https://github.com/badges/shields/issues/8757)
|
||||
- fall back to classifiers if [pypi] license text is really long [#8690](https://github.com/badges/shields/issues/8690)
|
||||
- allow passing key to [stackexchange] [#8539](https://github.com/badges/shields/issues/8539)
|
||||
- Dependency updates
|
||||
|
||||
## server-2022-12-01
|
||||
|
||||
- fix: support logoColor to shield icons. [#8263](https://github.com/badges/shields/issues/8263)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -98,6 +98,7 @@ private:
|
||||
sl_insight_userUuid: 'SL_INSIGHT_USER_UUID'
|
||||
sl_insight_apiToken: 'SL_INSIGHT_API_TOKEN'
|
||||
sonarqube_token: 'SONARQUBE_TOKEN'
|
||||
stackapps_api_key: 'STACKAPPS_API_KEY'
|
||||
teamcity_user: 'TEAMCITY_USER'
|
||||
teamcity_pass: 'TEAMCITY_PASS'
|
||||
twitch_client_id: 'TWITCH_CLIENT_ID'
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
public:
|
||||
bind:
|
||||
address: '::'
|
||||
|
||||
metrics:
|
||||
prometheus:
|
||||
enabled: false
|
||||
@@ -12,33 +11,26 @@ public:
|
||||
intervalSeconds: 15
|
||||
ssl:
|
||||
isSecure: false
|
||||
|
||||
cors:
|
||||
allowedOrigin: []
|
||||
|
||||
services:
|
||||
github:
|
||||
baseUri: 'https://api.github.com/'
|
||||
baseUri: 'https://api.github.com'
|
||||
debug:
|
||||
enabled: false
|
||||
intervalSeconds: 200
|
||||
restApiVersion: '2022-11-28'
|
||||
obs:
|
||||
authorizedOrigins: 'https://api.opensuse.org'
|
||||
weblate:
|
||||
authorizedOrigins: 'https://hosted.weblate.org'
|
||||
trace: false
|
||||
|
||||
cacheHeaders:
|
||||
defaultCacheLengthSeconds: 120
|
||||
|
||||
handleInternalErrors: true
|
||||
|
||||
fetchLimit: '10MB'
|
||||
userAgentBase: 'shields (self-hosted)'
|
||||
|
||||
requestTimeoutSeconds: 120
|
||||
requestTimeoutMaxAgeSeconds: 30
|
||||
|
||||
requireCloudflare: false
|
||||
|
||||
private: {}
|
||||
|
||||
@@ -221,8 +221,14 @@ class BaseService {
|
||||
const logTrace = (...args) => trace.logTrace('fetch', ...args)
|
||||
let logUrl = url
|
||||
const logOptions = Object.assign({}, options)
|
||||
if ('searchParams' in options) {
|
||||
const params = new URLSearchParams(options.searchParams)
|
||||
if ('searchParams' in options && options.searchParams != null) {
|
||||
const params = new URLSearchParams(
|
||||
Object.fromEntries(
|
||||
Object.entries(options.searchParams).filter(
|
||||
([k, v]) => v !== undefined
|
||||
)
|
||||
)
|
||||
)
|
||||
logUrl = `${url}?${params.toString()}`
|
||||
delete logOptions.searchParams
|
||||
}
|
||||
|
||||
@@ -440,14 +440,21 @@ describe('BaseService', function () {
|
||||
)
|
||||
|
||||
const url = 'some-url'
|
||||
const options = { headers: { Cookie: 'some-cookie' } }
|
||||
const options = {
|
||||
headers: { Cookie: 'some-cookie' },
|
||||
searchParams: { param1: 'foobar', param2: undefined },
|
||||
}
|
||||
await serviceInstance._request({ url, options })
|
||||
|
||||
expect(trace.logTrace).to.be.calledWithMatch(
|
||||
'fetch',
|
||||
sinon.match.string,
|
||||
'Request',
|
||||
`${url}\n${JSON.stringify(options, null, 2)}`
|
||||
`${url}?param1=foobar\n${JSON.stringify(
|
||||
{ headers: options.headers },
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
)
|
||||
expect(trace.logTrace).to.be.calledWithMatch(
|
||||
'fetch',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -126,6 +126,7 @@ const publicConfigSchema = Joi.object({
|
||||
enabled: Joi.boolean().required(),
|
||||
intervalSeconds: Joi.number().integer().min(1).required(),
|
||||
},
|
||||
restApiVersion: Joi.date().raw().required(),
|
||||
},
|
||||
gitlab: defaultService,
|
||||
jira: defaultService,
|
||||
@@ -186,6 +187,7 @@ const privateConfigSchema = Joi.object({
|
||||
sl_insight_userUuid: Joi.string(),
|
||||
sl_insight_apiToken: Joi.string(),
|
||||
sonarqube_token: Joi.string(),
|
||||
stackapps_api_key: Joi.string(),
|
||||
teamcity_user: Joi.string(),
|
||||
teamcity_pass: Joi.string(),
|
||||
twitch_client_id: 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)
|
||||
}
|
||||
})()
|
||||
|
||||
8
doc/authentication.md
Normal file
8
doc/authentication.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Badges Requiring Authentication
|
||||
|
||||
There are two patterns for how shields.io can interact with APIs that require auth:
|
||||
|
||||
1. We can store one token at the service level which allows us to read public data for everyone's projects, or lift a rate limit. If you are looking for information on configuring credentials for a self-hosted instance see https://github.com/badges/shields/blob/master/doc/server-secrets.md
|
||||
2. If every user needs to provide their own token, that has to be a token which can be passed to us as a query param in the badge URL. This means it must be possible to generate a key or token that can be exposed in a public github README public with no negative consequences. (i.e: that key or token only exposes public metrics).
|
||||
|
||||
If every user would need to supply their own token for some particular service and it is only possible to generate a key or token which allows access to sensitive data or allows write access to resources, we can't provide an integration for this service.
|
||||
12
doc/logos.md
12
doc/logos.md
@@ -22,6 +22,18 @@ Any custom logo can be passed in a URL parameter by base64 encoding it. e.g:
|
||||
|
||||
 - https://img.shields.io/badge/play-station-blue.svg?logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEiIHdpZHRoPSI2MDAiIGhlaWdodD0iNjAwIj48cGF0aCBkPSJNMTI5IDExMWMtNTUgNC05MyA2Ni05MyA3OEwwIDM5OGMtMiA3MCAzNiA5MiA2OSA5MWgxYzc5IDAgODctNTcgMTMwLTEyOGgyMDFjNDMgNzEgNTAgMTI4IDEyOSAxMjhoMWMzMyAxIDcxLTIxIDY5LTkxbC0zNi0yMDljMC0xMi00MC03OC05OC03OGgtMTBjLTYzIDAtOTIgMzUtOTIgNDJIMjM2YzAtNy0yOS00Mi05Mi00MmgtMTV6IiBmaWxsPSIjZmZmIi8+PC9zdmc+
|
||||
|
||||
### logoColor parameter
|
||||
|
||||
The `logoColor` param can be used to set the color of the logo. Hex, rgb, rgba, hsl, hsla and css named colors can all be used. For SimpleIcons named logos (which are monochrome), the color will be applied to the SimpleIcons logo.
|
||||
|
||||
-  - https://img.shields.io/badge/logo-javascript-blue?logo=javascript
|
||||
-  - https://img.shields.io/badge/logo-javascript-blue?logo=javascript&logoColor=f5f5f5
|
||||
|
||||
In the case where Shields hosts a custom multi-colored logo, if the `logoColor` param is passed, the corresponding SimpleIcons logo will be substituted and colored.
|
||||
|
||||
-  - https://img.shields.io/badge/logo-gitlab-blue?logo=gitlab
|
||||
-  - https://img.shields.io/badge/logo-gitlab-blue?logo=gitlab&logoColor=white
|
||||
|
||||
## Contributing Logos
|
||||
|
||||
Our preferred way to consume icons is via the SimpleIcons logo. As a first port of call, we encourage you to contribute logos to [the SimpleIcons project][simple-icons github]. Please review their [guidance](https://github.com/simple-icons/simple-icons/blob/develop/CONTRIBUTING.md) before contributing.
|
||||
|
||||
@@ -244,6 +244,17 @@ Create an account, sign in and obtain a uuid and token from your
|
||||
to give your self-hosted Shields installation access to a
|
||||
private SonarQube instance or private project on a public instance.
|
||||
|
||||
### StackApps (for StackExchange and StackOverflow)
|
||||
|
||||
- `STACKAPPS_API_KEY`: (yml: `private.stackapps_api_key`)
|
||||
|
||||
Anonymous requests to the stackexchange API are limited to 300 calls per day.
|
||||
To increase your quota to 10,000 calls per day, create an account at
|
||||
[StackApps](https://stackapps.com/) and
|
||||
[register an OAuth app](https://stackapps.com/apps/oauth/register). Having registered
|
||||
an OAuth app, you'll be granted a key which can be used to increase your request quota.
|
||||
It is not necessary to performa full OAuth Flow to gain an access token.
|
||||
|
||||
### TeamCity
|
||||
|
||||
- `TEAMCITY_ORIGINS` (yml: `public.services.teamcity.authorizedOrigins`)
|
||||
|
||||
12
doc/static-badges.md
Normal file
12
doc/static-badges.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Static Badges
|
||||
|
||||
It is possible to use shields.io to make a wide variety of badges displaying static text and/or logos. For example:
|
||||
|
||||
-  - https://img.shields.io/badge/any%20text-you%20like-blue
|
||||
-  - https://img.shields.io/badge/just%20the%20message-8A2BE2
|
||||
-  - https://img.shields.io/badge/%27for%20the%20badge%27%20style-20B2AA?style=for-the-badge
|
||||
-  - https://img.shields.io/badge/with%20a%20logo-grey?style=for-the-badge&logo=javascript
|
||||
|
||||
Full documentation of styles and parameters: https://shields.io/#styles
|
||||
|
||||
More documentation on logos: https://github.com/badges/shields/blob/master/doc/logos.md
|
||||
@@ -6,7 +6,7 @@ import React, {
|
||||
} from 'react'
|
||||
import styled from 'styled-components'
|
||||
import humanizeString from 'humanize-string'
|
||||
import { stringify as stringifyQueryString } from 'query-string'
|
||||
import qs from 'query-string'
|
||||
import { advertisedStyles } from '../../lib/supported-features'
|
||||
import { noAutocorrect, StyledInput } from '../common'
|
||||
import {
|
||||
@@ -94,7 +94,7 @@ function getQueryString({
|
||||
}
|
||||
})
|
||||
|
||||
const queryString = stringifyQueryString(outQuery)
|
||||
const queryString = qs.stringify(outQuery)
|
||||
|
||||
return { queryString, isComplete }
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
2315
package-lock.json
generated
2315
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
56
package.json
56
package.json
@@ -25,17 +25,17 @@
|
||||
"@fontsource/lekton": "^4.5.11",
|
||||
"@renovate/pep440": "^1.0.0",
|
||||
"@renovatebot/ruby-semver": "^1.1.7",
|
||||
"@sentry/node": "^7.21.1",
|
||||
"@sentry/node": "^7.29.0",
|
||||
"@shields_io/camp": "^18.1.1",
|
||||
"badge-maker": "file:badge-maker",
|
||||
"bytes": "^3.1.2",
|
||||
"camelcase": "^7.0.0",
|
||||
"chalk": "^5.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",
|
||||
"cross-env": "^7.0.3",
|
||||
"dayjs": "^1.11.6",
|
||||
"dayjs": "^1.11.7",
|
||||
"decamelize": "^3.2.0",
|
||||
"emojic": "^1.1.17",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
@@ -58,11 +58,11 @@
|
||||
"path-to-regexp": "^6.2.1",
|
||||
"pretty-bytes": "^6.0.0",
|
||||
"priorityqueuejs": "^2.0.0",
|
||||
"prom-client": "^14.1.0",
|
||||
"prom-client": "^14.1.1",
|
||||
"qs": "^6.11.0",
|
||||
"query-string": "^7.1.1",
|
||||
"query-string": "^8.1.0",
|
||||
"semver": "~7.3.8",
|
||||
"simple-icons": "7.20.0",
|
||||
"simple-icons": "8.2.0",
|
||||
"webextension-store-meta": "^1.0.5",
|
||||
"xmldom": "~0.6.0",
|
||||
"xpath": "~0.0.32"
|
||||
@@ -142,7 +142,7 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.2",
|
||||
"@babel/core": "^7.20.12",
|
||||
"@babel/polyfill": "^7.12.1",
|
||||
"@babel/register": "7.18.9",
|
||||
"@istanbuljs/schema": "^0.1.3",
|
||||
@@ -150,14 +150,14 @@
|
||||
"@types/chai": "^4.3.4",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/lodash.groupby": "^4.6.7",
|
||||
"@types/mocha": "^10.0.0",
|
||||
"@types/mocha": "^10.0.1",
|
||||
"@types/node": "^16.7.10",
|
||||
"@types/react-helmet": "^6.1.5",
|
||||
"@types/react-helmet": "^6.1.6",
|
||||
"@types/react-modal": "^3.13.1",
|
||||
"@types/react-select": "^4.0.17",
|
||||
"@types/styled-components": "5.1.26",
|
||||
"@typescript-eslint/eslint-plugin": "^5.44.0",
|
||||
"@typescript-eslint/parser": "^5.42.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.0",
|
||||
"@typescript-eslint/parser": "^5.46.0",
|
||||
"babel-plugin-inline-react-svg": "^2.0.1",
|
||||
"babel-preset-gatsby": "^2.22.0",
|
||||
"c8": "^7.12.0",
|
||||
@@ -169,13 +169,13 @@
|
||||
"child-process-promise": "^2.2.1",
|
||||
"clipboard-copy": "^4.0.1",
|
||||
"concurrently": "^7.6.0",
|
||||
"cypress": "^11.2.0",
|
||||
"cypress": "^12.3.0",
|
||||
"cypress-wait-for-stable-dom": "^0.1.0",
|
||||
"danger": "^11.2.0",
|
||||
"danger": "^11.2.1",
|
||||
"danger-plugin-no-test-shortcuts": "^2.0.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-config-standard": "^16.0.3",
|
||||
"eslint-config-standard-jsx": "^10.0.0",
|
||||
"eslint-config-standard-react": "^11.0.1",
|
||||
@@ -189,27 +189,27 @@
|
||||
"eslint-plugin-promise": "^5.2.0",
|
||||
"eslint-plugin-react": "^7.31.11",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-sort-class-members": "^1.15.2",
|
||||
"eslint-plugin-sort-class-members": "^1.16.0",
|
||||
"fetch-ponyfill": "^7.1.0",
|
||||
"form-data": "^4.0.0",
|
||||
"gatsby": "4.23.1",
|
||||
"gatsby-plugin-catch-links": "^4.19.0",
|
||||
"gatsby-plugin-page-creator": "^4.24.0",
|
||||
"gatsby-plugin-react-helmet": "^5.22.0",
|
||||
"gatsby-plugin-catch-links": "^4.25.0",
|
||||
"gatsby-plugin-page-creator": "^4.25.0",
|
||||
"gatsby-plugin-react-helmet": "^5.25.0",
|
||||
"gatsby-plugin-remove-trailing-slashes": "^4.9.0",
|
||||
"gatsby-plugin-styled-components": "^5.24.0",
|
||||
"gatsby-plugin-typescript": "^4.22.0",
|
||||
"gatsby-plugin-typescript": "^4.25.0",
|
||||
"humanize-string": "^2.1.0",
|
||||
"icedfrisby": "4.0.0",
|
||||
"icedfrisby-nock": "^2.1.0",
|
||||
"is-svg": "^4.3.2",
|
||||
"js-yaml-loader": "^1.2.2",
|
||||
"jsdoc": "^4.0.0",
|
||||
"lint-staged": "^13.0.4",
|
||||
"lint-staged": "^13.1.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.difference": "^4.5.0",
|
||||
"minimist": "^1.2.7",
|
||||
"mocha": "^10.1.0",
|
||||
"mocha": "^10.2.0",
|
||||
"mocha-env-reporter": "^4.0.0",
|
||||
"mocha-junit-reporter": "^2.2.0",
|
||||
"mocha-yaml-loader": "^1.0.3",
|
||||
@@ -219,7 +219,7 @@
|
||||
"npm-run-all": "^4.1.5",
|
||||
"open-cli": "^7.1.0",
|
||||
"portfinder": "^1.0.32",
|
||||
"prettier": "2.8.0",
|
||||
"prettier": "2.8.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-error-overlay": "^6.0.11",
|
||||
@@ -232,14 +232,14 @@
|
||||
"rimraf": "^3.0.2",
|
||||
"sazerac": "^2.0.0",
|
||||
"simple-git-hooks": "^2.8.1",
|
||||
"sinon": "^14.0.2",
|
||||
"sinon": "^15.0.1",
|
||||
"sinon-chai": "^3.7.0",
|
||||
"snap-shot-it": "^7.9.6",
|
||||
"start-server-and-test": "1.14.0",
|
||||
"snap-shot-it": "^7.9.10",
|
||||
"start-server-and-test": "1.15.2",
|
||||
"styled-components": "^5.3.6",
|
||||
"ts-mocha": "^10.0.0",
|
||||
"tsd": "^0.24.1",
|
||||
"typescript": "^4.9.3",
|
||||
"tsd": "^0.25.0",
|
||||
"typescript": "^4.9.4",
|
||||
"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) {
|
||||
|
||||
19
scripts/update-github-api.js
Normal file
19
scripts/update-github-api.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import fs from 'fs/promises'
|
||||
import got from 'got'
|
||||
import yaml from 'js-yaml'
|
||||
|
||||
const resp = await got('https://api.github.com/versions').json()
|
||||
const latestDate = resp.sort()[resp.length - 1]
|
||||
|
||||
const config = yaml.load(await fs.readFile('./config/default.yml', 'utf8'))
|
||||
|
||||
if (latestDate === config.public.services.github.restApiVersion) {
|
||||
console.log("We're already using the latest version. No change needed.")
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
config.public.services.github.restApiVersion = latestDate
|
||||
await fs.writeFile(
|
||||
'./config/default.yml',
|
||||
yaml.dump(config, { forceQuotes: true })
|
||||
)
|
||||
@@ -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' })
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
100
services/github/github-actions-workflow-status.service.js
Normal file
100
services/github/github-actions-workflow-status.service.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import Joi from 'joi'
|
||||
import { isBuildStatus, renderBuildStatusBadge } from '../build-status.js'
|
||||
import { BaseSvgScrapingService } from '../index.js'
|
||||
import { documentation } from './github-helpers.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
message: Joi.alternatives()
|
||||
.try(isBuildStatus, Joi.equal('no status'))
|
||||
.required(),
|
||||
}).required()
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
event: Joi.string(),
|
||||
branch: Joi.alternatives().try(Joi.string(), Joi.number().cast('string')),
|
||||
}).required()
|
||||
|
||||
const keywords = ['action', 'actions']
|
||||
|
||||
export default class GithubActionsWorkflowStatus extends BaseSvgScrapingService {
|
||||
static category = 'build'
|
||||
|
||||
static route = {
|
||||
base: 'github/actions/workflow/status',
|
||||
pattern: ':user/:repo/:workflow+',
|
||||
queryParamSchema,
|
||||
}
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'GitHub Workflow Status',
|
||||
namedParams: {
|
||||
user: 'actions',
|
||||
repo: 'toolkit',
|
||||
workflow: 'unit-tests.yml',
|
||||
},
|
||||
staticPreview: renderBuildStatusBadge({
|
||||
status: 'passing',
|
||||
}),
|
||||
documentation,
|
||||
keywords,
|
||||
},
|
||||
{
|
||||
title: 'GitHub Workflow Status (with branch)',
|
||||
namedParams: {
|
||||
user: 'actions',
|
||||
repo: 'toolkit',
|
||||
workflow: 'unit-tests.yml',
|
||||
},
|
||||
queryParams: {
|
||||
branch: 'main',
|
||||
},
|
||||
staticPreview: renderBuildStatusBadge({
|
||||
status: 'passing',
|
||||
}),
|
||||
documentation,
|
||||
keywords,
|
||||
},
|
||||
{
|
||||
title: 'GitHub Workflow Status (with event)',
|
||||
namedParams: {
|
||||
user: 'actions',
|
||||
repo: 'toolkit',
|
||||
workflow: 'unit-tests.yml',
|
||||
},
|
||||
queryParams: {
|
||||
event: 'push',
|
||||
},
|
||||
staticPreview: renderBuildStatusBadge({
|
||||
status: 'passing',
|
||||
}),
|
||||
documentation,
|
||||
keywords,
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'build',
|
||||
}
|
||||
|
||||
async fetch({ user, repo, workflow, branch, event }) {
|
||||
const { message: status } = await this._requestSvg({
|
||||
schema,
|
||||
url: `https://github.com/${user}/${repo}/actions/workflows/${encodeURIComponent(
|
||||
workflow
|
||||
)}/badge.svg`,
|
||||
options: { searchParams: { branch, event } },
|
||||
valueMatcher: />([^<>]+)<\/tspan><\/text><\/g><path/,
|
||||
errorMessages: {
|
||||
404: 'repo or workflow not found',
|
||||
},
|
||||
})
|
||||
|
||||
return { status }
|
||||
}
|
||||
|
||||
async handle({ user, repo, workflow }, { branch, event }) {
|
||||
const { status } = await this.fetch({ user, repo, workflow, branch, event })
|
||||
return renderBuildStatusBadge({ status })
|
||||
}
|
||||
}
|
||||
66
services/github/github-actions-workflow-status.tester.js
Normal file
66
services/github/github-actions-workflow-status.tester.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import Joi from 'joi'
|
||||
import { isBuildStatus } from '../build-status.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
const isWorkflowStatus = Joi.alternatives()
|
||||
.try(isBuildStatus, Joi.equal('no status'))
|
||||
.required()
|
||||
|
||||
t.create('nonexistent repo')
|
||||
.get('/badges/shields-fakeness/fake.yml.json')
|
||||
.expectBadge({
|
||||
label: 'build',
|
||||
message: 'repo or workflow not found',
|
||||
})
|
||||
|
||||
t.create('nonexistent workflow')
|
||||
.get('/actions/toolkit/not-a-real-workflow.yml.json')
|
||||
.expectBadge({
|
||||
label: 'build',
|
||||
message: 'repo or workflow not found',
|
||||
})
|
||||
|
||||
t.create('nonexistent branch')
|
||||
.get('/actions/toolkit/unit-tests.yml.json?branch=not-a-real-branch')
|
||||
.expectBadge({
|
||||
label: 'build',
|
||||
message: 'no status',
|
||||
})
|
||||
|
||||
t.create('nonexistent event')
|
||||
.get('/actions/toolkit/unit-tests.yml.json?event=not-a-real-event')
|
||||
.expectBadge({
|
||||
label: 'build',
|
||||
message: 'no status',
|
||||
})
|
||||
|
||||
t.create('numeric branch name')
|
||||
.get('/actions/toolkit/unit-tests.yml.json?branch=9999')
|
||||
.expectBadge({
|
||||
label: 'build',
|
||||
// the key thing we're testing here is that this doesn't fail with
|
||||
// "invalid query parameter: branch"
|
||||
message: 'no status',
|
||||
})
|
||||
|
||||
t.create('valid workflow')
|
||||
.get('/actions/toolkit/unit-tests.yml.json')
|
||||
.expectBadge({
|
||||
label: 'build',
|
||||
message: isWorkflowStatus,
|
||||
})
|
||||
|
||||
t.create('valid workflow (with branch)')
|
||||
.get('/actions/toolkit/unit-tests.yml.json?branch=main')
|
||||
.expectBadge({
|
||||
label: 'build',
|
||||
message: isWorkflowStatus,
|
||||
})
|
||||
|
||||
t.create('valid workflow (with event)')
|
||||
.get('/actions/toolkit/unit-tests.yml.json?event=push')
|
||||
.expectBadge({
|
||||
label: 'build',
|
||||
message: isWorkflowStatus,
|
||||
})
|
||||
@@ -41,6 +41,7 @@ class GithubApiProvider {
|
||||
onTokenInvalidated = tokenString => {},
|
||||
globalToken,
|
||||
reserveFraction = 0.25,
|
||||
restApiVersion,
|
||||
}) {
|
||||
Object.assign(this, {
|
||||
baseUrl,
|
||||
@@ -55,6 +56,7 @@ class GithubApiProvider {
|
||||
this.searchTokens = new TokenPool({ batchSize: 5 })
|
||||
this.graphqlTokens = new TokenPool({ batchSize: 25 })
|
||||
}
|
||||
this.restApiVersion = restApiVersion
|
||||
}
|
||||
|
||||
addToken(tokenString) {
|
||||
@@ -175,6 +177,7 @@ class GithubApiProvider {
|
||||
headers: {
|
||||
'User-Agent': userAgent,
|
||||
Authorization: `token ${tokenString}`,
|
||||
'X-GitHub-Api-Version': this.restApiVersion,
|
||||
...options.headers,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -41,6 +41,7 @@ describe('GithubAuthV3Service', function () {
|
||||
)
|
||||
const githubApiProvider = new GithubApiProvider({
|
||||
baseUrl: 'https://github-api.example.com',
|
||||
restApiVersion: '2022-11-28',
|
||||
})
|
||||
const mockToken = { update: sinon.mock(), invalidate: sinon.mock() }
|
||||
sinon.stub(githubApiProvider.standardTokens, 'next').returns(mockToken)
|
||||
@@ -57,6 +58,7 @@ describe('GithubAuthV3Service', function () {
|
||||
'User-Agent': 'shields (self-hosted)/dev',
|
||||
Accept: 'application/vnd.github.antiope-preview+json',
|
||||
Authorization: 'token undefined',
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -33,10 +33,11 @@ class GithubConstellation {
|
||||
}
|
||||
|
||||
this.apiProvider = new GithubApiProvider({
|
||||
baseUrl: process.env.GITHUB_URL || 'https://api.github.com',
|
||||
baseUrl: config.service.baseUri,
|
||||
globalToken,
|
||||
withPooling: !globalToken,
|
||||
onTokenInvalidated: tokenString => this.onTokenInvalidated(tokenString),
|
||||
restApiVersion: config.service.restApiVersion,
|
||||
})
|
||||
|
||||
this.oauthHelper = this.constructor._createOauthHelper(config)
|
||||
|
||||
@@ -1,100 +1,28 @@
|
||||
import Joi from 'joi'
|
||||
import { isBuildStatus, renderBuildStatusBadge } from '../build-status.js'
|
||||
import { BaseSvgScrapingService } from '../index.js'
|
||||
import { documentation } from './github-helpers.js'
|
||||
import { BaseService } from '../index.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
message: Joi.alternatives()
|
||||
.try(isBuildStatus, Joi.equal('no status'))
|
||||
.required(),
|
||||
}).required()
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
event: Joi.string(),
|
||||
}).required()
|
||||
|
||||
const keywords = ['action', 'actions']
|
||||
|
||||
export default class GithubWorkflowStatus extends BaseSvgScrapingService {
|
||||
export default class DeprecatedGithubWorkflowStatus extends BaseService {
|
||||
static category = 'build'
|
||||
|
||||
static route = {
|
||||
base: 'github/workflow/status',
|
||||
pattern: ':user/:repo/:workflow/:branch*',
|
||||
queryParamSchema,
|
||||
pattern: ':various+',
|
||||
}
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'GitHub Workflow Status',
|
||||
pattern: ':user/:repo/:workflow',
|
||||
namedParams: {
|
||||
user: 'actions',
|
||||
repo: 'toolkit',
|
||||
workflow: 'toolkit-unit-tests',
|
||||
},
|
||||
staticPreview: renderBuildStatusBadge({
|
||||
status: 'passing',
|
||||
}),
|
||||
documentation,
|
||||
keywords,
|
||||
},
|
||||
{
|
||||
title: 'GitHub Workflow Status (branch)',
|
||||
pattern: ':user/:repo/:workflow/:branch',
|
||||
namedParams: {
|
||||
user: 'actions',
|
||||
repo: 'toolkit',
|
||||
workflow: 'toolkit-unit-tests',
|
||||
branch: 'master',
|
||||
},
|
||||
staticPreview: renderBuildStatusBadge({
|
||||
status: 'passing',
|
||||
}),
|
||||
documentation,
|
||||
keywords,
|
||||
},
|
||||
{
|
||||
title: 'GitHub Workflow Status (event)',
|
||||
pattern: ':user/:repo/:workflow',
|
||||
namedParams: {
|
||||
user: 'actions',
|
||||
repo: 'toolkit',
|
||||
workflow: 'toolkit-unit-tests',
|
||||
},
|
||||
queryParams: {
|
||||
event: 'push',
|
||||
},
|
||||
staticPreview: renderBuildStatusBadge({
|
||||
status: 'passing',
|
||||
}),
|
||||
documentation,
|
||||
keywords,
|
||||
},
|
||||
]
|
||||
static examples = []
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'build',
|
||||
}
|
||||
static defaultBadgeData = { label: 'build' }
|
||||
|
||||
async fetch({ user, repo, workflow, branch, event }) {
|
||||
const { message: status } = await this._requestSvg({
|
||||
schema,
|
||||
url: `https://github.com/${user}/${repo}/workflows/${encodeURIComponent(
|
||||
workflow
|
||||
)}/badge.svg`,
|
||||
options: { searchParams: { branch, event } },
|
||||
valueMatcher: />([^<>]+)<\/tspan><\/text><\/g><path/,
|
||||
errorMessages: {
|
||||
404: 'repo, branch, or workflow not found',
|
||||
},
|
||||
})
|
||||
|
||||
return { status }
|
||||
}
|
||||
|
||||
async handle({ user, repo, workflow, branch }, { event }) {
|
||||
const { status } = await this.fetch({ user, repo, workflow, branch, event })
|
||||
return renderBuildStatusBadge({ status })
|
||||
async handle() {
|
||||
return {
|
||||
label: 'build',
|
||||
message: 'https://github.com/badges/shields/issues/8671',
|
||||
/*
|
||||
This is a 'special' deprecation because we are making a breaking change
|
||||
We've implemented it as a custom class instead of a normal
|
||||
deprecatedService so that we can include link.
|
||||
*/
|
||||
link: ['https://github.com/badges/shields/issues/8671'],
|
||||
color: 'red',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,42 @@
|
||||
import Joi from 'joi'
|
||||
import { isBuildStatus } from '../build-status.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
import { ServiceTester } from '../tester.js'
|
||||
|
||||
const isWorkflowStatus = Joi.alternatives()
|
||||
.try(isBuildStatus, Joi.equal('no status'))
|
||||
.required()
|
||||
export const t = new ServiceTester({
|
||||
id: 'GithubWorkflowStatus',
|
||||
title: 'Github Workflow Status',
|
||||
pathPrefix: '/github/workflow/status',
|
||||
})
|
||||
|
||||
t.create('nonexistent repo')
|
||||
t.create('no longer available (previously nonexistent repo)')
|
||||
.get('/badges/shields-fakeness/fake.json')
|
||||
.expectBadge({
|
||||
label: 'build',
|
||||
message: 'repo, branch, or workflow not found',
|
||||
message: 'https://github.com/badges/shields/issues/8671',
|
||||
})
|
||||
|
||||
t.create('nonexistent workflow')
|
||||
t.create('no longer available (previously nonexistent workflow)')
|
||||
.get('/actions/toolkit/not-a-real-workflow.json')
|
||||
.expectBadge({
|
||||
label: 'build',
|
||||
message: 'repo, branch, or workflow not found',
|
||||
message: 'https://github.com/badges/shields/issues/8671',
|
||||
})
|
||||
|
||||
t.create('valid workflow')
|
||||
t.create('no longer available (previously valid workflow)')
|
||||
.get('/actions/toolkit/toolkit-unit-tests.json')
|
||||
.expectBadge({
|
||||
label: 'build',
|
||||
message: isWorkflowStatus,
|
||||
message: 'https://github.com/badges/shields/issues/8671',
|
||||
})
|
||||
|
||||
t.create('valid workflow (branch)')
|
||||
t.create('no longer available (previously valid workflow - branch)')
|
||||
.get('/actions/toolkit/toolkit-unit-tests/master.json')
|
||||
.expectBadge({
|
||||
label: 'build',
|
||||
message: isWorkflowStatus,
|
||||
message: 'https://github.com/badges/shields/issues/8671',
|
||||
})
|
||||
|
||||
t.create('valid workflow (event)')
|
||||
t.create('no longer available (previously valid workflow - event)')
|
||||
.get('/actions/toolkit/toolkit-unit-tests.json?event=push')
|
||||
.expectBadge({
|
||||
label: 'build',
|
||||
message: isWorkflowStatus,
|
||||
message: 'https://github.com/badges/shields/issues/8671',
|
||||
})
|
||||
|
||||
@@ -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 }
|
||||
@@ -11,6 +11,7 @@ const versionSchema = Joi.array()
|
||||
.items(
|
||||
Joi.object({
|
||||
version_number: Joi.string().required(),
|
||||
game_versions: Joi.array().items(Joi.string()).min(1).required(),
|
||||
}).required()
|
||||
)
|
||||
.required()
|
||||
|
||||
34
services/modrinth/modrinth-game-versions.service.js
Normal file
34
services/modrinth/modrinth-game-versions.service.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { BaseModrinthService, documentation } from './modrinth-base.js'
|
||||
|
||||
export default class ModrinthGameVersions extends BaseModrinthService {
|
||||
static category = 'platform-support'
|
||||
|
||||
static route = {
|
||||
base: 'modrinth/game-versions',
|
||||
pattern: ':projectId',
|
||||
}
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'Modrinth Game Versions',
|
||||
namedParams: { projectId: 'AANobbMI' },
|
||||
staticPreview: this.render({ versions: ['1.19.2', '1.19.1', '1.19'] }),
|
||||
documentation,
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = { label: 'game versions' }
|
||||
|
||||
static render({ versions }) {
|
||||
return {
|
||||
message: versions.join(' | '),
|
||||
color: 'blue',
|
||||
}
|
||||
}
|
||||
|
||||
async handle({ projectId }) {
|
||||
const { 0: latest } = await this.fetchVersions({ projectId })
|
||||
const versions = latest.game_versions
|
||||
return this.constructor.render({ versions })
|
||||
}
|
||||
}
|
||||
15
services/modrinth/modrinth-game-versions.tester.js
Normal file
15
services/modrinth/modrinth-game-versions.tester.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { createServiceTester } from '../tester.js'
|
||||
import { withRegex } from '../test-validators.js'
|
||||
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('Game Versions')
|
||||
.get('/AANobbMI.json')
|
||||
.expectBadge({
|
||||
label: 'game versions',
|
||||
message: withRegex(/\d+\.\d+(\.\d+)?( \| )?/),
|
||||
})
|
||||
|
||||
t.create('Game Versions (not found)')
|
||||
.get('/not-existing.json')
|
||||
.expectBadge({ label: 'game versions', message: 'not found', color: 'red' })
|
||||
@@ -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,
|
||||
|
||||
@@ -50,7 +50,17 @@ function getLicenses(packageData) {
|
||||
const {
|
||||
info: { license },
|
||||
} = packageData
|
||||
if (license) {
|
||||
|
||||
/*
|
||||
The .license field may either contain
|
||||
- a short license description (e.g: 'MIT' or 'GPL-3.0') or
|
||||
- the full text of a license
|
||||
but there is nothing in the response that tells us explicitly.
|
||||
We have to make an assumption based on the length.
|
||||
See https://github.com/badges/shields/issues/8689 and
|
||||
https://github.com/badges/shields/pull/8690 for more info.
|
||||
*/
|
||||
if (license && license.length < 40) {
|
||||
return [license]
|
||||
} else {
|
||||
const parenthesizedAcronymRegex = /\(([^)]+)\)/
|
||||
|
||||
@@ -116,6 +116,13 @@ describe('PyPI helpers', function () {
|
||||
classifiers: ['License :: OSI Approved :: MIT License'],
|
||||
},
|
||||
}),
|
||||
given({
|
||||
info: {
|
||||
license:
|
||||
'this text is really really really really really really long',
|
||||
classifiers: ['License :: OSI Approved :: MIT License'],
|
||||
},
|
||||
}),
|
||||
given({
|
||||
info: {
|
||||
license: '',
|
||||
|
||||
37
services/stackexchange/stackexchange-base.js
Normal file
37
services/stackexchange/stackexchange-base.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { BaseJsonService } from '../index.js'
|
||||
import { metric } from '../text-formatters.js'
|
||||
import { floorCount as floorCountColor } from '../color-formatters.js'
|
||||
|
||||
export function renderQuestionsBadge({
|
||||
suffix,
|
||||
stackexchangesite,
|
||||
query,
|
||||
numValue,
|
||||
}) {
|
||||
const label = `${stackexchangesite} ${query} questions`
|
||||
return {
|
||||
label,
|
||||
message: `${metric(numValue)}${suffix}`,
|
||||
color: floorCountColor(numValue, 1000, 10000, 20000),
|
||||
}
|
||||
}
|
||||
|
||||
export class StackExchangeBase extends BaseJsonService {
|
||||
static category = 'chat'
|
||||
|
||||
static auth = {
|
||||
passKey: 'stackapps_api_key',
|
||||
authorizedOrigins: ['https://api.stackexchange.com'],
|
||||
isRequired: false,
|
||||
}
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'stackoverflow',
|
||||
}
|
||||
|
||||
async fetch(params) {
|
||||
return this._requestJson(
|
||||
this.authHelper.withQueryStringAuth({ passKey: 'key' }, params)
|
||||
)
|
||||
}
|
||||
}
|
||||
38
services/stackexchange/stackexchange-base.spec.js
Normal file
38
services/stackexchange/stackexchange-base.spec.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import Joi from 'joi'
|
||||
import { expect } from 'chai'
|
||||
import nock from 'nock'
|
||||
import { cleanUpNockAfterEach, defaultContext } from '../test-helpers.js'
|
||||
import { StackExchangeBase } from './stackexchange-base.js'
|
||||
|
||||
class DummyStackExchangeService extends StackExchangeBase {
|
||||
static route = { base: 'fake-base' }
|
||||
|
||||
async handle() {
|
||||
const data = await this.fetch({
|
||||
schema: Joi.any(),
|
||||
url: 'https://api.stackexchange.com/2.2/tags/python/info',
|
||||
})
|
||||
return { message: data.message }
|
||||
}
|
||||
}
|
||||
|
||||
describe('StackExchangeBase', function () {
|
||||
describe('auth', function () {
|
||||
cleanUpNockAfterEach()
|
||||
|
||||
const config = { private: { stackapps_api_key: 'fake-key' } }
|
||||
|
||||
it('sends the auth information as configured', async function () {
|
||||
const scope = nock('https://api.stackexchange.com')
|
||||
.get('/2.2/tags/python/info')
|
||||
.query({ key: 'fake-key' })
|
||||
.reply(200, { message: 'fake message' })
|
||||
|
||||
expect(
|
||||
await DummyStackExchangeService.invoke(defaultContext, config, {})
|
||||
).to.deep.equal({ message: 'fake message' })
|
||||
|
||||
scope.done()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,16 +0,0 @@
|
||||
import { metric } from '../text-formatters.js'
|
||||
import { floorCount as floorCountColor } from '../color-formatters.js'
|
||||
|
||||
export default function renderQuestionsBadge({
|
||||
suffix,
|
||||
stackexchangesite,
|
||||
query,
|
||||
numValue,
|
||||
}) {
|
||||
const label = `${stackexchangesite} ${query} questions`
|
||||
return {
|
||||
label,
|
||||
message: `${metric(numValue)}${suffix}`,
|
||||
color: floorCountColor(numValue, 1000, 10000, 20000),
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
import dayjs from 'dayjs'
|
||||
import Joi from 'joi'
|
||||
import { nonNegativeInteger } from '../validators.js'
|
||||
import { BaseJsonService } from '../index.js'
|
||||
import renderQuestionsBadge from './stackexchange-helpers.js'
|
||||
import {
|
||||
renderQuestionsBadge,
|
||||
StackExchangeBase,
|
||||
} from './stackexchange-base.js'
|
||||
|
||||
const tagSchema = Joi.object({
|
||||
total: nonNegativeInteger,
|
||||
}).required()
|
||||
|
||||
export default class StackExchangeMonthlyQuestions extends BaseJsonService {
|
||||
static category = 'chat'
|
||||
|
||||
export default class StackExchangeMonthlyQuestions extends StackExchangeBase {
|
||||
static route = {
|
||||
base: 'stackexchange',
|
||||
pattern: ':stackexchangesite/qm/:query',
|
||||
@@ -29,10 +29,6 @@ export default class StackExchangeMonthlyQuestions extends BaseJsonService {
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'stackoverflow',
|
||||
}
|
||||
|
||||
static render(props) {
|
||||
return renderQuestionsBadge({
|
||||
suffix: '/month',
|
||||
@@ -51,7 +47,7 @@ export default class StackExchangeMonthlyQuestions extends BaseJsonService {
|
||||
.endOf('month')
|
||||
.unix()
|
||||
|
||||
const parsedData = await this._requestJson({
|
||||
const parsedData = await this.fetch({
|
||||
schema: tagSchema,
|
||||
options: {
|
||||
decompress: true,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Joi from 'joi'
|
||||
import { metric } from '../text-formatters.js'
|
||||
import { floorCount as floorCountColor } from '../color-formatters.js'
|
||||
import { BaseJsonService } from '../index.js'
|
||||
import { StackExchangeBase } from './stackexchange-base.js'
|
||||
|
||||
const reputationSchema = Joi.object({
|
||||
items: Joi.array()
|
||||
@@ -14,9 +14,7 @@ const reputationSchema = Joi.object({
|
||||
.required(),
|
||||
}).required()
|
||||
|
||||
export default class StackExchangeReputation extends BaseJsonService {
|
||||
static category = 'chat'
|
||||
|
||||
export default class StackExchangeReputation extends StackExchangeBase {
|
||||
static route = {
|
||||
base: 'stackexchange',
|
||||
pattern: ':stackexchangesite/r/:query',
|
||||
@@ -34,10 +32,6 @@ export default class StackExchangeReputation extends BaseJsonService {
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'stackoverflow',
|
||||
}
|
||||
|
||||
static render({ stackexchangesite, numValue }) {
|
||||
const label = `${stackexchangesite} reputation`
|
||||
|
||||
@@ -51,7 +45,7 @@ export default class StackExchangeReputation extends BaseJsonService {
|
||||
async handle({ stackexchangesite, query }) {
|
||||
const path = `users/${query}`
|
||||
|
||||
const parsedData = await this._requestJson({
|
||||
const parsedData = await this.fetch({
|
||||
schema: reputationSchema,
|
||||
options: { decompress: true, searchParams: { site: stackexchangesite } },
|
||||
url: `https://api.stackexchange.com/2.2/${path}`,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import Joi from 'joi'
|
||||
import { BaseJsonService } from '../index.js'
|
||||
import renderQuestionsBadge from './stackexchange-helpers.js'
|
||||
import {
|
||||
renderQuestionsBadge,
|
||||
StackExchangeBase,
|
||||
} from './stackexchange-base.js'
|
||||
|
||||
const tagSchema = Joi.object({
|
||||
items: Joi.array()
|
||||
@@ -13,9 +15,7 @@ const tagSchema = Joi.object({
|
||||
.required(),
|
||||
}).required()
|
||||
|
||||
export default class StackExchangeQuestions extends BaseJsonService {
|
||||
static category = 'chat'
|
||||
|
||||
export default class StackExchangeQuestions extends StackExchangeBase {
|
||||
static route = {
|
||||
base: 'stackexchange',
|
||||
pattern: ':stackexchangesite/t/:query',
|
||||
@@ -34,10 +34,6 @@ export default class StackExchangeQuestions extends BaseJsonService {
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'stackoverflow',
|
||||
}
|
||||
|
||||
static render(props) {
|
||||
return renderQuestionsBadge({
|
||||
suffix: '',
|
||||
@@ -48,7 +44,7 @@ export default class StackExchangeQuestions extends BaseJsonService {
|
||||
async handle({ stackexchangesite, query }) {
|
||||
const path = `tags/${query}/info`
|
||||
|
||||
const parsedData = await this._requestJson({
|
||||
const parsedData = await this.fetch({
|
||||
schema: tagSchema,
|
||||
options: { decompress: true, searchParams: { site: stackexchangesite } },
|
||||
url: `https://api.stackexchange.com/2.2/${path}`,
|
||||
|
||||
79
services/vpm/vpm-version.service.js
Normal file
79
services/vpm/vpm-version.service.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import Joi from 'joi'
|
||||
import { optionalUrl } from '../validators.js'
|
||||
import { latest, renderVersionBadge } from '../version.js'
|
||||
import { BaseJsonService, NotFound } from '../index.js'
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
repository_url: optionalUrl.required(),
|
||||
include_prereleases: Joi.equal(''),
|
||||
}).required()
|
||||
|
||||
const schema = Joi.object({
|
||||
packages: Joi.object()
|
||||
.pattern(
|
||||
/./,
|
||||
Joi.object({
|
||||
versions: Joi.object().pattern(/./, Joi.object()).min(1).required(),
|
||||
}).required()
|
||||
)
|
||||
.required(),
|
||||
}).required()
|
||||
|
||||
export default class VpmVersion extends BaseJsonService {
|
||||
static category = 'version'
|
||||
|
||||
static route = {
|
||||
base: 'vpm/v',
|
||||
pattern: ':packageId',
|
||||
queryParamSchema,
|
||||
}
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'VPM Package Version',
|
||||
namedParams: {
|
||||
packageId: 'com.vrchat.udonsharp',
|
||||
},
|
||||
queryParams: {
|
||||
repository_url: 'https://packages.vrchat.com/curated?download',
|
||||
},
|
||||
staticPreview: renderVersionBadge({ version: '1.1.6' }),
|
||||
},
|
||||
{
|
||||
title: 'VPM Package Version (including prereleases)',
|
||||
namedParams: {
|
||||
packageId: 'com.vrchat.udonsharp',
|
||||
},
|
||||
queryParams: {
|
||||
repository_url: 'https://packages.vrchat.com/curated?download',
|
||||
include_prereleases: null,
|
||||
},
|
||||
staticPreview: renderVersionBadge({ version: '1.1.6' }),
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'vpm',
|
||||
}
|
||||
|
||||
async fetch({ repositoryUrl }) {
|
||||
return this._requestJson({
|
||||
schema,
|
||||
url: repositoryUrl,
|
||||
})
|
||||
}
|
||||
|
||||
async handle(
|
||||
{ packageId },
|
||||
{ repository_url: repositoryUrl, include_prereleases: prereleases }
|
||||
) {
|
||||
const data = await this.fetch({ repositoryUrl })
|
||||
const pkg = data.packages[packageId]
|
||||
if (pkg === undefined)
|
||||
throw new NotFound({ prettyMessage: 'package not found' })
|
||||
const versions = Object.keys(pkg.versions)
|
||||
const version = latest(versions, { pre: prereleases !== undefined })
|
||||
|
||||
return renderVersionBadge({ version })
|
||||
}
|
||||
}
|
||||
51
services/vpm/vpm-version.tester.js
Normal file
51
services/vpm/vpm-version.tester.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { isSemver } from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('gets the package version of com.vrchat.udonsharp')
|
||||
.get(
|
||||
'/com.vrchat.udonsharp.json?repository_url=https%3A%2F%2Fpackages.vrchat.com%2Fcurated%3Fdownload'
|
||||
)
|
||||
.expectBadge({ label: 'vpm', message: isSemver })
|
||||
|
||||
t.create('gets the latest version')
|
||||
.intercept(nock =>
|
||||
nock('https://packages.vrchat.com')
|
||||
.get('/curated?download')
|
||||
.reply(200, {
|
||||
packages: {
|
||||
'com.vrchat.udonsharp': {
|
||||
versions: {
|
||||
'2.0.0': {},
|
||||
'2.1.0-rc1': {},
|
||||
'1.9.0': {},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
.get(
|
||||
'/com.vrchat.udonsharp.json?repository_url=https%3A%2F%2Fpackages.vrchat.com%2Fcurated%3Fdownload'
|
||||
)
|
||||
.expectBadge({ label: 'vpm', message: 'v2.0.0' })
|
||||
|
||||
t.create('gets the latest version including prerelease')
|
||||
.intercept(nock =>
|
||||
nock('https://packages.vrchat.com')
|
||||
.get('/curated?download')
|
||||
.reply(200, {
|
||||
packages: {
|
||||
'com.vrchat.udonsharp': {
|
||||
versions: {
|
||||
'2.0.0': {},
|
||||
'2.1.0-rc1': {},
|
||||
'1.9.0': {},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
.get(
|
||||
'/com.vrchat.udonsharp.json?repository_url=https%3A%2F%2Fpackages.vrchat.com%2Fcurated%3Fdownload&include_prereleases'
|
||||
)
|
||||
.expectBadge({ label: 'vpm', message: 'v2.1.0-rc1' })
|
||||
Reference in New Issue
Block a user