Compare commits
29 Commits
server-202
...
services-g
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0ef5046d0 | ||
|
|
f60c2058fa | ||
|
|
39e4d9bcbc | ||
|
|
2bf863fb09 | ||
|
|
ed277d4e79 | ||
|
|
fdc81c2e3a | ||
|
|
6ef9dcaba5 | ||
|
|
541cb9acf2 | ||
|
|
2492ff79f7 | ||
|
|
9c692cd53a | ||
|
|
7410bf2e97 | ||
|
|
a83cfa4fb6 | ||
|
|
4d64969738 | ||
|
|
ade213c6d3 | ||
|
|
1135fba9f6 | ||
|
|
0234fb077f | ||
|
|
ed86c1de21 | ||
|
|
a47f770c82 | ||
|
|
3176d6f7f3 | ||
|
|
0763d8ec66 | ||
|
|
3fcb959ed2 | ||
|
|
f562dfe868 | ||
|
|
22aee48544 | ||
|
|
098a24cae5 | ||
|
|
a2c8ed27b8 | ||
|
|
2dccd3d040 | ||
|
|
b8ce38a041 | ||
|
|
0784a14153 | ||
|
|
afc7b283bc |
3
.circleci/config.yml
Normal file
3
.circleci/config.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
version: 2
|
||||
# Do nothing
|
||||
# TODO: disable Circle
|
||||
@@ -3,7 +3,6 @@ shields.env
|
||||
.git/
|
||||
.gitignore
|
||||
.vscode/
|
||||
fly.toml
|
||||
|
||||
# Improve layer cacheability.
|
||||
Dockerfile
|
||||
|
||||
@@ -24,7 +24,6 @@ plugins:
|
||||
- chai-friendly
|
||||
- jsdoc
|
||||
- mocha
|
||||
- icedfrisby
|
||||
- no-extension-in-require
|
||||
- sort-class-members
|
||||
- import
|
||||
@@ -114,16 +113,9 @@ overrides:
|
||||
mocha: true
|
||||
rules:
|
||||
mocha/no-exclusive-tests: 'error'
|
||||
mocha/no-skipped-tests: 'error'
|
||||
mocha/no-mocha-arrows: 'error'
|
||||
mocha/prefer-arrow-callback: 'error'
|
||||
|
||||
- files:
|
||||
- 'services/**/*.tester.js'
|
||||
rules:
|
||||
icedfrisby/no-exclusive-tests: 'error'
|
||||
icedfrisby/no-skipped-tests: 'error'
|
||||
|
||||
rules:
|
||||
# Disable some rules from eslint:recommended.
|
||||
no-empty: ['error', { 'allowEmptyCatch': true }]
|
||||
@@ -186,7 +178,7 @@ rules:
|
||||
jsdoc/check-tag-names: 'error'
|
||||
jsdoc/check-types: 'error'
|
||||
jsdoc/implements-on-classes: 'error'
|
||||
jsdoc/tag-lines: ['error', 'any', { 'startLines': 1 }]
|
||||
jsdoc/newline-after-description: 'error'
|
||||
jsdoc/require-param: 'error'
|
||||
jsdoc/require-param-description: 'error'
|
||||
jsdoc/require-param-name: 'error'
|
||||
|
||||
8
.github/actions/integration-tests/action.yml
vendored
8
.github/actions/integration-tests/action.yml
vendored
@@ -7,19 +7,11 @@ inputs:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Migrate DB
|
||||
if: always()
|
||||
run: npm run migrate up
|
||||
env:
|
||||
POSTGRES_URL: postgresql://postgres:postgres@localhost:5432/ci_test
|
||||
shell: bash
|
||||
|
||||
- name: Integration Tests
|
||||
if: always()
|
||||
run: npm run test:integration -- --reporter json --reporter-option 'output=reports/integration-tests.json'
|
||||
env:
|
||||
GH_TOKEN: '${{ inputs.github-token }}'
|
||||
POSTGRES_URL: postgresql://postgres:postgres@localhost:5432/ci_test
|
||||
shell: bash
|
||||
|
||||
- name: Write Markdown Summary
|
||||
|
||||
10
.github/scripts/cleanup-review-apps.sh
vendored
10
.github/scripts/cleanup-review-apps.sh
vendored
@@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
apps=$(flyctl apps list --json | jq -r .[].ID | grep -E "pr-[0-9]+-badges-shields") || exit 0
|
||||
|
||||
for app in $apps
|
||||
do
|
||||
flyctl apps destroy "$app" -y
|
||||
done
|
||||
35
.github/scripts/deploy-review-app.sh
vendored
35
.github/scripts/deploy-review-app.sh
vendored
@@ -1,35 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
app="pr-$PR_NUMBER-badges-shields"
|
||||
region="ewr"
|
||||
org="shields-io"
|
||||
|
||||
# Get PR JSON from the API
|
||||
# This will fail if $PR_NUMBER is not a valid PR
|
||||
pr_json=$(curl --fail "https://api.github.com/repos/badges/shields/pulls/$PR_NUMBER")
|
||||
|
||||
# Attempt to apply the PR diff to the target branch
|
||||
# This will fail if it does not merge cleanly
|
||||
git config user.name "actions[bot]"
|
||||
git config user.email "actions@users.noreply.github.com"
|
||||
git fetch origin "pull/$PR_NUMBER/head:pr-$PR_NUMBER"
|
||||
git merge "pr-$PR_NUMBER"
|
||||
|
||||
# If the app does not already exist, create it
|
||||
if ! flyctl status --app "$app"; then
|
||||
flyctl launch --no-deploy --copy-config --name "$app" --region "$region" --org "$org"
|
||||
echo $SECRETS | tr " " "\n" | flyctl secrets import --app "$app"
|
||||
fi
|
||||
|
||||
# Deploy
|
||||
flyctl deploy --app "$app" --region "$region"
|
||||
|
||||
# Post a comment on the PR
|
||||
app_url=$(flyctl status --app "$app" --json | jq -r .Hostname)
|
||||
comment_url=$(echo "$pr_json" | jq .comments_url -r)
|
||||
curl "$comment_url" \
|
||||
-X POST \
|
||||
-H "Authorization: token $GITHUB_TOKEN" \
|
||||
--data "{\"body\":\"🚀 Updated review app: https://$app_url\"}"
|
||||
6
.github/workflows/build-docker-image.yml
vendored
6
.github/workflows/build-docker-image.yml
vendored
@@ -11,17 +11,15 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
version: v0.9.1
|
||||
|
||||
- name: Set Git Short SHA
|
||||
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
tags: ghcr.io/badges/shields:pr-validation
|
||||
tags: shieldsio/shields:pr-validation
|
||||
build-args: |
|
||||
version=${{ env.SHORT_SHA }}
|
||||
|
||||
24
.github/workflows/cleanup-review-apps.yml
vendored
24
.github/workflows/cleanup-review-apps.yml
vendored
@@ -1,24 +0,0 @@
|
||||
name: Cleanup Review Apps
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 7 * * *'
|
||||
# At 07:00, daily
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
cleanup-review-apps:
|
||||
runs-on: ubuntu-latest
|
||||
environment: 'Review Apps'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: superfly/flyctl-actions/setup-flyctl@master
|
||||
|
||||
- name: install jq
|
||||
run: |
|
||||
sudo apt-get -qq update
|
||||
sudo apt-get install -y jq
|
||||
|
||||
- run: .github/scripts/cleanup-review-apps.sh
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
|
||||
21
.github/workflows/create-release.yml
vendored
21
.github/workflows/create-release.yml
vendored
@@ -6,7 +6,6 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
@@ -36,8 +35,6 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
version: v0.9.1
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
@@ -46,26 +43,10 @@ jobs:
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push snapshot release to DockerHub
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: shieldsio/shields:server-${{ steps.date.outputs.date }}
|
||||
build-args: |
|
||||
version=server-${{ steps.date.outputs.date }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push snapshot release to GHCR
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ghcr.io/badges/shields:server-${{ steps.date.outputs.date }}
|
||||
build-args: |
|
||||
version=server-${{ steps.date.outputs.date }}
|
||||
|
||||
4
.github/workflows/danger.yml
vendored
4
.github/workflows/danger.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: Danger
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize]
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
|
||||
permissions:
|
||||
checks: write
|
||||
@@ -11,7 +11,7 @@ permissions:
|
||||
jobs:
|
||||
danger:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor != 'dependabot[bot]' && github.actor != 'repo-ranger[bot]'
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
9
.github/workflows/deploy-docs.yml
vendored
9
.github/workflows/deploy-docs.yml
vendored
@@ -16,13 +16,10 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Build
|
||||
run: npm run build-docs
|
||||
run: |
|
||||
npm ci
|
||||
npm run build-docs
|
||||
|
||||
- name: Deploy
|
||||
uses: JamesIves/github-pages-deploy-action@v4
|
||||
|
||||
43
.github/workflows/deploy-review-app.yml
vendored
43
.github/workflows/deploy-review-app.yml
vendored
@@ -1,43 +0,0 @@
|
||||
name: Create/Update Review App
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR Number to deploy e.g: 1234'
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
deploy-review-app:
|
||||
runs-on: ubuntu-latest
|
||||
environment: 'Review Apps'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: superfly/flyctl-actions/setup-flyctl@master
|
||||
|
||||
- name: install jq
|
||||
run: |
|
||||
sudo apt-get -qq update
|
||||
sudo apt-get install -y jq
|
||||
|
||||
- run: .github/scripts/deploy-review-app.sh
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.inputs.pr_number }}
|
||||
# credentials to set when we create the review app
|
||||
SECRETS: |
|
||||
GH_TOKEN=${{ secrets.GH_PAT }}
|
||||
LIBRARIESIO_TOKENS=${{ secrets.SERVICETESTS_LIBRARIESIO_TOKENS }}
|
||||
OBS_USER=${{ secrets.SERVICETESTS_OBS_USER }}
|
||||
OBS_PASS=${{ secrets.SERVICETESTS_OBS_PASS }}
|
||||
SL_INSIGHT_API_TOKEN=${{ secrets.SERVICETESTS_SL_INSIGHT_USER_UUID }}
|
||||
SL_INSIGHT_USER_UUID=${{ 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 }}
|
||||
@@ -1,7 +1,7 @@
|
||||
name: 'Dependency Review'
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
|
||||
jobs:
|
||||
enforce-dependency-review:
|
||||
|
||||
25
.github/workflows/publish-docker-next.yml
vendored
25
.github/workflows/publish-docker-next.yml
vendored
@@ -4,9 +4,6 @@ on:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
publish-docker-next:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -16,8 +13,6 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
version: v0.9.1
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
@@ -28,27 +23,11 @@ jobs:
|
||||
- name: Set Git Short SHA
|
||||
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push to DockerHub
|
||||
uses: docker/build-push-action@v4
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: shieldsio/shields:next
|
||||
build-args: |
|
||||
version=${{ env.SHORT_SHA }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push to GHCR
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ghcr.io/badges/shields:next
|
||||
build-args: |
|
||||
version=${{ env.SHORT_SHA }}
|
||||
|
||||
2
.github/workflows/test-e2e.yml
vendored
2
.github/workflows/test-e2e.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: E2E
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
|
||||
2
.github/workflows/test-frontend.yml
vendored
2
.github/workflows/test-frontend.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: Frontend
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
|
||||
14
.github/workflows/test-integration-17.yml
vendored
14
.github/workflows/test-integration-17.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: Integration@node 17
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
@@ -14,19 +14,15 @@ jobs:
|
||||
PAT_EXISTS: ${{ secrets.GH_PAT != '' }}
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: ci_test
|
||||
redis:
|
||||
image: redis
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
- 6379:6379
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
14
.github/workflows/test-integration.yml
vendored
14
.github/workflows/test-integration.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: Integration
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
@@ -14,19 +14,15 @@ jobs:
|
||||
PAT_EXISTS: ${{ secrets.GH_PAT != '' }}
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: ci_test
|
||||
redis:
|
||||
image: redis
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
- 6379:6379
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
2
.github/workflows/test-lint.yml
vendored
2
.github/workflows/test-lint.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: Lint
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
|
||||
2
.github/workflows/test-main-17.yml
vendored
2
.github/workflows/test-main-17.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: Main@node 17
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
|
||||
2
.github/workflows/test-main.yml
vendored
2
.github/workflows/test-main.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: Main
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
|
||||
2
.github/workflows/test-package-cli.yml
vendored
2
.github/workflows/test-package-cli.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: Package CLI
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
|
||||
2
.github/workflows/test-package-lib.yml
vendored
2
.github/workflows/test-package-lib.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: Package Library
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
|
||||
2
.github/workflows/update-github-api.yml
vendored
2
.github/workflows/update-github-api.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
run: node scripts/update-github-api.js
|
||||
|
||||
- name: Create Pull Request if config has changed
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
commit-message: Update GitHub API Version
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -96,7 +96,6 @@ typings/
|
||||
badge-examples.json
|
||||
supported-features.json
|
||||
service-definitions.yml
|
||||
frontend/categories/*.yaml
|
||||
|
||||
# Local runtime configuration.
|
||||
/config/local*.yml
|
||||
@@ -118,6 +117,3 @@ frontend/categories/*.yaml
|
||||
|
||||
# Flamebearer
|
||||
flamegraph.html
|
||||
|
||||
# config file for node-pg-migrate
|
||||
migrations-config.json
|
||||
|
||||
49
CHANGELOG.md
49
CHANGELOG.md
@@ -4,55 +4,6 @@ Note: this changelog is for the shields.io server. The changelog for the badge-m
|
||||
|
||||
---
|
||||
|
||||
## server-2023-06-01
|
||||
|
||||
- feat: Add total commits to [GitHubCommitActivity] [#9196](https://github.com/badges/shields/issues/9196)
|
||||
- set a custom error on 429 [#9159](https://github.com/badges/shields/issues/9159)
|
||||
- deprecate [travis].org badges [#9171](https://github.com/badges/shields/issues/9171)
|
||||
- count private sponsors on [GithubSponsors] badge [#9170](https://github.com/badges/shields/issues/9170)
|
||||
- Dependency updates
|
||||
|
||||
## server-2023-05-01
|
||||
|
||||
** Removal:** For users who need to maintain a Github Token pool, storage has been provided via the `RedisTokenPersistence` and `REDIS_URL` settings. This feature was deprecated in `server-2023-03-01`. As of this release, the `RedisTokenPersistence` backend is now removed. If you are using this feature, you will need to migrate to using the `SQLTokenPersistence` backend for storage and provide a postgres connection string via the `POSTGRES_URL` setting. [#8922](https://github.com/badges/shields/issues/8922)
|
||||
|
||||
- fail to start server if there are duplicate service names [#9099](https://github.com/badges/shields/issues/9099)
|
||||
- [SourceForge] Added badges for SourceForge [#9078](https://github.com/badges/shields/issues/9078) [#9102](https://github.com/badges/shields/issues/9102)
|
||||
- crates: Use `?include=` to reduce crates.io backend load [#9081](https://github.com/badges/shields/issues/9081)
|
||||
- Dependency updates
|
||||
|
||||
## server-2023-04-02
|
||||
|
||||
- [JenkinsCoverage] Update Jenkins Code Coverage API for new plugin version [#9010](https://github.com/badges/shields/issues/9010)
|
||||
- [CTAN] fallback to date if version is empty [#9036](https://github.com/badges/shields/issues/9036)
|
||||
- Update to [CTAN] API version 2.0 [#9016](https://github.com/badges/shields/issues/9016)
|
||||
- handle missing statistics array in [VisualStudioMarketplace] badges [#8985](https://github.com/badges/shields/issues/8985)
|
||||
- [Netlify] upgrade colors for SVG parsing [#8971](https://github.com/badges/shields/issues/8971)
|
||||
- Fix [Vcpkg] version service for different version fields [#8945](https://github.com/badges/shields/issues/8945)
|
||||
- only try to close pool if one exists [#8947](https://github.com/badges/shields/issues/8947)
|
||||
- misc minor fixes to [githubsize node pypi] [#8946](https://github.com/badges/shields/issues/8946)
|
||||
- Dependency updates
|
||||
|
||||
## server-2023-03-01
|
||||
|
||||
**Deprecation:** For users who need to maintain a Github Token pool, storage has been provided via the `RedisTokenPersistence` and `REDIS_URL` settings. As of this release, the `RedisTokenPersistence` backend is now deprecated and will be removed in a future release. If you are using this feature, you will need to migrate to using the `SQLTokenPersistence` backend for storage and provide a postgres connection string via the `POSTGRES_URL` setting. [#8922](https://github.com/badges/shields/issues/8922)
|
||||
|
||||
- fix: for crates.io versions, use max_stable_version if it exists [#8687](https://github.com/badges/shields/issues/8687)
|
||||
- don't autofocus search [#8927](https://github.com/badges/shields/issues/8927)
|
||||
- Add [Vcpkg] version service [#8923](https://github.com/badges/shields/issues/8923)
|
||||
- fix: Set uid/gid in docker image to 0 [#8908](https://github.com/badges/shields/issues/8908)
|
||||
- expose port 443 in Dockerfile [#8889](https://github.com/badges/shields/issues/8889)
|
||||
- Dependency updates
|
||||
|
||||
## server-2023-02-01
|
||||
|
||||
- replace [twitter] badge with static fallback [#8842](https://github.com/badges/shields/issues/8842)
|
||||
- Add various [Polymart] badges [#8811](https://github.com/badges/shields/issues/8811)
|
||||
- update [githubpipenv] tests/examples [#8797](https://github.com/badges/shields/issues/8797)
|
||||
- deprecate [apm] service [#8773](https://github.com/badges/shields/issues/8773)
|
||||
- deprecate lgtm [#8771](https://github.com/badges/shields/issues/8771)
|
||||
- Dependency updates
|
||||
|
||||
## server-2023-01-01
|
||||
|
||||
- Breaking change: Routes for GitHub workflows badge have changed. See https://github.com/badges/shields/issues/8671 for more details
|
||||
|
||||
@@ -134,19 +134,9 @@ Prettier before a commit by default.
|
||||
When adding or changing a service [please write tests][service-tests], and ensure the [title of your Pull Requests follows the required conventions](#running-service-tests-in-pull-requests) to ensure your tests are executed.
|
||||
When changing other code, please add unit tests.
|
||||
|
||||
The integration tests are not run by default. For most contributions it is OK to skip these unless you're working directly on the code for storing the GitHub token pool in postgres.
|
||||
|
||||
To run the integration tests:
|
||||
|
||||
- You must have PostgreSQL installed. Use `brew install postgresql`, `apt-get install postgresql`, etc.
|
||||
- Set a connection string either with an env var `POSTGRES_URL=postgresql://user:pass@127.0.0.1:5432/db_name` or by using
|
||||
```yaml
|
||||
private:
|
||||
postgres_url: 'postgresql://user:pass@127.0.0.1:5432/db_name'
|
||||
```
|
||||
in a yaml config file.
|
||||
- Run `npm run migrate up` to apply DB migrations
|
||||
- Run `npm run test:integration` to run the tests
|
||||
To run the integration tests, you must have Redis installed and in your PATH.
|
||||
Use `brew install redis`, `yum install redis`, etc. The test runner will
|
||||
start the server automatically.
|
||||
|
||||
[service-tests]: https://github.com/badges/shields/blob/master/doc/service-tests.md
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ LABEL fly.version=$version
|
||||
ENV NODE_ENV production
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY --from=Builder --chown=0:0 /usr/src/app /usr/src/app
|
||||
COPY --from=Builder /usr/src/app /usr/src/app
|
||||
|
||||
CMD node server
|
||||
|
||||
EXPOSE 80 443
|
||||
EXPOSE 80
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use strict'
|
||||
|
||||
const path = require('path')
|
||||
const isSvg = require('is-svg')
|
||||
const { spawn } = require('child-process-promise')
|
||||
const { expect, use } = require('chai')
|
||||
use(require('chai-string'))
|
||||
@@ -19,7 +20,6 @@ describe('The CLI', function () {
|
||||
})
|
||||
|
||||
it('should produce default badges', async function () {
|
||||
const { default: isSvg } = await import('is-svg')
|
||||
const { stdout } = await runCli(['cactus', 'grown'])
|
||||
expect(stdout)
|
||||
.to.satisfy(isSvg)
|
||||
@@ -28,13 +28,11 @@ describe('The CLI', function () {
|
||||
})
|
||||
|
||||
it('should produce colorschemed badges', async function () {
|
||||
const { default: isSvg } = await import('is-svg')
|
||||
const { stdout } = await runCli(['cactus', 'grown', ':green'])
|
||||
expect(stdout).to.satisfy(isSvg)
|
||||
})
|
||||
|
||||
it('should produce right-color badges', async function () {
|
||||
const { default: isSvg } = await import('is-svg')
|
||||
const { stdout } = await runCli(['cactus', 'grown', '#abcdef'])
|
||||
expect(stdout).to.satisfy(isSvg).and.to.include('#abcdef')
|
||||
})
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
'use strict'
|
||||
|
||||
const { expect } = require('chai')
|
||||
const isSvg = require('is-svg')
|
||||
const { makeBadge, ValidationError } = require('.')
|
||||
|
||||
describe('makeBadge function', function () {
|
||||
it('should produce badge with valid input', async function () {
|
||||
const { default: isSvg } = await import('is-svg')
|
||||
it('should produce badge with valid input', function () {
|
||||
expect(
|
||||
makeBadge({
|
||||
label: 'build',
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
const { test, given, forCases } = require('sazerac')
|
||||
const { expect } = require('chai')
|
||||
const snapshot = require('snap-shot-it')
|
||||
const isSvg = require('is-svg')
|
||||
const prettier = require('prettier')
|
||||
const makeBadge = require('./make-badge')
|
||||
|
||||
@@ -79,8 +80,7 @@ describe('The badge generator', function () {
|
||||
})
|
||||
|
||||
describe('SVG', function () {
|
||||
it('should produce SVG', async function () {
|
||||
const { default: isSvg } = await import('is-svg')
|
||||
it('should produce SVG', function () {
|
||||
expect(makeBadge({ label: 'cactus', message: 'grown', format: 'svg' }))
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('cactus')
|
||||
@@ -113,8 +113,7 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should replace undefined svg badge style with "flat"', async function () {
|
||||
const { default: isSvg } = await import('is-svg')
|
||||
it('should replace undefined svg badge style with "flat"', function () {
|
||||
const jsonBadgeWithUnknownStyle = makeBadge({
|
||||
label: 'name',
|
||||
message: 'Bob',
|
||||
|
||||
@@ -94,7 +94,6 @@ private:
|
||||
obs_user: 'OBS_USER'
|
||||
obs_pass: 'OBS_PASS'
|
||||
redis_url: 'REDIS_URL'
|
||||
postgres_url: 'POSTGRES_URL'
|
||||
sentry_dsn: 'SENTRY_DSN'
|
||||
sl_insight_userUuid: 'SL_INSIGHT_USER_UUID'
|
||||
sl_insight_apiToken: 'SL_INSIGHT_API_TOKEN'
|
||||
|
||||
@@ -4,6 +4,7 @@ private:
|
||||
gh_client_id: ...
|
||||
gh_client_secret: ...
|
||||
gitlab_token: ...
|
||||
redis_url: ...
|
||||
sentry_dsn: ...
|
||||
shields_secret: ...
|
||||
sl_insight_userUuid: ...
|
||||
|
||||
@@ -140,15 +140,6 @@ class BaseService {
|
||||
*/
|
||||
static examples = []
|
||||
|
||||
/**
|
||||
* Optional: an OpenAPI Paths Object describing this service's
|
||||
* route or routes in OpenAPI format.
|
||||
*
|
||||
* @see https://swagger.io/specification/#paths-object
|
||||
* @abstract
|
||||
*/
|
||||
static openApi = undefined
|
||||
|
||||
static get _cacheLength() {
|
||||
const cacheLengths = {
|
||||
build: 30,
|
||||
@@ -192,7 +183,7 @@ class BaseService {
|
||||
}
|
||||
|
||||
static getDefinition() {
|
||||
const { category, name, isDeprecated, openApi } = this
|
||||
const { category, name, isDeprecated } = this
|
||||
const { base, format, pattern } = this.route
|
||||
const queryParams = getQueryParamNames(this.route)
|
||||
|
||||
@@ -209,7 +200,7 @@ class BaseService {
|
||||
route = undefined
|
||||
}
|
||||
|
||||
const result = { category, name, isDeprecated, route, examples, openApi }
|
||||
const result = { category, name, isDeprecated, route, examples }
|
||||
|
||||
assertValidServiceDefinition(result, `getDefinition() for ${this.name}`)
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { NotFound, InvalidResponse, Inaccessible } from './errors.js'
|
||||
|
||||
const defaultErrorMessages = {
|
||||
404: 'not found',
|
||||
429: 'rate limited by upstream service',
|
||||
}
|
||||
|
||||
export default function checkErrorResponse(errorMessages = {}) {
|
||||
|
||||
@@ -45,42 +45,6 @@ describe('async error handler', function () {
|
||||
})
|
||||
})
|
||||
|
||||
context('when status is 429', function () {
|
||||
const buffer = Buffer.from('some stuff')
|
||||
const res = { statusCode: 429 }
|
||||
|
||||
it('throws InvalidResponse', async function () {
|
||||
try {
|
||||
await checkErrorResponse()({ res, buffer })
|
||||
expect.fail('Expected to throw')
|
||||
} catch (e) {
|
||||
expect(e).to.be.an.instanceof(InvalidResponse)
|
||||
expect(e.message).to.equal(
|
||||
'Invalid Response: Got status code 429 (expected 200)'
|
||||
)
|
||||
expect(e.prettyMessage).to.equal('rate limited by upstream service')
|
||||
expect(e.response).to.equal(res)
|
||||
expect(e.buffer).to.equal(buffer)
|
||||
}
|
||||
})
|
||||
|
||||
it('displays the custom too many requests', async function () {
|
||||
const notFoundMessage = "terribly sorry but that's one too many requests"
|
||||
try {
|
||||
await checkErrorResponse({ 429: notFoundMessage })({ res, buffer })
|
||||
expect.fail('Expected to throw')
|
||||
} catch (e) {
|
||||
expect(e).to.be.an.instanceof(InvalidResponse)
|
||||
expect(e.message).to.equal(
|
||||
'Invalid Response: Got status code 429 (expected 200)'
|
||||
)
|
||||
expect(e.prettyMessage).to.equal(
|
||||
"terribly sorry but that's one too many requests"
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
context('when status is 4xx', function () {
|
||||
it('throws InvalidResponse', async function () {
|
||||
const res = { statusCode: 499 }
|
||||
|
||||
@@ -129,7 +129,6 @@ function transformExample(inExample, index, ServiceClass) {
|
||||
ServiceClass
|
||||
)
|
||||
|
||||
const category = categories.find(c => c.id === ServiceClass.category)
|
||||
return {
|
||||
title,
|
||||
example: {
|
||||
@@ -147,7 +146,9 @@ function transformExample(inExample, index, ServiceClass) {
|
||||
style: style === 'flat' ? undefined : style,
|
||||
namedLogo,
|
||||
},
|
||||
keywords: category ? keywords.concat(category.keywords) : keywords,
|
||||
keywords: keywords.concat(
|
||||
categories.find(c => c.id === ServiceClass.category).keywords
|
||||
),
|
||||
documentation: documentation ? { __html: documentation } : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import BaseJsonService from '../base-json.js'
|
||||
|
||||
class BadBaseService {}
|
||||
class GoodMixedService extends BaseJsonService {
|
||||
class GoodService extends BaseJsonService {
|
||||
static category = 'build'
|
||||
static route = { base: 'it/is', pattern: 'good' }
|
||||
}
|
||||
class BadMixedService extends BadBaseService {}
|
||||
class BadService extends BadBaseService {}
|
||||
|
||||
export default [GoodMixedService, BadMixedService]
|
||||
export default [GoodService, BadService]
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
class BadNoBaseService {}
|
||||
class BadService {}
|
||||
|
||||
export default BadNoBaseService
|
||||
export default BadService
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
class BadBaseService {}
|
||||
class BadChildService extends BadBaseService {}
|
||||
class BadService extends BadBaseService {}
|
||||
|
||||
export default BadChildService
|
||||
export default BadService
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import BaseJsonService from '../base-json.js'
|
||||
|
||||
class GoodServiceArrayOne extends BaseJsonService {
|
||||
class GoodServiceOne extends BaseJsonService {
|
||||
static category = 'build'
|
||||
static route = { base: 'good', pattern: 'one' }
|
||||
}
|
||||
class GoodServiceArrayTwo extends BaseJsonService {
|
||||
class GoodServiceTwo extends BaseJsonService {
|
||||
static category = 'build'
|
||||
static route = { base: 'good', pattern: 'two' }
|
||||
}
|
||||
|
||||
export default [GoodServiceArrayOne, GoodServiceArrayTwo]
|
||||
export default [GoodServiceOne, GoodServiceTwo]
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import BaseJsonService from '../base-json.js'
|
||||
|
||||
class GoodServiceObjectOne extends BaseJsonService {
|
||||
class GoodServiceOne extends BaseJsonService {
|
||||
static category = 'build'
|
||||
static route = { base: 'good', pattern: 'one' }
|
||||
}
|
||||
class GoodServiceObjectTwo extends BaseJsonService {
|
||||
class GoodServiceTwo extends BaseJsonService {
|
||||
static category = 'build'
|
||||
static route = { base: 'good', pattern: 'two' }
|
||||
}
|
||||
|
||||
export { GoodServiceObjectOne, GoodServiceObjectTwo }
|
||||
export { GoodServiceOne, GoodServiceTwo }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { globSync } from 'glob'
|
||||
import glob from 'glob'
|
||||
import countBy from 'lodash.countby'
|
||||
import categories from '../../services/categories.js'
|
||||
import BaseService from './base.js'
|
||||
@@ -27,25 +27,11 @@ class InvalidService extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
function getServicePaths(pattern) {
|
||||
return globSync(toUnixPath(path.join(serviceDir, '**', pattern))).sort()
|
||||
}
|
||||
|
||||
function assertNamesUnique(names, { message }) {
|
||||
const duplicates = {}
|
||||
Object.entries(countBy(names))
|
||||
.filter(([name, count]) => count > 1)
|
||||
.forEach(([name, count]) => {
|
||||
duplicates[name] = count
|
||||
})
|
||||
if (Object.keys(duplicates).length) {
|
||||
throw new Error(`${message}: ${JSON.stringify(duplicates, undefined, 2)}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadServiceClasses(servicePaths) {
|
||||
if (!servicePaths) {
|
||||
servicePaths = getServicePaths('*.service.js')
|
||||
servicePaths = glob.sync(
|
||||
toUnixPath(path.join(serviceDir, '**', '*.service.js'))
|
||||
)
|
||||
}
|
||||
|
||||
const serviceClasses = []
|
||||
@@ -65,8 +51,8 @@ async function loadServiceClasses(servicePaths) {
|
||||
if (serviceClass && serviceClass.prototype instanceof BaseService) {
|
||||
// Decorate each service class with the directory that contains it.
|
||||
serviceClass.serviceFamily = servicePath
|
||||
.replace(serviceDir, '')
|
||||
.split(path.sep)[1]
|
||||
.replace(toUnixPath(serviceDir), '')
|
||||
.split('/')[1]
|
||||
serviceClass.validateDefinition()
|
||||
return serviceClasses.push(serviceClass)
|
||||
}
|
||||
@@ -76,14 +62,29 @@ async function loadServiceClasses(servicePaths) {
|
||||
})
|
||||
}
|
||||
|
||||
return serviceClasses
|
||||
}
|
||||
|
||||
function assertNamesUnique(names, { message }) {
|
||||
const duplicates = {}
|
||||
Object.entries(countBy(names))
|
||||
.filter(([name, count]) => count > 1)
|
||||
.forEach(([name, count]) => {
|
||||
duplicates[name] = count
|
||||
})
|
||||
if (Object.keys(duplicates).length) {
|
||||
throw new Error(`${message}: ${JSON.stringify(duplicates, undefined, 2)}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function checkNames() {
|
||||
const services = await loadServiceClasses()
|
||||
assertNamesUnique(
|
||||
serviceClasses.map(({ name }) => name),
|
||||
services.map(({ name }) => name),
|
||||
{
|
||||
message: 'Duplicate service names found',
|
||||
}
|
||||
)
|
||||
|
||||
return serviceClasses
|
||||
}
|
||||
|
||||
async function collectDefinitions() {
|
||||
@@ -101,16 +102,16 @@ async function collectDefinitions() {
|
||||
|
||||
async function loadTesters() {
|
||||
return Promise.all(
|
||||
getServicePaths('*.tester.js').map(
|
||||
async path => await import(`file://${path}`)
|
||||
)
|
||||
glob
|
||||
.sync(path.join(serviceDir, '**', '*.tester.js'))
|
||||
.map(async path => await import(`file://${path}`))
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
InvalidService,
|
||||
loadServiceClasses,
|
||||
getServicePaths,
|
||||
checkNames,
|
||||
collectDefinitions,
|
||||
loadTesters,
|
||||
}
|
||||
|
||||
@@ -2,11 +2,7 @@ import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import chai from 'chai'
|
||||
import chaiAsPromised from 'chai-as-promised'
|
||||
import {
|
||||
loadServiceClasses,
|
||||
getServicePaths,
|
||||
InvalidService,
|
||||
} from './loader.js'
|
||||
import { loadServiceClasses, InvalidService } from './loader.js'
|
||||
chai.use(chaiAsPromised)
|
||||
|
||||
const { expect } = chai
|
||||
@@ -69,15 +65,3 @@ describe('loadServiceClasses function', function () {
|
||||
).to.eventually.have.length(5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getServicePaths', function () {
|
||||
// these tests just make sure we discover a
|
||||
// plausibly large number of .service and .tester files
|
||||
it('finds a non-zero number of services in the project', function () {
|
||||
expect(getServicePaths('*.service.js')).to.have.length.above(400)
|
||||
})
|
||||
|
||||
it('finds a non-zero number of testers in the project', function () {
|
||||
expect(getServicePaths('*.tester.js')).to.have.length.above(400)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,335 +0,0 @@
|
||||
const baseUrl = process.env.BASE_URL || 'https://img.shields.io'
|
||||
const globalParamRefs = [
|
||||
{ $ref: '#/components/parameters/style' },
|
||||
{ $ref: '#/components/parameters/logo' },
|
||||
{ $ref: '#/components/parameters/logoColor' },
|
||||
{ $ref: '#/components/parameters/label' },
|
||||
{ $ref: '#/components/parameters/labelColor' },
|
||||
{ $ref: '#/components/parameters/color' },
|
||||
{ $ref: '#/components/parameters/cacheSeconds' },
|
||||
{ $ref: '#/components/parameters/link' },
|
||||
]
|
||||
|
||||
function getCodeSamples(altText) {
|
||||
return [
|
||||
{
|
||||
lang: 'URL',
|
||||
label: 'URL',
|
||||
source: '$url',
|
||||
},
|
||||
{
|
||||
lang: 'Markdown',
|
||||
label: 'Markdown',
|
||||
source: ``,
|
||||
},
|
||||
{
|
||||
lang: 'reStructuredText',
|
||||
label: 'rSt',
|
||||
source: `.. image:: $url\n: alt: ${altText}`,
|
||||
},
|
||||
{
|
||||
lang: 'AsciiDoc',
|
||||
label: 'AsciiDoc',
|
||||
source: `image:$url[${altText}]`,
|
||||
},
|
||||
{
|
||||
lang: 'HTML',
|
||||
label: 'HTML',
|
||||
source: `<img alt="${altText}" src="$url">`,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function pattern2openapi(pattern) {
|
||||
return pattern
|
||||
.replace(/:([A-Za-z0-9_\-.]+)(?=[/]?)/g, (matches, grp1) => `{${grp1}}`)
|
||||
.replace(/\([^)]*\)/g, '')
|
||||
.replace(/\+$/, '')
|
||||
}
|
||||
|
||||
function getEnum(pattern, paramName) {
|
||||
const re = new RegExp(`${paramName}\\(([A-Za-z0-9_\\-|]+)\\)`)
|
||||
const match = pattern.match(re)
|
||||
if (match === null) {
|
||||
return undefined
|
||||
}
|
||||
if (!match[1].includes('|')) {
|
||||
return undefined
|
||||
}
|
||||
return match[1].split('|')
|
||||
}
|
||||
|
||||
function param2openapi(pattern, paramName, exampleValue, paramType) {
|
||||
const outParam = {}
|
||||
outParam.name = paramName
|
||||
// We don't have description if we are building the OpenAPI spec from examples[]
|
||||
outParam.in = paramType
|
||||
if (paramType === 'path') {
|
||||
outParam.required = true
|
||||
} else {
|
||||
/* Occasionally we do have required query params, but we can't
|
||||
detect this if we are building the OpenAPI spec from examples[]
|
||||
so just assume all query params are optional */
|
||||
outParam.required = false
|
||||
}
|
||||
|
||||
if (exampleValue === null && paramType === 'query') {
|
||||
outParam.schema = { type: 'boolean' }
|
||||
outParam.allowEmptyValue = true
|
||||
} else {
|
||||
outParam.schema = { type: 'string' }
|
||||
}
|
||||
|
||||
if (paramType === 'path') {
|
||||
outParam.schema.enum = getEnum(pattern, paramName)
|
||||
}
|
||||
|
||||
outParam.example = exampleValue
|
||||
return outParam
|
||||
}
|
||||
|
||||
function getVariants(pattern) {
|
||||
/*
|
||||
given a URL pattern (which may include '/one/or/:more?/:optional/:parameters*')
|
||||
return an array of all possible permutations:
|
||||
[
|
||||
'/one/or/:more/:optional/:parameters',
|
||||
'/one/or/:optional/:parameters',
|
||||
'/one/or/:more/:optional',
|
||||
'/one/or/:optional',
|
||||
]
|
||||
*/
|
||||
const patterns = [pattern.split('/')]
|
||||
while (patterns.flat().find(p => p.endsWith('?') || p.endsWith('*'))) {
|
||||
for (let i = 0; i < patterns.length; i++) {
|
||||
const pattern = patterns[i]
|
||||
for (let j = 0; j < pattern.length; j++) {
|
||||
const path = pattern[j]
|
||||
if (path.endsWith('?') || path.endsWith('*')) {
|
||||
pattern[j] = path.slice(0, -1)
|
||||
patterns.push(patterns[i].filter(p => p !== pattern[j]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < patterns.length; i++) {
|
||||
patterns[i] = patterns[i].join('/')
|
||||
}
|
||||
return patterns
|
||||
}
|
||||
|
||||
function examples2openapi(examples) {
|
||||
const paths = {}
|
||||
for (const example of examples) {
|
||||
const patterns = getVariants(example.example.pattern)
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const openApiPattern = pattern2openapi(pattern)
|
||||
if (
|
||||
openApiPattern.includes('*') ||
|
||||
openApiPattern.includes('?') ||
|
||||
openApiPattern.includes('+') ||
|
||||
openApiPattern.includes('(')
|
||||
) {
|
||||
throw new Error(`unexpected characters in pattern '${openApiPattern}'`)
|
||||
}
|
||||
|
||||
/*
|
||||
There's several things going on in this block:
|
||||
1. Filter out any examples for params that don't appear
|
||||
in this variant of the route
|
||||
2. Make sure we add params to the array
|
||||
in the same order they appear in the route
|
||||
3. If there are any params we don't have an example value for,
|
||||
make sure they still appear in the pathParams array with
|
||||
exampleValue == undefined anyway
|
||||
*/
|
||||
const pathParams = []
|
||||
for (const param of openApiPattern
|
||||
.split('/')
|
||||
.filter(p => p.startsWith('{') && p.endsWith('}'))) {
|
||||
const paramName = param.slice(1, -1)
|
||||
const exampleValue = example.example.namedParams[paramName]
|
||||
pathParams.push(param2openapi(pattern, paramName, exampleValue, 'path'))
|
||||
}
|
||||
|
||||
const queryParams = example.example.queryParams || {}
|
||||
|
||||
const parameters = [
|
||||
...pathParams,
|
||||
...Object.entries(queryParams).map(([paramName, exampleValue]) =>
|
||||
param2openapi(pattern, paramName, exampleValue, 'query')
|
||||
),
|
||||
...globalParamRefs,
|
||||
]
|
||||
paths[openApiPattern] = {
|
||||
get: {
|
||||
summary: example.title,
|
||||
description: example?.documentation?.__html
|
||||
.replace(/<br>/g, '<br />') // react does not like <br>
|
||||
.replace(/{/g, '{')
|
||||
.replace(/}/g, '}')
|
||||
.replace(/<style>(.|\n)*?<\/style>/, ''), // workaround for w3c-validation TODO: remove later
|
||||
parameters,
|
||||
'x-code-samples': getCodeSamples(example.title),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
function addGlobalProperties(endpoints) {
|
||||
const paths = {}
|
||||
for (const key of Object.keys(endpoints)) {
|
||||
paths[key] = endpoints[key]
|
||||
paths[key].get.parameters = [
|
||||
...paths[key].get.parameters,
|
||||
...globalParamRefs,
|
||||
]
|
||||
paths[key].get['x-code-samples'] = getCodeSamples(paths[key].get.summary)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
function services2openapi(services) {
|
||||
const paths = {}
|
||||
for (const service of services) {
|
||||
if (service.openApi) {
|
||||
// if the service declares its own OpenAPI definition, use that...
|
||||
for (const [key, value] of Object.entries(
|
||||
addGlobalProperties(service.openApi)
|
||||
)) {
|
||||
if (key in paths) {
|
||||
throw new Error(`Conflicting route: ${key}`)
|
||||
}
|
||||
paths[key] = value
|
||||
}
|
||||
} else {
|
||||
// ...otherwise do our best to build one from examples[]
|
||||
for (const [key, value] of Object.entries(
|
||||
examples2openapi(service.examples)
|
||||
)) {
|
||||
// allow conflicting routes for legacy examples
|
||||
paths[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
function category2openapi(category, services) {
|
||||
const spec = {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
version: '1.0.0',
|
||||
title: category.name,
|
||||
license: {
|
||||
name: 'CC0',
|
||||
},
|
||||
},
|
||||
servers: [{ url: baseUrl }],
|
||||
components: {
|
||||
parameters: {
|
||||
style: {
|
||||
name: 'style',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'One of: flat (default), flat-square, plastic, for-the-badge, social',
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
example: 'flat',
|
||||
},
|
||||
logo: {
|
||||
name: 'logo',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'One of the named logos (bitcoin, dependabot, gitlab, npm, paypal, serverfault, stackexchange, superuser, telegram, travis) or simple-icons. All simple-icons are referenced using icon slugs. You can click the icon title on <a href="https://simpleicons.org/" rel="noopener noreferrer" target="_blank">simple-icons</a> to copy the slug or they can be found in the <a href="https://github.com/simple-icons/simple-icons/blob/master/slugs.md">slugs.md file</a> in the simple-icons repository.',
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
example: 'appveyor',
|
||||
},
|
||||
logoColor: {
|
||||
name: 'logoColor',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'The color of the logo (hex, rgb, rgba, hsl, hsla and css named colors supported). Supported for named logos and Shields logos but not for custom logos. For multicolor Shields logos, the corresponding named logo will be used and colored.',
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
example: 'violet',
|
||||
},
|
||||
label: {
|
||||
name: 'label',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'Override the default left-hand-side text (<a href="https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding">URL-Encoding</a> needed for spaces or special characters!)',
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
example: 'healthiness',
|
||||
},
|
||||
labelColor: {
|
||||
name: 'labelColor',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'Background color of the left part (hex, rgb, rgba, hsl, hsla and css named colors supported).',
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
example: 'abcdef',
|
||||
},
|
||||
color: {
|
||||
name: 'color',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'Background color of the right part (hex, rgb, rgba, hsl, hsla and css named colors supported).',
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
example: 'fedcba',
|
||||
},
|
||||
cacheSeconds: {
|
||||
name: 'cacheSeconds',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'HTTP cache lifetime (rules are applied to infer a default value on a per-badge basis, any values specified below the default will be ignored).',
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
example: '3600',
|
||||
},
|
||||
link: {
|
||||
name: 'link',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'Specify what clicking on the left/right of a badge should do. Note that this only works when integrating your badge in an `<object>` HTML tag, but not an `<img>` tag or a markup language.',
|
||||
style: 'form',
|
||||
explode: true,
|
||||
schema: {
|
||||
type: 'array',
|
||||
maxItems: 2,
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
paths: services2openapi(services),
|
||||
}
|
||||
|
||||
return spec
|
||||
}
|
||||
|
||||
export { category2openapi }
|
||||
@@ -1,379 +0,0 @@
|
||||
import chai from 'chai'
|
||||
import { category2openapi } from './openapi.js'
|
||||
import BaseJsonService from './base-json.js'
|
||||
const { expect } = chai
|
||||
|
||||
class OpenApiService extends BaseJsonService {
|
||||
static category = 'build'
|
||||
static route = { base: 'openapi/service', pattern: ':packageName/:distTag*' }
|
||||
|
||||
// this service defines its own API Paths Object
|
||||
static openApi = {
|
||||
'/openapi/service/{packageName}': {
|
||||
get: {
|
||||
summary: 'OpenApiService Summary',
|
||||
description: 'OpenApiService Description',
|
||||
parameters: [
|
||||
{
|
||||
name: 'packageName',
|
||||
description: 'packageName description',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'badge-maker',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'/openapi/service/{packageName}/{distTag}': {
|
||||
get: {
|
||||
summary: 'OpenApiService Summary (with Tag)',
|
||||
description: 'OpenApiService Description (with Tag)',
|
||||
parameters: [
|
||||
{
|
||||
name: 'packageName',
|
||||
description: 'packageName description',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'badge-maker',
|
||||
},
|
||||
{
|
||||
name: 'distTag',
|
||||
description: 'distTag description',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'latest',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
class LegacyService extends BaseJsonService {
|
||||
static category = 'build'
|
||||
static route = { base: 'legacy/service', pattern: ':packageName/:distTag*' }
|
||||
|
||||
// this service defines an Examples Array
|
||||
static examples = [
|
||||
{
|
||||
title: 'LegacyService Title',
|
||||
namedParams: { packageName: 'badge-maker' },
|
||||
staticPreview: { label: 'build', message: 'passing' },
|
||||
documentation: 'LegacyService Description',
|
||||
},
|
||||
{
|
||||
title: 'LegacyService Title (with Tag)',
|
||||
namedParams: { packageName: 'badge-maker', distTag: 'latest' },
|
||||
staticPreview: { label: 'build', message: 'passing' },
|
||||
documentation: 'LegacyService Description (with Tag)',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const expected = {
|
||||
openapi: '3.0.0',
|
||||
info: { version: '1.0.0', title: 'build', license: { name: 'CC0' } },
|
||||
servers: [{ url: 'https://img.shields.io' }],
|
||||
components: {
|
||||
parameters: {
|
||||
style: {
|
||||
name: 'style',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'One of: flat (default), flat-square, plastic, for-the-badge, social',
|
||||
schema: { type: 'string' },
|
||||
example: 'flat',
|
||||
},
|
||||
logo: {
|
||||
name: 'logo',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'One of the named logos (bitcoin, dependabot, gitlab, npm, paypal, serverfault, stackexchange, superuser, telegram, travis) or simple-icons. All simple-icons are referenced using icon slugs. You can click the icon title on <a href="https://simpleicons.org/" rel="noopener noreferrer" target="_blank">simple-icons</a> to copy the slug or they can be found in the <a href="https://github.com/simple-icons/simple-icons/blob/master/slugs.md">slugs.md file</a> in the simple-icons repository.',
|
||||
schema: { type: 'string' },
|
||||
example: 'appveyor',
|
||||
},
|
||||
logoColor: {
|
||||
name: 'logoColor',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'The color of the logo (hex, rgb, rgba, hsl, hsla and css named colors supported). Supported for named logos and Shields logos but not for custom logos. For multicolor Shields logos, the corresponding named logo will be used and colored.',
|
||||
schema: { type: 'string' },
|
||||
example: 'violet',
|
||||
},
|
||||
label: {
|
||||
name: 'label',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'Override the default left-hand-side text (<a href="https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding">URL-Encoding</a> needed for spaces or special characters!)',
|
||||
schema: { type: 'string' },
|
||||
example: 'healthiness',
|
||||
},
|
||||
labelColor: {
|
||||
name: 'labelColor',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'Background color of the left part (hex, rgb, rgba, hsl, hsla and css named colors supported).',
|
||||
schema: { type: 'string' },
|
||||
example: 'abcdef',
|
||||
},
|
||||
color: {
|
||||
name: 'color',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'Background color of the right part (hex, rgb, rgba, hsl, hsla and css named colors supported).',
|
||||
schema: { type: 'string' },
|
||||
example: 'fedcba',
|
||||
},
|
||||
cacheSeconds: {
|
||||
name: 'cacheSeconds',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'HTTP cache lifetime (rules are applied to infer a default value on a per-badge basis, any values specified below the default will be ignored).',
|
||||
schema: { type: 'string' },
|
||||
example: '3600',
|
||||
},
|
||||
link: {
|
||||
name: 'link',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'Specify what clicking on the left/right of a badge should do. Note that this only works when integrating your badge in an `<object>` HTML tag, but not an `<img>` tag or a markup language.',
|
||||
style: 'form',
|
||||
explode: true,
|
||||
schema: { type: 'array', maxItems: 2, items: { type: 'string' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
paths: {
|
||||
'/openapi/service/{packageName}': {
|
||||
get: {
|
||||
summary: 'OpenApiService Summary',
|
||||
description: 'OpenApiService Description',
|
||||
parameters: [
|
||||
{
|
||||
name: 'packageName',
|
||||
description: 'packageName description',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'badge-maker',
|
||||
},
|
||||
{ $ref: '#/components/parameters/style' },
|
||||
{ $ref: '#/components/parameters/logo' },
|
||||
{ $ref: '#/components/parameters/logoColor' },
|
||||
{ $ref: '#/components/parameters/label' },
|
||||
{ $ref: '#/components/parameters/labelColor' },
|
||||
{ $ref: '#/components/parameters/color' },
|
||||
{ $ref: '#/components/parameters/cacheSeconds' },
|
||||
{ $ref: '#/components/parameters/link' },
|
||||
],
|
||||
'x-code-samples': [
|
||||
{ lang: 'URL', label: 'URL', source: '$url' },
|
||||
{
|
||||
lang: 'Markdown',
|
||||
label: 'Markdown',
|
||||
source: '',
|
||||
},
|
||||
{
|
||||
lang: 'reStructuredText',
|
||||
label: 'rSt',
|
||||
source: '.. image:: $url\n: alt: OpenApiService Summary',
|
||||
},
|
||||
{
|
||||
lang: 'AsciiDoc',
|
||||
label: 'AsciiDoc',
|
||||
source: 'image:$url[OpenApiService Summary]',
|
||||
},
|
||||
{
|
||||
lang: 'HTML',
|
||||
label: 'HTML',
|
||||
source: '<img alt="OpenApiService Summary" src="$url">',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'/openapi/service/{packageName}/{distTag}': {
|
||||
get: {
|
||||
summary: 'OpenApiService Summary (with Tag)',
|
||||
description: 'OpenApiService Description (with Tag)',
|
||||
parameters: [
|
||||
{
|
||||
name: 'packageName',
|
||||
description: 'packageName description',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'badge-maker',
|
||||
},
|
||||
{
|
||||
name: 'distTag',
|
||||
description: 'distTag description',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'latest',
|
||||
},
|
||||
{ $ref: '#/components/parameters/style' },
|
||||
{ $ref: '#/components/parameters/logo' },
|
||||
{ $ref: '#/components/parameters/logoColor' },
|
||||
{ $ref: '#/components/parameters/label' },
|
||||
{ $ref: '#/components/parameters/labelColor' },
|
||||
{ $ref: '#/components/parameters/color' },
|
||||
{ $ref: '#/components/parameters/cacheSeconds' },
|
||||
{ $ref: '#/components/parameters/link' },
|
||||
],
|
||||
'x-code-samples': [
|
||||
{ lang: 'URL', label: 'URL', source: '$url' },
|
||||
{
|
||||
lang: 'Markdown',
|
||||
label: 'Markdown',
|
||||
source: '',
|
||||
},
|
||||
{
|
||||
lang: 'reStructuredText',
|
||||
label: 'rSt',
|
||||
source:
|
||||
'.. image:: $url\n: alt: OpenApiService Summary (with Tag)',
|
||||
},
|
||||
{
|
||||
lang: 'AsciiDoc',
|
||||
label: 'AsciiDoc',
|
||||
source: 'image:$url[OpenApiService Summary (with Tag)]',
|
||||
},
|
||||
{
|
||||
lang: 'HTML',
|
||||
label: 'HTML',
|
||||
source: '<img alt="OpenApiService Summary (with Tag)" src="$url">',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'/legacy/service/{packageName}/{distTag}': {
|
||||
get: {
|
||||
summary: 'LegacyService Title (with Tag)',
|
||||
description: 'LegacyService Description (with Tag)',
|
||||
parameters: [
|
||||
{
|
||||
name: 'packageName',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'badge-maker',
|
||||
},
|
||||
{
|
||||
name: 'distTag',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'latest',
|
||||
},
|
||||
{ $ref: '#/components/parameters/style' },
|
||||
{ $ref: '#/components/parameters/logo' },
|
||||
{ $ref: '#/components/parameters/logoColor' },
|
||||
{ $ref: '#/components/parameters/label' },
|
||||
{ $ref: '#/components/parameters/labelColor' },
|
||||
{ $ref: '#/components/parameters/color' },
|
||||
{ $ref: '#/components/parameters/cacheSeconds' },
|
||||
{ $ref: '#/components/parameters/link' },
|
||||
],
|
||||
'x-code-samples': [
|
||||
{ lang: 'URL', label: 'URL', source: '$url' },
|
||||
{
|
||||
lang: 'Markdown',
|
||||
label: 'Markdown',
|
||||
source: '',
|
||||
},
|
||||
{
|
||||
lang: 'reStructuredText',
|
||||
label: 'rSt',
|
||||
source: '.. image:: $url\n: alt: LegacyService Title (with Tag)',
|
||||
},
|
||||
{
|
||||
lang: 'AsciiDoc',
|
||||
label: 'AsciiDoc',
|
||||
source: 'image:$url[LegacyService Title (with Tag)]',
|
||||
},
|
||||
{
|
||||
lang: 'HTML',
|
||||
label: 'HTML',
|
||||
source: '<img alt="LegacyService Title (with Tag)" src="$url">',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'/legacy/service/{packageName}': {
|
||||
get: {
|
||||
summary: 'LegacyService Title (with Tag)',
|
||||
description: 'LegacyService Description (with Tag)',
|
||||
parameters: [
|
||||
{
|
||||
name: 'packageName',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'badge-maker',
|
||||
},
|
||||
{ $ref: '#/components/parameters/style' },
|
||||
{ $ref: '#/components/parameters/logo' },
|
||||
{ $ref: '#/components/parameters/logoColor' },
|
||||
{ $ref: '#/components/parameters/label' },
|
||||
{ $ref: '#/components/parameters/labelColor' },
|
||||
{ $ref: '#/components/parameters/color' },
|
||||
{ $ref: '#/components/parameters/cacheSeconds' },
|
||||
{ $ref: '#/components/parameters/link' },
|
||||
],
|
||||
'x-code-samples': [
|
||||
{ lang: 'URL', label: 'URL', source: '$url' },
|
||||
{
|
||||
lang: 'Markdown',
|
||||
label: 'Markdown',
|
||||
source: '',
|
||||
},
|
||||
{
|
||||
lang: 'reStructuredText',
|
||||
label: 'rSt',
|
||||
source: '.. image:: $url\n: alt: LegacyService Title (with Tag)',
|
||||
},
|
||||
{
|
||||
lang: 'AsciiDoc',
|
||||
label: 'AsciiDoc',
|
||||
source: 'image:$url[LegacyService Title (with Tag)]',
|
||||
},
|
||||
{
|
||||
lang: 'HTML',
|
||||
label: 'HTML',
|
||||
source: '<img alt="LegacyService Title (with Tag)" src="$url">',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
function clean(obj) {
|
||||
// remove any undefined values in the object
|
||||
return JSON.parse(JSON.stringify(obj))
|
||||
}
|
||||
|
||||
describe('category2openapi', function () {
|
||||
it('generates an Open API spec', function () {
|
||||
expect(
|
||||
clean(
|
||||
category2openapi({ name: 'build' }, [
|
||||
OpenApiService.getDefinition(),
|
||||
LegacyService.getDefinition(),
|
||||
])
|
||||
)
|
||||
).to.deep.equal(expected)
|
||||
})
|
||||
})
|
||||
@@ -46,28 +46,6 @@ const serviceDefinition = Joi.object({
|
||||
})
|
||||
)
|
||||
.default([]),
|
||||
openApi: Joi.object().pattern(
|
||||
/./,
|
||||
Joi.object({
|
||||
get: Joi.object({
|
||||
summary: Joi.string().required(),
|
||||
description: Joi.string().required(),
|
||||
parameters: Joi.array()
|
||||
.items(
|
||||
Joi.object({
|
||||
name: Joi.string().required(),
|
||||
description: Joi.string(),
|
||||
in: Joi.string().valid('query', 'path').required(),
|
||||
required: Joi.boolean().required(),
|
||||
schema: Joi.object({ type: Joi.string().required() }).required(),
|
||||
example: Joi.string(),
|
||||
})
|
||||
)
|
||||
.min(1)
|
||||
.required(),
|
||||
}).required(),
|
||||
}).required()
|
||||
),
|
||||
}).required()
|
||||
|
||||
function assertValidServiceDefinition(example, message = undefined) {
|
||||
|
||||
@@ -183,7 +183,6 @@ const privateConfigSchema = Joi.object({
|
||||
obs_user: Joi.string(),
|
||||
obs_pass: Joi.string(),
|
||||
redis_url: Joi.string().uri({ scheme: ['redis', 'rediss'] }),
|
||||
postgres_url: Joi.string().uri({ scheme: 'postgresql' }),
|
||||
sentry_dsn: Joi.string(),
|
||||
sl_insight_userUuid: Joi.string(),
|
||||
sl_insight_apiToken: Joi.string(),
|
||||
|
||||
95
core/token-pooling/redis-token-persistence.integration.js
Normal file
95
core/token-pooling/redis-token-persistence.integration.js
Normal file
@@ -0,0 +1,95 @@
|
||||
import RedisServer from 'redis-server'
|
||||
import Redis from 'ioredis'
|
||||
import { expect } from 'chai'
|
||||
import RedisTokenPersistence from './redis-token-persistence.js'
|
||||
|
||||
describe('Redis token persistence', function () {
|
||||
let server
|
||||
// In CI, expect redis already to be running.
|
||||
if (!process.env.CI) {
|
||||
beforeEach(async function () {
|
||||
server = new RedisServer({ config: { host: 'localhost' } })
|
||||
await server.open()
|
||||
})
|
||||
}
|
||||
|
||||
const key = 'tokenPersistenceIntegrationTest'
|
||||
|
||||
let redis
|
||||
beforeEach(async function () {
|
||||
redis = new Redis()
|
||||
await redis.del(key)
|
||||
})
|
||||
afterEach(async function () {
|
||||
if (redis) {
|
||||
await redis.quit()
|
||||
redis = undefined
|
||||
}
|
||||
})
|
||||
|
||||
if (!process.env.CI) {
|
||||
afterEach(async function () {
|
||||
await server.close()
|
||||
server = undefined
|
||||
})
|
||||
}
|
||||
|
||||
let persistence
|
||||
beforeEach(function () {
|
||||
persistence = new RedisTokenPersistence({ key })
|
||||
})
|
||||
afterEach(async function () {
|
||||
if (persistence) {
|
||||
await persistence.stop()
|
||||
persistence = undefined
|
||||
}
|
||||
})
|
||||
|
||||
context('when the key does not exist', function () {
|
||||
it('does nothing', async function () {
|
||||
const tokens = await persistence.initialize()
|
||||
expect(tokens).to.deep.equal([])
|
||||
})
|
||||
})
|
||||
|
||||
context('when the key exists', function () {
|
||||
const initialTokens = ['a', 'b', 'c'].map(char => char.repeat(40))
|
||||
|
||||
beforeEach(async function () {
|
||||
await redis.sadd(key, initialTokens)
|
||||
})
|
||||
|
||||
it('loads the contents', async function () {
|
||||
const tokens = await persistence.initialize()
|
||||
expect(tokens.sort()).to.deep.equal(initialTokens)
|
||||
})
|
||||
|
||||
context('when tokens are added', function () {
|
||||
it('saves the change', async function () {
|
||||
const newToken = 'e'.repeat(40)
|
||||
const expected = initialTokens.slice()
|
||||
expected.push(newToken)
|
||||
|
||||
await persistence.initialize()
|
||||
await persistence.noteTokenAdded(newToken)
|
||||
|
||||
const savedTokens = await redis.smembers(key)
|
||||
expect(savedTokens.sort()).to.deep.equal(expected)
|
||||
})
|
||||
})
|
||||
|
||||
context('when tokens are removed', function () {
|
||||
it('saves the change', async function () {
|
||||
const expected = Array.from(initialTokens)
|
||||
const toRemove = expected.pop()
|
||||
|
||||
await persistence.initialize()
|
||||
|
||||
await persistence.noteTokenRemoved(toRemove)
|
||||
|
||||
const savedTokens = await redis.smembers(key)
|
||||
expect(savedTokens.sort()).to.deep.equal(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
57
core/token-pooling/redis-token-persistence.js
Normal file
57
core/token-pooling/redis-token-persistence.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { URL } from 'url'
|
||||
import Redis from 'ioredis'
|
||||
import log from '../server/log.js'
|
||||
|
||||
export default class RedisTokenPersistence {
|
||||
constructor({ url, key }) {
|
||||
this.url = url
|
||||
this.key = key
|
||||
this.noteTokenAdded = this.noteTokenAdded.bind(this)
|
||||
this.noteTokenRemoved = this.noteTokenRemoved.bind(this)
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
const options =
|
||||
this.url && this.url.startsWith('rediss:')
|
||||
? {
|
||||
// https://www.compose.com/articles/ssl-connections-arrive-for-redis-on-compose/
|
||||
tls: { servername: new URL(this.url).hostname },
|
||||
}
|
||||
: undefined
|
||||
this.redis = new Redis(this.url, options)
|
||||
this.redis.on('error', e => {
|
||||
log.error(e)
|
||||
})
|
||||
|
||||
const tokens = await this.redis.smembers(this.key)
|
||||
return tokens
|
||||
}
|
||||
|
||||
async stop() {
|
||||
await this.redis.quit()
|
||||
}
|
||||
|
||||
async onTokenAdded(token) {
|
||||
await this.redis.sadd(this.key, token)
|
||||
}
|
||||
|
||||
async onTokenRemoved(token) {
|
||||
await this.redis.srem(this.key, token)
|
||||
}
|
||||
|
||||
async noteTokenAdded(token) {
|
||||
try {
|
||||
await this.onTokenAdded(token)
|
||||
} catch (e) {
|
||||
log.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
async noteTokenRemoved(token) {
|
||||
try {
|
||||
await this.onTokenRemoved(token)
|
||||
} catch (e) {
|
||||
log.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import pg from 'pg'
|
||||
import { expect } from 'chai'
|
||||
import configModule from 'config'
|
||||
import SqlTokenPersistence from './sql-token-persistence.js'
|
||||
|
||||
const config = configModule.util.toObject()
|
||||
const postgresUrl = config?.private?.postgres_url
|
||||
const tableName = 'token_persistence_integration_test'
|
||||
|
||||
describe('SQL token persistence', function () {
|
||||
let pool
|
||||
let persistence
|
||||
|
||||
before('Mock db connection and load app', async function () {
|
||||
// Create a new pool with a connection limit of 1
|
||||
pool = new pg.Pool({
|
||||
connectionString: postgresUrl,
|
||||
|
||||
// Reuse the connection to make sure we always hit the same pg_temp schema
|
||||
max: 1,
|
||||
|
||||
// Disable auto-disconnection of idle clients to make sure we always hit the same pg_temp schema
|
||||
idleTimeoutMillis: 0,
|
||||
})
|
||||
persistence = new SqlTokenPersistence({
|
||||
url: postgresUrl,
|
||||
table: tableName,
|
||||
})
|
||||
})
|
||||
after(async function () {
|
||||
if (persistence) {
|
||||
await persistence.stop()
|
||||
persistence = undefined
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach('Create temporary table', async function () {
|
||||
await pool.query(
|
||||
`CREATE TEMPORARY TABLE ${tableName} (LIKE github_user_tokens INCLUDING ALL);`
|
||||
)
|
||||
})
|
||||
afterEach('Drop temporary table', async function () {
|
||||
await pool.query(`DROP TABLE IF EXISTS pg_temp.${tableName};`)
|
||||
})
|
||||
|
||||
context('when the key does not exist', function () {
|
||||
it('does nothing', async function () {
|
||||
const tokens = await persistence.initialize(pool)
|
||||
expect(tokens).to.deep.equal([])
|
||||
})
|
||||
})
|
||||
|
||||
context('when the key exists', function () {
|
||||
const initialTokens = ['a', 'b', 'c'].map(char => char.repeat(40))
|
||||
|
||||
beforeEach(async function () {
|
||||
initialTokens.forEach(async token => {
|
||||
await pool.query(
|
||||
`INSERT INTO pg_temp.${tableName} (token) VALUES ($1::text);`,
|
||||
[token]
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('loads the contents', async function () {
|
||||
const tokens = await persistence.initialize(pool)
|
||||
expect(tokens.sort()).to.deep.equal(initialTokens)
|
||||
})
|
||||
|
||||
context('when tokens are added', function () {
|
||||
it('saves the change', async function () {
|
||||
const newToken = 'e'.repeat(40)
|
||||
const expected = initialTokens.slice()
|
||||
expected.push(newToken)
|
||||
|
||||
await persistence.initialize(pool)
|
||||
await persistence.noteTokenAdded(newToken)
|
||||
|
||||
const result = await pool.query(
|
||||
`SELECT token FROM pg_temp.${tableName};`
|
||||
)
|
||||
const savedTokens = result.rows.map(row => row.token)
|
||||
expect(savedTokens.sort()).to.deep.equal(expected)
|
||||
})
|
||||
})
|
||||
|
||||
context('when tokens are removed', function () {
|
||||
it('saves the change', async function () {
|
||||
const expected = Array.from(initialTokens)
|
||||
const toRemove = expected.pop()
|
||||
|
||||
await persistence.initialize(pool)
|
||||
await persistence.noteTokenRemoved(toRemove)
|
||||
|
||||
const result = await pool.query(
|
||||
`SELECT token FROM pg_temp.${tableName};`
|
||||
)
|
||||
const savedTokens = result.rows.map(row => row.token)
|
||||
expect(savedTokens.sort()).to.deep.equal(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,57 +0,0 @@
|
||||
import pg from 'pg'
|
||||
import log from '../server/log.js'
|
||||
|
||||
export default class SqlTokenPersistence {
|
||||
constructor({ url, table }) {
|
||||
this.url = url
|
||||
this.table = table
|
||||
this.noteTokenAdded = this.noteTokenAdded.bind(this)
|
||||
this.noteTokenRemoved = this.noteTokenRemoved.bind(this)
|
||||
}
|
||||
|
||||
async initialize(pool) {
|
||||
if (pool) {
|
||||
this.pool = pool
|
||||
} else {
|
||||
this.pool = new pg.Pool({ connectionString: this.url })
|
||||
}
|
||||
const result = await this.pool.query(`SELECT token FROM ${this.table};`)
|
||||
return result.rows.map(row => row.token)
|
||||
}
|
||||
|
||||
async stop() {
|
||||
if (this.pool) {
|
||||
await this.pool.end()
|
||||
}
|
||||
}
|
||||
|
||||
async onTokenAdded(token) {
|
||||
return await this.pool.query(
|
||||
`INSERT INTO ${this.table} (token) VALUES ($1::text) ON CONFLICT (token) DO NOTHING;`,
|
||||
[token]
|
||||
)
|
||||
}
|
||||
|
||||
async onTokenRemoved(token) {
|
||||
return await this.pool.query(
|
||||
`DELETE FROM ${this.table} WHERE token=$1::text;`,
|
||||
[token]
|
||||
)
|
||||
}
|
||||
|
||||
async noteTokenAdded(token) {
|
||||
try {
|
||||
await this.onTokenAdded(token)
|
||||
} catch (e) {
|
||||
log.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
async noteTokenRemoved(token) {
|
||||
try {
|
||||
await this.onTokenRemoved(token)
|
||||
} catch (e) {
|
||||
log.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
// DANGER_GITHUB_API_TOKEN=your-github-api-token npm run danger -- pr https://github.com/badges/shields/pull/2665
|
||||
|
||||
const { danger, fail, message, warn } = require('danger')
|
||||
const { default: noTestShortcuts } = require('danger-plugin-no-test-shortcuts')
|
||||
const { fileMatch } = danger.git
|
||||
|
||||
const documentation = fileMatch(
|
||||
@@ -172,3 +173,11 @@ affectedServices.forEach(service => {
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Prevent merging exclusive services tests.
|
||||
noTestShortcuts({
|
||||
testFilePredicate: filePath => filePath.endsWith('.tester.js'),
|
||||
patterns: {
|
||||
only: ['only()'],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -44,7 +44,7 @@ In case you get the _"getaddrinfo ENOTFOUND localhost"_ error, visit [http://127
|
||||
|
||||
## (3) Open an Issue
|
||||
|
||||
Before you want to implement your service, you may want to [open an issue](https://github.com/badges/shields/issues/new?template=3_Badge_request.yml) and describe what you have in mind:
|
||||
Before you want to implement your service, you may want to [open an issue](https://github.com/badges/shields/issues/new?template=3_Badge_request.md) and describe what you have in mind:
|
||||
|
||||
- What is the badge for?
|
||||
- Which API do you want to use?
|
||||
|
||||
@@ -45,14 +45,14 @@ The tests are also divided into several parts:
|
||||
7. [The service tests themselves][service tests] live integration tests of the
|
||||
services, and some mocked tests
|
||||
1. `*.tester.js` in subfolders of [`services`][services]
|
||||
8. Integration tests of PostgreSQL-backed persistence code
|
||||
1. [`core/token-pooling/sql-token-persistence.integration.js`][sql-token-persistence.integration]
|
||||
8. Integration tests of Redis-backed persistence code
|
||||
1. [`core/token-pooling/redis-token-persistence.integration.js`][redis-token-persistence.integration]
|
||||
9. Integration tests of the GitHub authorization code
|
||||
1. [`services/github/github-api-provider.integration.js`][github-api-provider.integration]
|
||||
|
||||
[service-test-runner]: https://github.com/badges/shields/tree/master/core/service-test-runner
|
||||
[service tests]: https://github.com/badges/shields/blob/master/doc/service-tests.md
|
||||
[sql-token-persistence.integration]: https://github.com/badges/shields/blob/master/core/token-pooling/sql-token-persistence.integration.js
|
||||
[redis-token-persistence.integration]: https://github.com/badges/shields/blob/master/core/token-pooling/redis-token-persistence.integration.js
|
||||
[github-api-provider.integration]: https://github.com/badges/shields/blob/master/services/github/github-api-provider.integration.js
|
||||
|
||||
Our goal is to reach 100% coverage of the code in the
|
||||
|
||||
@@ -14,41 +14,49 @@ Production hosting is managed by the Shields ops team:
|
||||
[operations issues]: https://github.com/badges/shields/issues?q=is%3Aissue+is%3Aopen+label%3Aoperations
|
||||
[ops discord]: https://discordapp.com/channels/308323056592486420/480747695879749633
|
||||
|
||||
| Component | Subcomponent | People with access |
|
||||
| ----------------------------- | --------------------------- | --------------------------------------------------------------- |
|
||||
| shields-io-production | Full access | @calebcartwright, @chris48s, @paulmelnikow |
|
||||
| shields-io-production | Access management | @calebcartwright, @chris48s, @paulmelnikow |
|
||||
| Raster server | Full access as team members | @paulmelnikow, @chris48s, @calebcartwright, @platan |
|
||||
| shields-server.com redirector | Full access as team members | @paulmelnikow, @chris48s, @calebcartwright, @platan |
|
||||
| Cloudflare (CDN) | Account owner | @espadrine |
|
||||
| Cloudflare (CDN) | Access management | @espadrine |
|
||||
| Cloudflare (CDN) | Admin access | @calebcartwright, @chris48s, @espadrine, @paulmelnikow, @PyvesB |
|
||||
| Twitch | OAuth app | @PyvesB |
|
||||
| Discord | OAuth app | @PyvesB |
|
||||
| YouTube | Account owner | @PyvesB |
|
||||
| GitLab | Account owner | @calebcartwright |
|
||||
| GitLab | Account access | @calebcartwright, @chris48s, @paulmelnikow, @PyvesB |
|
||||
| OpenStreetMap (for Wheelmap) | Account owner | @paulmelnikow |
|
||||
| DNS | Account owner | @olivierlacan |
|
||||
| DNS | Read-only account access | @espadrine, @paulmelnikow, @chris48s |
|
||||
| Sentry | Error reports | @espadrine, @paulmelnikow |
|
||||
| Metrics server | Owner | @platan |
|
||||
| UptimeRobot | Account owner | @paulmelnikow |
|
||||
| More metrics | Owner | @RedSparr0w |
|
||||
| Component | Subcomponent | People with access |
|
||||
| ----------------------------- | ------------------------------- | --------------------------------------------------------------- |
|
||||
| shields-io-production | Full access | @calebcartwright, @chris48s, @paulmelnikow |
|
||||
| shields-io-production | Access management | @calebcartwright, @chris48s, @paulmelnikow |
|
||||
| Compose.io Redis | Account owner | @paulmelnikow |
|
||||
| Compose.io Redis | Account access | @paulmelnikow |
|
||||
| Compose.io Redis | Database connection credentials | @calebcartwright, @chris48s, @paulmelnikow, @pyvesb |
|
||||
| Raster server | Full access as team members | @paulmelnikow, @chris48s, @calebcartwright, @platan |
|
||||
| shields-server.com redirector | Full access as team members | @paulmelnikow, @chris48s, @calebcartwright, @platan |
|
||||
| Cloudflare (CDN) | Account owner | @espadrine |
|
||||
| Cloudflare (CDN) | Access management | @espadrine |
|
||||
| Cloudflare (CDN) | Admin access | @calebcartwright, @chris48s, @espadrine, @paulmelnikow, @PyvesB |
|
||||
| Twitch | OAuth app | @PyvesB |
|
||||
| Discord | OAuth app | @PyvesB |
|
||||
| YouTube | Account owner | @PyvesB |
|
||||
| GitLab | Account owner | @calebcartwright |
|
||||
| GitLab | Account access | @calebcartwright, @chris48s, @paulmelnikow, @PyvesB |
|
||||
| OpenStreetMap (for Wheelmap) | Account owner | @paulmelnikow |
|
||||
| DNS | Account owner | @olivierlacan |
|
||||
| DNS | Read-only account access | @espadrine, @paulmelnikow, @chris48s |
|
||||
| Sentry | Error reports | @espadrine, @paulmelnikow |
|
||||
| Metrics server | Owner | @platan |
|
||||
| UptimeRobot | Account owner | @paulmelnikow |
|
||||
| More metrics | Owner | @RedSparr0w |
|
||||
|
||||
## Attached state
|
||||
|
||||
Shields has mercifully little persistent state:
|
||||
|
||||
1. The GitHub tokens we collect are stored in a fly.io postgres database
|
||||
1. The GitHub tokens we collect are saved on each server in a cloud Redis
|
||||
database. They can also be fetched from the [GitHub auth admin endpoint][]
|
||||
for debugging.
|
||||
2. The server keeps the [resource cache][] in memory. It is neither
|
||||
persisted nor inspectable.
|
||||
|
||||
[github auth admin endpoint]: https://github.com/badges/shields/blob/master/services/github/auth/admin.js
|
||||
[resource cache]: https://github.com/badges/shields/blob/master/core/base-service/resource-cache.js
|
||||
|
||||
## Configuration
|
||||
|
||||
To bootstrap the configuration of non-secret settings, we set a single environment variable:
|
||||
To bootstrap the configuration process,
|
||||
[the script that starts the server][start-shields.sh] sets a single
|
||||
environment variable:
|
||||
|
||||
```
|
||||
NODE_CONFIG_ENV=shields-io-production
|
||||
@@ -63,8 +71,7 @@ files:
|
||||
contains non-secrets which are checked in to the main repo.
|
||||
- [`default.yml`][default.yml]. This file contains defaults.
|
||||
|
||||
Secrets are supplied directly as environment vars.
|
||||
|
||||
[start-shields.sh]: https://github.com/badges/ServerScript/blob/master/start-shields.sh#L7
|
||||
[config]: https://github.com/lorenwest/node-config/wiki/Configuration-Files
|
||||
[local-shields-io-production.yml]: ../config/local-shields-io-production.template.yml
|
||||
[shields-io-production.yml]: ../config/shields-io-production.yml
|
||||
|
||||
@@ -45,11 +45,11 @@ We are happy to document and collate any self-hosting patterns/approaches that o
|
||||
We try to make it as easy as possible for users to self-host a Shields server so we publish a few releases of the server. Please be sure to refer to the [self hosting guide][self hosting] for a detailed walk through on how to spin up a server.
|
||||
|
||||
- The server uses [Calendar Versioning](https://calver.org/). Tags of the form `server-YYYY-MM-DD` are server releases (these are the tags that are relevant to self-hosting users, e.g. [server-2021-02-01](https://github.com/badges/shields/releases/tag/server-2021-02-01)).
|
||||
- As well as [tags on GitHub](https://github.com/badges/shields/tags), server releases are also pushed to [DockerHub](https://registry.hub.docker.com/r/shieldsio/shields/tags) and [GitHub Container Registry](https://github.com/badges/shields/pkgs/container/shields/versions?filters%5Bversion_type%5D=tagged). See the self-hosting section on [Docker](https://github.com/badges/shields/blob/master/doc/self-hosting.md#Docker) for more details.
|
||||
- As well as [tags on GitHub](https://github.com/badges/shields/tags), server releases are also pushed to [DockerHub](https://registry.hub.docker.com/r/shieldsio/shields/tags). See the self-hosting section on [Docker](https://github.com/badges/shields/blob/master/doc/self-hosting.md#Docker) for more details.
|
||||
- We publish release notes for server releases in the [CHANGELOG](https://github.com/badges/shields/blob/master/CHANGELOG.md). There may occasionally be non-backwards compatible changes to be aware of.
|
||||
- We will normally put out one release per month. If there is a security patch or major bugfix affecting self-hosting users, we may put out an out-of-sequence release.
|
||||
- Releases are just a snapshot in time. We advise always tracking the latest release to ensure you are up-to-date with the latest bug fixes and security updates. There are no 'patch' releases - we don't backport fixes to old releases. Tagged versions just provide a convenient way to apply upgrades in a controlled way or roll back to an older version if necessary and communicate about versions.
|
||||
- You can stay on the bleeding edge by tracking the `master` branch for source installs or the `next` tag on DockerHub/GHCR.
|
||||
- You can stay on the bleeding edge by tracking the `master` branch for source installs or the `next` tag on DockerHub.
|
||||
|
||||
[shields.io]: https://shields.io
|
||||
[npm package]: https://www.npmjs.com/package/badge-maker
|
||||
|
||||
@@ -71,30 +71,18 @@ vercel
|
||||
|
||||
## Docker
|
||||
|
||||
### Public Images
|
||||
### DockerHub
|
||||
|
||||
We publish images to:
|
||||
We publish images to DockerHub at https://registry.hub.docker.com/r/shieldsio/shields
|
||||
|
||||
- DockerHub at https://registry.hub.docker.com/r/shieldsio/shields and
|
||||
- GitHub Container Registry at https://github.com/badges/shields/pkgs/container/shields
|
||||
The `next` tag is the latest build from `master`, or tagged releases are available
|
||||
https://registry.hub.docker.com/r/shieldsio/shields/tags
|
||||
|
||||
The `next` tag is the latest build from `master`, or tagged snapshot releases are available:
|
||||
|
||||
- https://registry.hub.docker.com/r/shieldsio/shields/tags
|
||||
- https://github.com/badges/shields/pkgs/container/shields/versions?filters%5Bversion_type%5D=tagged
|
||||
|
||||
```sh
|
||||
# DockerHub
|
||||
```console
|
||||
$ docker pull shieldsio/shields:next
|
||||
$ docker run shieldsio/shields:next
|
||||
```
|
||||
|
||||
```sh
|
||||
# GHCR
|
||||
$ docker pull ghcr.io/badges/shields:next
|
||||
$ docker pull ghcr.io/badges/shields:next
|
||||
```
|
||||
|
||||
### Building Docker Image Locally
|
||||
|
||||
Alternatively, you can build and run the server locally using Docker. First build an image:
|
||||
|
||||
@@ -125,17 +125,11 @@ Because of GitHub rate limits, you will need to provide a token, or else badges
|
||||
will stop working once you hit 60 requests per hour, the
|
||||
[unauthenticated rate limit][github rate limit].
|
||||
|
||||
You can [create a personal access token][personal access tokens] (PATs) through the
|
||||
You can [create a personal access token][personal access tokens] through the
|
||||
GitHub website. When you create the token, you can choose to give read access
|
||||
to your repositories. If you do that, your self-hosted Shields installation
|
||||
will have access to your private repositories.
|
||||
|
||||
For most users we recommend using a classic PAT as opposed to a [fine-grained PAT][fine-grained pat].
|
||||
It is possible to request a fairly large subset of the GitHub badge suite using a
|
||||
fine-grained PAT for authentication but there are also some badges that won't work.
|
||||
This is because some of our badges make use of GitHub's v4 GraphQL API and the
|
||||
GraphQL API only supports authentication with a classic PAT.
|
||||
|
||||
When a `gh_token` is specified, it is used in place of the Shields token
|
||||
rotation logic.
|
||||
|
||||
@@ -145,7 +139,6 @@ token, though it's not required.
|
||||
|
||||
[github rate limit]: https://developer.github.com/v3/#rate-limiting
|
||||
[personal access tokens]: https://github.com/settings/tokens
|
||||
[fine-grained pat]: https://github.blog/2022-10-18-introducing-fine-grained-personal-access-tokens-for-github/
|
||||
|
||||
- `GH_CLIENT_ID` (yml: `private.gh_client_id`)
|
||||
- `GH_CLIENT_SECRET` (yml: `private.gh_client_secret`)
|
||||
|
||||
47
fly.toml
47
fly.toml
@@ -1,47 +0,0 @@
|
||||
app = "shields-io-review-apps"
|
||||
|
||||
kill_signal = "SIGINT"
|
||||
kill_timeout = 5
|
||||
processes = []
|
||||
|
||||
[env]
|
||||
HTTPS="false"
|
||||
GITLAB_ORIGINS = "https://gitlab.com"
|
||||
METRICS_PROMETHEUS_ENABLED = "false"
|
||||
REQUEST_TIMEOUT_SECONDS = "20"
|
||||
REQUIRE_CLOUDFLARE = "false"
|
||||
USER_AGENT_BASE = "Shields-Review-App"
|
||||
|
||||
[deploy]
|
||||
strategy = "immediate"
|
||||
|
||||
[experimental]
|
||||
allowed_public_ports = []
|
||||
auto_rollback = true
|
||||
|
||||
[[services]]
|
||||
http_checks = []
|
||||
internal_port = 80
|
||||
processes = ["app"]
|
||||
protocol = "tcp"
|
||||
script_checks = []
|
||||
|
||||
[services.concurrency]
|
||||
hard_limit = 25
|
||||
soft_limit = 20
|
||||
type = "connections"
|
||||
|
||||
[[services.ports]]
|
||||
force_https = true
|
||||
handlers = ["http"]
|
||||
port = 80
|
||||
|
||||
[[services.ports]]
|
||||
handlers = ["tls", "http"]
|
||||
port = 443
|
||||
|
||||
[[services.tcp_checks]]
|
||||
grace_period = "1s"
|
||||
interval = "15s"
|
||||
restart_limit = 0
|
||||
timeout = "2s"
|
||||
@@ -66,7 +66,7 @@ export default function Footer({ baseUrl }: { baseUrl: string }): JSX.Element {
|
||||
<p>
|
||||
Have an idea for an awesome new badge?
|
||||
<br />
|
||||
<a href="https://github.com/badges/shields/issues/new?labels=service-badge&template=3_Badge_request.yml">
|
||||
<a href="https://github.com/badges/shields/issues/new?labels=service-badge&template=3_Badge_request.md">
|
||||
Tell us about it
|
||||
</a>{' '}
|
||||
and we might bring it to you!
|
||||
|
||||
@@ -27,6 +27,7 @@ export default function Search({
|
||||
<form action="javascript:void 0" autoComplete="off">
|
||||
<BlockInput
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
onChange={onQueryChanged}
|
||||
placeholder="search"
|
||||
/>
|
||||
|
||||
@@ -327,18 +327,17 @@ export default function Usage({ baseUrl }: { baseUrl: string }): JSX.Element {
|
||||
<QueryParam
|
||||
documentation={
|
||||
<span>
|
||||
Insert one of the named logos from (<NamedLogos />) or
|
||||
simple-icons. All simple-icons are referenced using icon slugs.
|
||||
You can click the icon title on{' '}
|
||||
Insert one of the named logos from (<NamedLogos />) or{' '}
|
||||
<a
|
||||
href="https://simpleicons.org/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
simple-icons
|
||||
</a>{' '}
|
||||
to copy the slug or they can be found in the{' '}
|
||||
<a href="https://github.com/simple-icons/simple-icons/blob/master/slugs.md">
|
||||
</a>
|
||||
. Simple-icons are referenced using icon slugs which can be
|
||||
found on the simple-icons site or in the{' '}
|
||||
<a href="https://github.com/simple-icons/simple-icons/blob/develop/slugs.md">
|
||||
slugs.md file
|
||||
</a>{' '}
|
||||
in the simple-icons repository.
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
exports.shorthands = undefined
|
||||
|
||||
exports.up = pgm => {
|
||||
pgm.createTable('github_user_tokens', {
|
||||
id: 'id',
|
||||
token: { type: 'varchar(1000)', notNull: true, unique: true },
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = pgm => {
|
||||
pgm.dropTable('github_user_tokens')
|
||||
}
|
||||
5864
package-lock.json
generated
5864
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
107
package.json
107
package.json
@@ -21,32 +21,32 @@
|
||||
"url": "https://github.com/badges/shields"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/lato": "^5.0.1",
|
||||
"@fontsource/lekton": "^5.0.1",
|
||||
"@fontsource/lato": "^4.5.10",
|
||||
"@fontsource/lekton": "^4.5.11",
|
||||
"@renovate/pep440": "^1.0.0",
|
||||
"@renovatebot/ruby-semver": "^2.1.11",
|
||||
"@sentry/node": "^7.53.1",
|
||||
"@shields_io/camp": "^18.1.2",
|
||||
"@xmldom/xmldom": "0.8.7",
|
||||
"@renovatebot/ruby-semver": "^1.1.7",
|
||||
"@sentry/node": "^7.29.0",
|
||||
"@shields_io/camp": "^18.1.1",
|
||||
"badge-maker": "file:badge-maker",
|
||||
"bytes": "^3.1.2",
|
||||
"camelcase": "^7.0.1",
|
||||
"chalk": "^5.2.0",
|
||||
"check-node-version": "^4.2.1",
|
||||
"cloudflare-middleware": "^1.0.4",
|
||||
"config": "^3.3.9",
|
||||
"config": "^3.3.8",
|
||||
"cross-env": "^7.0.3",
|
||||
"dayjs": "^1.11.7",
|
||||
"decamelize": "^3.2.0",
|
||||
"emojic": "^1.1.17",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"fast-xml-parser": "^4.2.2",
|
||||
"glob": "^10.2.6",
|
||||
"fast-xml-parser": "^4.0.12",
|
||||
"glob": "^8.0.3",
|
||||
"global-agent": "^3.0.0",
|
||||
"got": "^12.6.0",
|
||||
"got": "^12.5.3",
|
||||
"graphql": "^15.6.1",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"joi": "17.9.2",
|
||||
"ioredis": "5.2.4",
|
||||
"joi": "17.7.0",
|
||||
"joi-extension-semver": "5.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonpath": "~1.1.1",
|
||||
@@ -54,18 +54,17 @@
|
||||
"lodash.groupby": "^4.6.0",
|
||||
"lodash.times": "^4.3.2",
|
||||
"node-env-flag": "^0.1.0",
|
||||
"node-pg-migrate": "^6.2.2",
|
||||
"parse-link-header": "^2.0.0",
|
||||
"path-to-regexp": "^6.2.1",
|
||||
"pg": "^8.11.0",
|
||||
"pretty-bytes": "^6.1.0",
|
||||
"pretty-bytes": "^6.0.0",
|
||||
"priorityqueuejs": "^2.0.0",
|
||||
"prom-client": "^14.2.0",
|
||||
"qs": "^6.11.2",
|
||||
"prom-client": "^14.1.1",
|
||||
"qs": "^6.11.0",
|
||||
"query-string": "^8.1.0",
|
||||
"semver": "~7.5.0",
|
||||
"simple-icons": "9.0.0",
|
||||
"semver": "~7.3.8",
|
||||
"simple-icons": "8.2.0",
|
||||
"webextension-store-meta": "^1.0.5",
|
||||
"xmldom": "~0.6.0",
|
||||
"xpath": "~0.0.32"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -118,8 +117,7 @@
|
||||
"e2e": "start-server-and-test start http://localhost:3000 test:e2e",
|
||||
"e2e-on-build": "cross-env CYPRESS_baseUrl=http://localhost:8080 start-server-and-test start:server:e2e-on-build http://localhost:8080 test:e2e",
|
||||
"badge": "cross-env NODE_CONFIG_ENV=test TRACE_SERVICES=true node scripts/badge-cli.js",
|
||||
"build-docs": "rimraf api-docs/ && jsdoc --pedantic -c ./jsdoc.json . && echo 'contributing.shields.io' > api-docs/CNAME",
|
||||
"migrate": "node scripts/write-migrations-config.js > migrations-config.json && node-pg-migrate --config-file=migrations-config.json"
|
||||
"build-docs": "rimraf api-docs/ && jsdoc --pedantic -c ./jsdoc.json . && echo 'contributing.shields.io' > api-docs/CNAME"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*.@(js|ts|tsx)": [
|
||||
@@ -144,25 +142,25 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.8",
|
||||
"@babel/core": "^7.20.12",
|
||||
"@babel/polyfill": "^7.12.1",
|
||||
"@babel/register": "7.21.0",
|
||||
"@babel/register": "7.18.9",
|
||||
"@istanbuljs/schema": "^0.1.3",
|
||||
"@mapbox/react-click-to-select": "^2.2.1",
|
||||
"@types/chai": "^4.3.5",
|
||||
"@types/chai": "^4.3.4",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/lodash.groupby": "^4.6.7",
|
||||
"@types/mocha": "^10.0.1",
|
||||
"@types/node": "^16.7.10",
|
||||
"@types/react-helmet": "^6.1.6",
|
||||
"@types/react-modal": "^3.16.0",
|
||||
"@types/react-modal": "^3.13.1",
|
||||
"@types/react-select": "^4.0.17",
|
||||
"@types/styled-components": "5.1.26",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.2",
|
||||
"@typescript-eslint/parser": "^5.58.0",
|
||||
"babel-plugin-inline-react-svg": "^2.0.2",
|
||||
"@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.13.0",
|
||||
"c8": "^7.12.0",
|
||||
"caller": "^1.1.0",
|
||||
"chai": "^4.3.7",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
@@ -170,28 +168,28 @@
|
||||
"chai-string": "^1.4.0",
|
||||
"child-process-promise": "^2.2.1",
|
||||
"clipboard-copy": "^4.0.1",
|
||||
"concurrently": "^8.0.1",
|
||||
"cypress": "^12.13.0",
|
||||
"concurrently": "^7.6.0",
|
||||
"cypress": "^12.3.0",
|
||||
"cypress-wait-for-stable-dom": "^0.1.0",
|
||||
"danger": "^11.2.6",
|
||||
"deepmerge": "^4.3.1",
|
||||
"danger": "^11.2.1",
|
||||
"danger-plugin-no-test-shortcuts": "^2.0.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-config-standard": "^16.0.3",
|
||||
"eslint-config-standard-jsx": "^10.0.0",
|
||||
"eslint-config-standard-react": "^11.0.1",
|
||||
"eslint-plugin-chai-friendly": "^0.7.2",
|
||||
"eslint-plugin-cypress": "^2.13.3",
|
||||
"eslint-plugin-icedfrisby": "^0.1.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jsdoc": "^44.2.7",
|
||||
"eslint-plugin-cypress": "^2.12.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsdoc": "^39.6.4",
|
||||
"eslint-plugin-mocha": "^10.1.0",
|
||||
"eslint-plugin-no-extension-in-require": "^0.2.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^5.2.0",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react": "^7.31.11",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-sort-class-members": "^1.18.0",
|
||||
"eslint-plugin-sort-class-members": "^1.16.0",
|
||||
"fetch-ponyfill": "^7.1.0",
|
||||
"form-data": "^4.0.0",
|
||||
"gatsby": "4.23.1",
|
||||
@@ -204,24 +202,24 @@
|
||||
"humanize-string": "^2.1.0",
|
||||
"icedfrisby": "4.0.0",
|
||||
"icedfrisby-nock": "^2.1.0",
|
||||
"is-svg": "^5.0.0",
|
||||
"is-svg": "^4.3.2",
|
||||
"js-yaml-loader": "^1.2.2",
|
||||
"jsdoc": "^4.0.2",
|
||||
"lint-staged": "^13.2.2",
|
||||
"jsdoc": "^4.0.0",
|
||||
"lint-staged": "^13.1.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.difference": "^4.5.0",
|
||||
"minimist": "^1.2.8",
|
||||
"minimist": "^1.2.7",
|
||||
"mocha": "^10.2.0",
|
||||
"mocha-env-reporter": "^4.0.0",
|
||||
"mocha-junit-reporter": "^2.2.0",
|
||||
"mocha-yaml-loader": "^1.0.3",
|
||||
"nock": "13.3.1",
|
||||
"node-mocks-http": "^1.12.2",
|
||||
"nodemon": "^2.0.22",
|
||||
"nock": "13.2.9",
|
||||
"node-mocks-http": "^1.12.1",
|
||||
"nodemon": "^2.0.20",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"open-cli": "^7.2.0",
|
||||
"open-cli": "^7.1.0",
|
||||
"portfinder": "^1.0.32",
|
||||
"prettier": "2.8.8",
|
||||
"prettier": "2.8.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-error-overlay": "^6.0.11",
|
||||
@@ -230,17 +228,18 @@
|
||||
"react-pose": "^4.0.10",
|
||||
"react-select": "^4.3.1",
|
||||
"read-all-stdin-sync": "^1.0.5",
|
||||
"rimraf": "^5.0.1",
|
||||
"redis-server": "^1.2.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"sazerac": "^2.0.0",
|
||||
"simple-git-hooks": "^2.8.1",
|
||||
"sinon": "^15.1.0",
|
||||
"sinon": "^15.0.1",
|
||||
"sinon-chai": "^3.7.0",
|
||||
"snap-shot-it": "^7.9.10",
|
||||
"start-server-and-test": "2.0.0",
|
||||
"styled-components": "^5.3.11",
|
||||
"start-server-and-test": "1.15.2",
|
||||
"styled-components": "^5.3.6",
|
||||
"ts-mocha": "^10.0.0",
|
||||
"tsd": "^0.28.1",
|
||||
"typescript": "^5.0.4",
|
||||
"tsd": "^0.25.0",
|
||||
"typescript": "^4.9.4",
|
||||
"url": "^0.11.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import yaml from 'js-yaml'
|
||||
import { collectDefinitions } from '../core/base-service/loader.js'
|
||||
import { category2openapi } from '../core/base-service/openapi.js'
|
||||
|
||||
const specsPath = path.join('frontend', 'categories')
|
||||
|
||||
function writeSpec(filename, spec) {
|
||||
// Omit undefined
|
||||
// https://github.com/nodeca/js-yaml/issues/356#issuecomment-312430599
|
||||
const cleaned = JSON.parse(JSON.stringify(spec))
|
||||
|
||||
fs.writeFileSync(
|
||||
filename,
|
||||
yaml.dump(cleaned, { flowLevel: 5, forceQuotes: true })
|
||||
)
|
||||
}
|
||||
|
||||
;(async () => {
|
||||
const definitions = await collectDefinitions()
|
||||
|
||||
for (const category of definitions.categories) {
|
||||
const services = definitions.services.filter(
|
||||
service => service.category === category.id && !service.isDeprecated
|
||||
)
|
||||
|
||||
writeSpec(
|
||||
path.join(specsPath, `${category.id}.yaml`),
|
||||
category2openapi(category, services)
|
||||
)
|
||||
}
|
||||
|
||||
let coreServices = []
|
||||
coreServices = coreServices.concat(
|
||||
definitions.services.filter(
|
||||
service => service.category === 'static' && !service.isDeprecated
|
||||
)
|
||||
)
|
||||
coreServices = coreServices.concat(
|
||||
definitions.services.filter(
|
||||
service => service.category === 'dynamic' && !service.isDeprecated
|
||||
)
|
||||
)
|
||||
writeSpec(
|
||||
path.join(specsPath, '1core.yaml'),
|
||||
category2openapi({ name: 'Core' }, coreServices)
|
||||
)
|
||||
})()
|
||||
@@ -3,19 +3,6 @@ import { collectDefinitions } from '../core/base-service/loader.js'
|
||||
;(async () => {
|
||||
const definitions = await collectDefinitions()
|
||||
|
||||
// filter out static, dynamic and debug badge examples
|
||||
const publicCategories = definitions.categories.map(c => c.id)
|
||||
definitions.services = definitions.services.filter(s =>
|
||||
publicCategories.includes(s.category)
|
||||
)
|
||||
|
||||
// drop the openApi property for the "legacy" frontend
|
||||
for (const service of definitions.services) {
|
||||
if (service.openApi) {
|
||||
service.openApi = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// Omit undefined
|
||||
// https://github.com/nodeca/js-yaml/issues/356#issuecomment-312430599
|
||||
const cleaned = JSON.parse(JSON.stringify(definitions))
|
||||
|
||||
@@ -20,17 +20,17 @@ if (data.stats.passes > 0) {
|
||||
process.stdout.write(`✔ ${data.stats.passes} passed\n`)
|
||||
}
|
||||
if (data.stats.failures > 0) {
|
||||
process.stdout.write(`✖ ${data.stats.failures} failed\n`)
|
||||
process.stdout.write(`✖ ${data.stats.failures} failed\n\n`)
|
||||
}
|
||||
if (data.stats.pending > 0) {
|
||||
process.stdout.write(`● ${data.stats.pending} pending\n`)
|
||||
process.stdout.write(`● ${data.stats.pending} pending\n\n`)
|
||||
process.exit(2)
|
||||
}
|
||||
process.stdout.write('\n')
|
||||
|
||||
if (data.stats.failures > 0) {
|
||||
process.stdout.write('## Failures\n\n')
|
||||
for (const test of data.failures) {
|
||||
for (const test of data.tests) {
|
||||
if (test.err && Object.keys(test.err).length > 0) {
|
||||
process.stdout.write(`### ${test.title}\n\n`)
|
||||
process.stdout.write(`${test.fullTitle}\n\n`)
|
||||
process.stdout.write('```\n')
|
||||
process.stdout.write(`${test.err.stack}\n`)
|
||||
@@ -38,10 +38,3 @@ if (data.stats.failures > 0) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.stats.pending > 0) {
|
||||
process.stdout.write('## Pending\n\n')
|
||||
for (const test of data.pending) {
|
||||
process.stdout.write(`${test.fullTitle}\n\n`)
|
||||
}
|
||||
}
|
||||
|
||||
24
scripts/redis-connectivity-test.js
Normal file
24
scripts/redis-connectivity-test.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import config from 'config'
|
||||
import GithubConstellation from '../services/github/github-constellation.js'
|
||||
const objectConfig = config.util.toObject()
|
||||
console.log(objectConfig)
|
||||
|
||||
const { persistence } = new GithubConstellation({
|
||||
service: objectConfig.public.services.github,
|
||||
private: objectConfig.private,
|
||||
})
|
||||
|
||||
async function main() {
|
||||
const tokens = await persistence.initialize()
|
||||
console.log(`${tokens.length} tokens loaded`)
|
||||
await persistence.stop()
|
||||
}
|
||||
|
||||
;(async () => {
|
||||
try {
|
||||
await main()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
process.exit(1)
|
||||
}
|
||||
})()
|
||||
@@ -1,11 +0,0 @@
|
||||
import configModule from 'config'
|
||||
const config = configModule.util.toObject()
|
||||
|
||||
const postgresUrl = config?.private?.postgres_url
|
||||
|
||||
if (!postgresUrl) {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
process.stdout.write(JSON.stringify({ url: postgresUrl }))
|
||||
process.exit(0)
|
||||
@@ -42,13 +42,6 @@ if (fs.existsSync('.env')) {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (config.private.redis_url != null) {
|
||||
console.error(
|
||||
'RedisTokenPersistence has been removed. Migrate to SqlTokenPersistence'
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const legacySecretsPath = path.join(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'private',
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import {
|
||||
loadServiceClasses,
|
||||
collectDefinitions,
|
||||
} from '../core/base-service/loader.js'
|
||||
import { checkNames, collectDefinitions } from '../core/base-service/loader.js'
|
||||
|
||||
// When these tests fail, they will throw AssertionErrors. Wrapping them in an
|
||||
// `expect().not.to.throw()` makes the error output unreadable.
|
||||
|
||||
it('Services have unique names', async function () {
|
||||
this.timeout(30000)
|
||||
await loadServiceClasses()
|
||||
await checkNames()
|
||||
})
|
||||
|
||||
it('Can collect the service definitions', async function () {
|
||||
|
||||
@@ -44,7 +44,7 @@ class BaseCratesService extends BaseJsonService {
|
||||
async fetch({ crate, version }) {
|
||||
const url = version
|
||||
? `https://crates.io/api/v1/crates/${crate}/${version}`
|
||||
: `https://crates.io/api/v1/crates/${crate}?include=versions,downloads`
|
||||
: `https://crates.io/api/v1/crates/${crate}`
|
||||
return this._requestJson({ schema, url })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ t.create('recent downloads (null)')
|
||||
.get('/dr/libc.json')
|
||||
.intercept(nock =>
|
||||
nock('https://crates.io')
|
||||
.get('/api/v1/crates/libc?include=versions,downloads')
|
||||
.get('/api/v1/crates/libc')
|
||||
.reply(200, {
|
||||
crate: {
|
||||
downloads: 42,
|
||||
|
||||
@@ -19,14 +19,12 @@ export default class CratesVersion extends BaseCratesService {
|
||||
if (json.errors) {
|
||||
throw new InvalidResponse({ prettyMessage: json.errors[0].detail })
|
||||
}
|
||||
return json.crate.max_stable_version
|
||||
? json.crate.max_stable_version
|
||||
: json.crate.max_version
|
||||
return { version: json.version ? json.version.num : json.crate.max_version }
|
||||
}
|
||||
|
||||
async handle({ crate }) {
|
||||
const json = await this.fetch({ crate })
|
||||
const version = this.transform(json)
|
||||
const { version } = this.transform(json)
|
||||
return renderVersionBadge({ version })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,8 @@ import CratesVersion from './crates-version.service.js'
|
||||
|
||||
describe('CratesVersion', function () {
|
||||
test(CratesVersion.prototype.transform, () => {
|
||||
given({ crate: { max_version: '1.1.0' } }).expect('1.1.0')
|
||||
given({
|
||||
crate: { max_stable_version: '1.1.0', max_version: '1.9.0-alpha' },
|
||||
}).expect('1.1.0')
|
||||
given({ version: { num: '1.0.0' } }).expect({ version: '1.0.0' })
|
||||
given({ crate: { max_version: '1.1.0' } }).expect({ version: '1.1.0' })
|
||||
})
|
||||
|
||||
it('throws InvalidResponse on error response', function () {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import Joi from 'joi'
|
||||
import { renderLicenseBadge } from '../licenses.js'
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
import { BaseJsonService, InvalidResponse } from '../index.js'
|
||||
import { BaseJsonService } from '../index.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
license: Joi.array().items(Joi.string()).single(),
|
||||
version: Joi.object({
|
||||
number: Joi.string().allow('').required(),
|
||||
date: Joi.string().allow('').required(),
|
||||
number: Joi.string().required(),
|
||||
}).required(),
|
||||
}).required()
|
||||
|
||||
@@ -15,7 +14,7 @@ class BaseCtanService extends BaseJsonService {
|
||||
static defaultBadgeData = { label: 'ctan' }
|
||||
|
||||
async fetch({ library }) {
|
||||
const url = `https://www.ctan.org/json/2.0/pkg/${library}`
|
||||
const url = `http://www.ctan.org/json/pkg/${library}`
|
||||
return this._requestJson({
|
||||
schema,
|
||||
url,
|
||||
@@ -68,22 +67,7 @@ class CtanVersion extends BaseCtanService {
|
||||
|
||||
async handle({ library }) {
|
||||
const json = await this.fetch({ library })
|
||||
const version = json.version.number
|
||||
if (version !== '') {
|
||||
return renderVersionBadge({ version })
|
||||
} else {
|
||||
const date = json.version.date
|
||||
if (date !== '') {
|
||||
return renderVersionBadge({
|
||||
version: date,
|
||||
versionFormatter: color => 'blue',
|
||||
})
|
||||
} else {
|
||||
return new InvalidResponse({
|
||||
underlyingError: new Error('Both number and date are empty'),
|
||||
})
|
||||
}
|
||||
}
|
||||
return renderVersionBadge({ version: json.version.number })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
import Joi from 'joi'
|
||||
import { ServiceTester } from '../tester.js'
|
||||
import { withRegex } from '../test-validators.js'
|
||||
|
||||
// same as isVPlusDottedVersionAtLeastOne, but also accepts an optional
|
||||
// single lowercase alphabet letter suffix
|
||||
// e.g.: v1.81a
|
||||
const isVPlusDottedVersionAtLeastOneWithOptionalAlphabetLetter = withRegex(
|
||||
/^v\d+(\.\d+)?(\.\d+)?[a-z]?$/
|
||||
)
|
||||
import { isVPlusDottedVersionAtLeastOne } from '../test-validators.js'
|
||||
|
||||
export const t = new ServiceTester({
|
||||
id: 'ctan',
|
||||
@@ -22,12 +14,11 @@ t.create('license').get('/l/novel.json').expectBadge({
|
||||
t.create('license missing')
|
||||
.get('/l/novel.json')
|
||||
.intercept(nock =>
|
||||
nock('https://www.ctan.org')
|
||||
.get('/json/2.0/pkg/novel')
|
||||
nock('http://www.ctan.org')
|
||||
.get('/json/pkg/novel')
|
||||
.reply(200, {
|
||||
version: {
|
||||
number: 'notRelevant',
|
||||
date: 'notRelevant',
|
||||
},
|
||||
})
|
||||
)
|
||||
@@ -39,13 +30,12 @@ t.create('license missing')
|
||||
t.create('single license')
|
||||
.get('/l/tex.json')
|
||||
.intercept(nock =>
|
||||
nock('https://www.ctan.org')
|
||||
.get('/json/2.0/pkg/tex')
|
||||
nock('http://www.ctan.org')
|
||||
.get('/json/pkg/tex')
|
||||
.reply(200, {
|
||||
license: 'knuth',
|
||||
version: {
|
||||
number: 'notRelevant',
|
||||
date: 'notRelevant',
|
||||
},
|
||||
})
|
||||
)
|
||||
@@ -56,18 +46,17 @@ t.create('single license')
|
||||
|
||||
t.create('version').get('/v/novel.json').expectBadge({
|
||||
label: 'ctan',
|
||||
message: isVPlusDottedVersionAtLeastOneWithOptionalAlphabetLetter,
|
||||
message: isVPlusDottedVersionAtLeastOne,
|
||||
})
|
||||
|
||||
t.create('version')
|
||||
.get('/v/novel.json')
|
||||
.intercept(nock =>
|
||||
nock('https://www.ctan.org')
|
||||
.get('/json/2.0/pkg/novel')
|
||||
nock('http://www.ctan.org')
|
||||
.get('/json/pkg/novel')
|
||||
.reply(200, {
|
||||
version: {
|
||||
number: 'v1.11',
|
||||
date: '',
|
||||
},
|
||||
})
|
||||
)
|
||||
@@ -76,9 +65,3 @@ t.create('version')
|
||||
message: 'v1.11',
|
||||
color: 'blue',
|
||||
})
|
||||
|
||||
t.create('date as version').get('/v/l3kernel.json').expectBadge({
|
||||
label: 'ctan',
|
||||
message: Joi.date().iso(),
|
||||
color: 'blue',
|
||||
})
|
||||
|
||||
@@ -6,53 +6,6 @@ import jsonPath from './json-path.js'
|
||||
export default class DynamicJson extends jsonPath(BaseJsonService) {
|
||||
static enabledMetrics = [MetricNames.SERVICE_RESPONSE_SIZE]
|
||||
static route = createRoute('json')
|
||||
static openApi = {
|
||||
'/badge/dynamic/json': {
|
||||
get: {
|
||||
summary: 'Dynamic JSON Badge',
|
||||
description: `<p>
|
||||
The Dynamic JSON Badge allows you to extract an arbitrary value from any
|
||||
JSON Document using a JSONPath selector and show it on a badge.
|
||||
</p>`,
|
||||
parameters: [
|
||||
{
|
||||
name: 'url',
|
||||
description: 'The URL to a JSON document',
|
||||
in: 'query',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example:
|
||||
'https://github.com/badges/shields/raw/master/package.json',
|
||||
},
|
||||
{
|
||||
name: 'query',
|
||||
description:
|
||||
'A <a href="https://jsonpath.com/">JSONPath</a> expression that will be used to query the document',
|
||||
in: 'query',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: '$.name',
|
||||
},
|
||||
{
|
||||
name: 'prefix',
|
||||
description: 'Optional prefix to append to the value',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
example: '[',
|
||||
},
|
||||
{
|
||||
name: 'suffix',
|
||||
description: 'Optional suffix to append to the value',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
example: ']',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
async fetch({ schema, url, errorMessages }) {
|
||||
return this._requestJson({
|
||||
|
||||
@@ -27,7 +27,7 @@ t.create('Malformed url')
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'Package Name',
|
||||
message: 'invalid',
|
||||
message: 'inaccessible',
|
||||
color: 'lightgrey',
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DOMParser } from '@xmldom/xmldom'
|
||||
import { DOMParser } from 'xmldom'
|
||||
import xpath from 'xpath'
|
||||
import { MetricNames } from '../../core/base-service/metric-helper.js'
|
||||
import { renderDynamicBadge, errorMessages } from '../dynamic-common.js'
|
||||
@@ -15,53 +15,6 @@ export default class DynamicXml extends BaseService {
|
||||
static category = 'dynamic'
|
||||
static enabledMetrics = [MetricNames.SERVICE_RESPONSE_SIZE]
|
||||
static route = createRoute('xml')
|
||||
static openApi = {
|
||||
'/badge/dynamic/xml': {
|
||||
get: {
|
||||
summary: 'Dynamic XML Badge',
|
||||
description: `<p>
|
||||
The Dynamic XML Badge allows you to extract an arbitrary value from any
|
||||
XML Document using an XPath selector and show it on a badge.
|
||||
</p>`,
|
||||
parameters: [
|
||||
{
|
||||
name: 'url',
|
||||
description: 'The URL to a XML document',
|
||||
in: 'query',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'https://httpbin.org/xml',
|
||||
},
|
||||
{
|
||||
name: 'query',
|
||||
description:
|
||||
'A <a href="http://xpather.com/">XPath</a> expression that will be used to query the document',
|
||||
in: 'query',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: '//slideshow/slide[1]/title',
|
||||
},
|
||||
{
|
||||
name: 'prefix',
|
||||
description: 'Optional prefix to append to the value',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
example: '[',
|
||||
},
|
||||
{
|
||||
name: 'suffix',
|
||||
description: 'Optional suffix to append to the value',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
example: ']',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
static defaultBadgeData = { label: 'custom badge' }
|
||||
|
||||
transform({ pathExpression, buffer }) {
|
||||
|
||||
@@ -6,53 +6,6 @@ import jsonPath from './json-path.js'
|
||||
export default class DynamicYaml extends jsonPath(BaseYamlService) {
|
||||
static enabledMetrics = [MetricNames.SERVICE_RESPONSE_SIZE]
|
||||
static route = createRoute('yaml')
|
||||
static openApi = {
|
||||
'/badge/dynamic/yaml': {
|
||||
get: {
|
||||
summary: 'Dynamic YAML Badge',
|
||||
description: `<p>
|
||||
The Dynamic YAML Badge allows you to extract an arbitrary value from any
|
||||
YAML Document using a JSONPath selector and show it on a badge.
|
||||
</p>`,
|
||||
parameters: [
|
||||
{
|
||||
name: 'url',
|
||||
description: 'The URL to a YAML document',
|
||||
in: 'query',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example:
|
||||
'https://raw.githubusercontent.com/badges/shields/master/.github/dependabot.yml',
|
||||
},
|
||||
{
|
||||
name: 'query',
|
||||
description:
|
||||
'A <a href="https://jsonpath.com/">JSONPath</a> expression that will be used to query the document',
|
||||
in: 'query',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: '$.version',
|
||||
},
|
||||
{
|
||||
name: 'prefix',
|
||||
description: 'Optional prefix to append to the value',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
example: '[',
|
||||
},
|
||||
{
|
||||
name: 'suffix',
|
||||
description: 'Optional suffix to append to the value',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
example: ']',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
async fetch({ schema, url, errorMessages }) {
|
||||
return this._requestYaml({
|
||||
|
||||
@@ -11,144 +11,14 @@ const queryParamSchema = Joi.object({
|
||||
url: optionalUrl.required(),
|
||||
}).required()
|
||||
|
||||
const description = `<p>
|
||||
Using the endpoint badge, you can provide content for a badge through
|
||||
a JSON endpoint. The content can be prerendered, or generated on the
|
||||
fly. To strike a balance between responsiveness and bandwidth
|
||||
utilization on one hand, and freshness on the other, cache behavior is
|
||||
configurable, subject to the Shields minimum. The endpoint URL is
|
||||
provided to Shields through the query string. Shields fetches it and
|
||||
formats the badge.
|
||||
</p>
|
||||
<p>
|
||||
The endpoint badge takes a single required query param: <code>url</code>, which is the URL to your JSON endpoint
|
||||
</p>
|
||||
<div>
|
||||
<h2>Example JSON Endpoint Response</h2>
|
||||
<code>{ "schemaVersion": 1, "label": "hello", "message": "sweet world", "color": "orange" }</code>
|
||||
<h2>Example Shields Response</h2>
|
||||
<img src="https://img.shields.io/badge/hello-sweet_world-orange" />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Schema</h2>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Property</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>schemaVersion</code></td>
|
||||
<td>Required. Always the number <code>1</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>label</code></td>
|
||||
<td>
|
||||
Required. The left text, or the empty string to omit the left side of
|
||||
the badge. This can be overridden by the query string.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>message</code></td>
|
||||
<td>Required. Can't be empty. The right text.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>color</code></td>
|
||||
<td>
|
||||
Default: <code>lightgrey</code>. The right color. Supports the eight
|
||||
named colors above, as well as hex, rgb, rgba, hsl, hsla and css named
|
||||
colors. This can be overridden by the query string.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>labelColor</code></td>
|
||||
<td>
|
||||
Default: <code>grey</code>. The left color. This can be overridden by
|
||||
the query string.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>isError</code></td>
|
||||
<td>
|
||||
Default: <code>false</code>. <code>true</code> to treat this as an
|
||||
error badge. This prevents the user from overriding the color. In the
|
||||
future, it may affect cache behavior.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>namedLogo</code></td>
|
||||
<td>
|
||||
Default: none. One of the named logos supported by Shields or
|
||||
<a href="https://simpleicons.org/">simple-icons</a>. Can be overridden
|
||||
by the query string.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>logoSvg</code></td>
|
||||
<td>Default: none. An SVG string containing a custom logo.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>logoColor</code></td>
|
||||
<td>
|
||||
Default: none. Same meaning as the query string. Can be overridden by
|
||||
the query string. Only works for named logos and Shields logos. If you
|
||||
override the color of a multicolor Shield logo, the corresponding
|
||||
named logo will be used and colored.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>logoWidth</code></td>
|
||||
<td>
|
||||
Default: none. Same meaning as the query string. Can be overridden by
|
||||
the query string.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>logoPosition</code></td>
|
||||
<td>
|
||||
Default: none. Same meaning as the query string. Can be overridden by
|
||||
the query string.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>style</code></td>
|
||||
<td>
|
||||
Default: <code>flat</code>. The default template to use. Can be
|
||||
overridden by the query string.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>`
|
||||
|
||||
export default class Endpoint extends BaseJsonService {
|
||||
static category = 'dynamic'
|
||||
|
||||
static route = {
|
||||
base: 'endpoint',
|
||||
pattern: '',
|
||||
queryParamSchema,
|
||||
}
|
||||
|
||||
static openApi = {
|
||||
'/endpoint': {
|
||||
get: {
|
||||
summary: 'Endpoint Badge',
|
||||
description,
|
||||
parameters: [
|
||||
{
|
||||
name: 'url',
|
||||
description: 'The URL to your JSON endpoint',
|
||||
in: 'query',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'https://shields.redsparr0w.com/2473/monday',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
static _cacheLength = 300
|
||||
static defaultBadgeData = { label: 'custom badge' }
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import zlib from 'zlib'
|
||||
import { expect } from 'chai'
|
||||
import { getShieldsIcon, getSimpleIcon } from '../../lib/logos.js'
|
||||
import { getShieldsIcon } from '../../lib/logos.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
@@ -73,13 +73,13 @@ t.create('named logo with color')
|
||||
schemaVersion: 1,
|
||||
label: 'hey',
|
||||
message: 'yo',
|
||||
namedLogo: 'github',
|
||||
namedLogo: 'npm',
|
||||
logoColor: 'blue',
|
||||
})
|
||||
)
|
||||
.after((err, res, body) => {
|
||||
expect(err).not.to.be.ok
|
||||
expect(body).to.include(getSimpleIcon({ name: 'github', color: 'blue' }))
|
||||
expect(body).to.include(getShieldsIcon({ name: 'npm', color: 'blue' }))
|
||||
})
|
||||
|
||||
const logoSvg = Buffer.from(
|
||||
|
||||
@@ -22,14 +22,14 @@ export default class GitHubCommitActivity extends GithubAuthV4Service {
|
||||
static category = 'activity'
|
||||
static route = {
|
||||
base: 'github/commit-activity',
|
||||
pattern: ':interval(t|y|m|4w|w)/:user/:repo/:branch*',
|
||||
pattern: ':interval(y|m|4w|w)/:user/:repo/:branch*',
|
||||
}
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'GitHub commit activity',
|
||||
// Override the pattern to omit the deprecated interval "4w".
|
||||
pattern: ':interval(t|y|m|w)/:user/:repo',
|
||||
pattern: ':interval(y|m|w)/:user/:repo',
|
||||
namedParams: { interval: 'm', user: 'eslint', repo: 'eslint' },
|
||||
staticPreview: this.render({ interval: 'm', commitCount: 457 }),
|
||||
keywords: ['commits'],
|
||||
@@ -38,7 +38,7 @@ export default class GitHubCommitActivity extends GithubAuthV4Service {
|
||||
{
|
||||
title: 'GitHub commit activity (branch)',
|
||||
// Override the pattern to omit the deprecated interval "4w".
|
||||
pattern: ':interval(t|y|m|w)/:user/:repo/:branch*',
|
||||
pattern: ':interval(y|m|w)/:user/:repo/:branch*',
|
||||
namedParams: {
|
||||
interval: 'm',
|
||||
user: 'badges',
|
||||
@@ -54,11 +54,7 @@ export default class GitHubCommitActivity extends GithubAuthV4Service {
|
||||
static defaultBadgeData = { label: 'commit activity', color: 'blue' }
|
||||
|
||||
static render({ interval, commitCount }) {
|
||||
// If total commits selected change label from commit activity to commits
|
||||
const label = interval === 't' ? 'commits' : undefined
|
||||
|
||||
const intervalLabel = {
|
||||
t: '',
|
||||
y: '/year',
|
||||
m: '/month',
|
||||
'4w': '/four weeks',
|
||||
@@ -66,7 +62,6 @@ export default class GitHubCommitActivity extends GithubAuthV4Service {
|
||||
}[interval]
|
||||
|
||||
return {
|
||||
label,
|
||||
message: `${metric(commitCount)}${intervalLabel}`,
|
||||
}
|
||||
}
|
||||
@@ -79,7 +74,7 @@ export default class GitHubCommitActivity extends GithubAuthV4Service {
|
||||
$user: String!
|
||||
$repo: String!
|
||||
$branch: String!
|
||||
$since: GitTimestamp
|
||||
$since: GitTimestamp!
|
||||
) {
|
||||
repository(owner: $user, name: $repo) {
|
||||
object(expression: $branch) {
|
||||
@@ -118,9 +113,7 @@ export default class GitHubCommitActivity extends GithubAuthV4Service {
|
||||
static getIntervalQueryStartDate({ interval }) {
|
||||
const now = new Date()
|
||||
|
||||
if (interval === 't') {
|
||||
return null
|
||||
} else if (interval === 'y') {
|
||||
if (interval === 'y') {
|
||||
now.setUTCFullYear(now.getUTCFullYear() - 1)
|
||||
} else if (interval === 'm' || interval === '4w') {
|
||||
now.setUTCDate(now.getUTCDate() - 30)
|
||||
|
||||
@@ -2,7 +2,6 @@ import Joi from 'joi'
|
||||
import {
|
||||
isMetricOverTimePeriod,
|
||||
isZeroOverTimePeriod,
|
||||
isMetric,
|
||||
} from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
@@ -12,11 +11,6 @@ const isCommitActivity = Joi.alternatives().try(
|
||||
isZeroOverTimePeriod
|
||||
)
|
||||
|
||||
t.create('commit activity (total)').get('/t/badges/shields.json').expectBadge({
|
||||
label: 'commits',
|
||||
message: isMetric,
|
||||
})
|
||||
|
||||
t.create('commit activity (1 year)').get('/y/eslint/eslint.json').expectBadge({
|
||||
label: 'commit activity',
|
||||
message: isMetricOverTimePeriod,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AuthHelper } from '../../core/base-service/auth-helper.js'
|
||||
import SqlTokenPersistence from '../../core/token-pooling/sql-token-persistence.js'
|
||||
import RedisTokenPersistence from '../../core/token-pooling/redis-token-persistence.js'
|
||||
import log from '../../core/server/log.js'
|
||||
import GithubApiProvider from './github-api-provider.js'
|
||||
import { setRoutes as setAcceptorRoutes } from './auth/acceptor.js'
|
||||
@@ -23,12 +23,12 @@ class GithubConstellation {
|
||||
this._debugEnabled = config.service.debug.enabled
|
||||
this._debugIntervalSeconds = config.service.debug.intervalSeconds
|
||||
|
||||
const { postgres_url: pgUrl, gh_token: globalToken } = config.private
|
||||
if (pgUrl) {
|
||||
log.log('Token persistence configured with dbUrl')
|
||||
this.persistence = new SqlTokenPersistence({
|
||||
url: pgUrl,
|
||||
table: 'github_user_tokens',
|
||||
const { redis_url: redisUrl, gh_token: globalToken } = config.private
|
||||
if (redisUrl) {
|
||||
log.log('Token persistence configured with redisUrl')
|
||||
this.persistence = new RedisTokenPersistence({
|
||||
url: redisUrl,
|
||||
key: 'githubUserTokens',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ const documentation = `
|
||||
badge can be added to the project readme to encourage potential
|
||||
contributors to review the suggested issues and to celebrate the
|
||||
contributions that have already been made.
|
||||
|
||||
The badge displays three pieces of information:
|
||||
<ul>
|
||||
<li>
|
||||
@@ -32,6 +33,7 @@ const documentation = `
|
||||
</li>
|
||||
<li>The number of days left of October.</li>
|
||||
</ul>
|
||||
|
||||
</p>
|
||||
|
||||
${githubDocumentation}
|
||||
|
||||
@@ -66,9 +66,9 @@ class GithubPipenvLockedPythonVersion extends ConditionalGithubAuthV3Service {
|
||||
namedParams: {
|
||||
user: 'metabolize',
|
||||
repo: 'rq-dashboard-on-heroku',
|
||||
branch: 'main',
|
||||
branch: 'master',
|
||||
},
|
||||
staticPreview: this.render({ version: '3.7', branch: 'main' }),
|
||||
staticPreview: this.render({ version: '3.7', branch: 'master' }),
|
||||
documentation,
|
||||
keywords,
|
||||
},
|
||||
@@ -135,7 +135,7 @@ class GithubPipenvLockedDependencyVersion extends ConditionalGithubAuthV3Service
|
||||
repo: 'rq-dashboard-on-heroku',
|
||||
kind: 'dev',
|
||||
packageName: 'black',
|
||||
branch: 'main',
|
||||
branch: 'master',
|
||||
},
|
||||
staticPreview: this.render({ dependency: 'black', version: '19.3b0' }),
|
||||
documentation,
|
||||
|
||||
@@ -47,7 +47,7 @@ t.create('Locked version of default dependency')
|
||||
|
||||
t.create('Locked version of default dependency (branch)')
|
||||
.get(
|
||||
'/locked/dependency-version/metabolize/rq-dashboard-on-heroku/rq-dashboard/main.json'
|
||||
'/locked/dependency-version/metabolize/rq-dashboard-on-heroku/rq-dashboard/master.json'
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'rq-dashboard',
|
||||
@@ -65,7 +65,7 @@ t.create('Locked version of dev dependency')
|
||||
|
||||
t.create('Locked version of dev dependency (branch)')
|
||||
.get(
|
||||
'/locked/dependency-version/metabolize/rq-dashboard-on-heroku/dev/black/main.json'
|
||||
'/locked/dependency-version/metabolize/rq-dashboard-on-heroku/dev/black/master.json'
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'black',
|
||||
|
||||
@@ -21,7 +21,7 @@ export default class GithubSize extends GithubAuthV3Service {
|
||||
|
||||
static route = {
|
||||
base: 'github/size',
|
||||
pattern: ':user/:repo/:path+',
|
||||
pattern: ':user/:repo/:path*',
|
||||
queryParamSchema,
|
||||
}
|
||||
|
||||
|
||||
@@ -45,12 +45,12 @@ export default class GithubSponsors extends GithubAuthV4Service {
|
||||
query ($user: String!) {
|
||||
repositoryOwner(login: $user) {
|
||||
... on User {
|
||||
sponsorshipsAsMaintainer(includePrivate: true) {
|
||||
sponsorshipsAsMaintainer {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
... on Organization {
|
||||
sponsorshipsAsMaintainer(includePrivate: true) {
|
||||
sponsorshipsAsMaintainer {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,13 +30,4 @@ export default [
|
||||
dateAdded: new Date('2019-11-29'),
|
||||
...commonProps,
|
||||
}),
|
||||
redirector({
|
||||
route: {
|
||||
base: 'jenkins/coverage/api',
|
||||
pattern: '',
|
||||
},
|
||||
category: 'coverage',
|
||||
transformPath: () => '/jenkins/coverage/apiv1',
|
||||
dateAdded: new Date('2023-03-21'),
|
||||
}),
|
||||
]
|
||||
|
||||
@@ -53,11 +53,3 @@ t.create('api prefix + job url in path')
|
||||
'https://jenkins.library.illinois.edu/job/OpenSourceProjects/job/Speedwagon/job/master'
|
||||
)}`
|
||||
)
|
||||
|
||||
t.create('old v1 api prefix to new prefix')
|
||||
.get(
|
||||
'/coverage/api.svg?jobUrl=http://loneraver.duckdns.org:8082/job/github/job/VisVid/job/master'
|
||||
)
|
||||
.expectRedirect(
|
||||
'/jenkins/coverage/apiv1.svg?jobUrl=http://loneraver.duckdns.org:8082/job/github/job/VisVid/job/master'
|
||||
)
|
||||
|
||||
@@ -42,7 +42,7 @@ const formatMap = {
|
||||
},
|
||||
pluginSpecificPath: 'cobertura',
|
||||
},
|
||||
apiv1: {
|
||||
api: {
|
||||
schema: Joi.object({
|
||||
results: Joi.object({
|
||||
elements: Joi.array()
|
||||
@@ -66,25 +66,6 @@ const formatMap = {
|
||||
},
|
||||
pluginSpecificPath: 'coverage/result',
|
||||
},
|
||||
apiv4: {
|
||||
schema: Joi.object({
|
||||
projectStatistics: Joi.object({
|
||||
line: Joi.string()
|
||||
.pattern(/\d+\.\d+%/)
|
||||
.required(),
|
||||
}).required(),
|
||||
}).required(),
|
||||
treeQueryParam: 'projectStatistics[line]',
|
||||
transform: json => {
|
||||
const lineCoverageStr = json.projectStatistics.line
|
||||
const lineCoverage = lineCoverageStr.substring(
|
||||
0,
|
||||
lineCoverageStr.length - 1
|
||||
)
|
||||
return { coverage: Number.parseFloat(lineCoverage) }
|
||||
},
|
||||
pluginSpecificPath: 'coverage',
|
||||
},
|
||||
}
|
||||
|
||||
const documentation = `
|
||||
@@ -93,7 +74,7 @@ const documentation = `
|
||||
<ul>
|
||||
<li><a href="https://plugins.jenkins.io/jacoco">JaCoCo</a></li>
|
||||
<li><a href="https://plugins.jenkins.io/cobertura">Cobertura</a></li>
|
||||
<li>Any plugin which integrates with version 1 or 4+ of the <a href="https://plugins.jenkins.io/code-coverage-api">Code Coverage API</a> (e.g. llvm-cov, Cobertura 1.13+, etc.)</li>
|
||||
<li>Any plugin which integrates with the <a href="https://plugins.jenkins.io/code-coverage-api">Code Coverage API</a> (e.g. llvm-cov, Cobertura 1.13+, etc.)</li>
|
||||
</ul>
|
||||
</p>
|
||||
`
|
||||
@@ -103,7 +84,7 @@ export default class JenkinsCoverage extends JenkinsBase {
|
||||
|
||||
static route = {
|
||||
base: 'jenkins/coverage',
|
||||
pattern: ':format(jacoco|cobertura|apiv1|apiv4)',
|
||||
pattern: ':format(jacoco|cobertura|api)',
|
||||
queryParamSchema,
|
||||
}
|
||||
|
||||
|
||||
@@ -31,49 +31,14 @@ t.create('cobertura: job found')
|
||||
)
|
||||
.expectBadge({ label: 'coverage', message: isIntegerPercentage })
|
||||
|
||||
t.create('code coverage API v1: job not found')
|
||||
t.create('code coverage API: job not found')
|
||||
.get(
|
||||
'/apiv1.json?jobUrl=https://jenkins.library.illinois.edu/job/does-not-exist'
|
||||
'/api.json?jobUrl=https://jenkins.library.illinois.edu/job/does-not-exist'
|
||||
)
|
||||
.expectBadge({ label: 'coverage', message: 'job or coverage not found' })
|
||||
|
||||
const coverageApiV1Response = {
|
||||
_class: 'io.jenkins.plugins.coverage.targets.CoverageResult',
|
||||
results: {
|
||||
elements: [
|
||||
{ name: 'Report', ratio: 100.0 },
|
||||
{ name: 'Group', ratio: 100.0 },
|
||||
{ name: 'Package', ratio: 66.666664 },
|
||||
{ name: 'File', ratio: 52.0 },
|
||||
{ name: 'Class', ratio: 52.0 },
|
||||
{ name: 'Line', ratio: 40.66363 },
|
||||
{ name: 'Conditional', ratio: 29.91968 },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
t.create('code coverage API v1: job found')
|
||||
t.create('code coverage API: job found')
|
||||
.get(
|
||||
'/apiv1.json?jobUrl=http://loneraver.duckdns.org:8082/job/github/job/VisVid/job/master'
|
||||
)
|
||||
.intercept(nock =>
|
||||
nock(
|
||||
'http://loneraver.duckdns.org:8082/job/github/job/VisVid/job/master/lastCompletedBuild'
|
||||
)
|
||||
.get('/coverage/result/api/json')
|
||||
.query(true)
|
||||
.reply(200, coverageApiV1Response)
|
||||
)
|
||||
.expectBadge({ label: 'coverage', message: isIntegerPercentage })
|
||||
|
||||
t.create('code coverage API v4+: job not found')
|
||||
.get(
|
||||
'/apiv4.json?jobUrl=https://jenkins.library.illinois.edu/job/does-not-exist'
|
||||
)
|
||||
.expectBadge({ label: 'coverage', message: 'job or coverage not found' })
|
||||
|
||||
t.create('code coverage API v4+: job found')
|
||||
.get(
|
||||
'/apiv4.json?jobUrl=https://jenkins.mm12.xyz/jenkins/job/nmfu/job/master'
|
||||
'/api.json?jobUrl=http://loneraver.duckdns.org:8082/job/github/job/VisVid/job/master/'
|
||||
)
|
||||
.expectBadge({ label: 'coverage', message: isIntegerPercentage })
|
||||
|
||||
@@ -14,7 +14,7 @@ const queryParamSchema = Joi.object({
|
||||
|
||||
const documentation = `
|
||||
<p>To find your user id, you can use <a link target="_blank" href="https://prouser123.me/misc/mastodon-userid-lookup.html">this tool</a>.</p><br>
|
||||
<p>Alternatively you can make a request to <code>https://your.mastodon.server/.well-known/webfinger?resource=acct:{user}@{domain}</code></p>
|
||||
<p>Alternatively you can make a request to <code><br>https://your.mastodon.server/.well-known/webfinger?resource=acct:{user}@{domain}</br></code></p>
|
||||
<p>Failing that, you can also visit your profile page, where your user ID will be in the header in a tag like this: <code><link href='https://your.mastodon.server/api/salmon/{your-user-id}' rel='salmon'></code></p>
|
||||
`
|
||||
|
||||
|
||||
@@ -29,9 +29,9 @@ const matrixStateSchema = Joi.array()
|
||||
const documentation = `
|
||||
<p>
|
||||
In order for this badge to work, the host of your room must allow guest accounts or dummy accounts to register, and the room must be world readable (chat history visible to anyone).
|
||||
<br>
|
||||
</br>
|
||||
The following steps will show you how to setup the badge URL using the Element Matrix client.
|
||||
<br>
|
||||
</br>
|
||||
<ul>
|
||||
<li>Select the desired room inside the Element client</li>
|
||||
<li>Click on the room settings button (gear icon) located near the top right of the client</li>
|
||||
@@ -41,11 +41,11 @@ const documentation = `
|
||||
<li>Remove the starting hash character (<code>#</code>)</li>
|
||||
<li>The final badge URL should look something like this <code>/matrix/twim:matrix.org.svg</code></li>
|
||||
</ul>
|
||||
<br>
|
||||
</br>
|
||||
Some Matrix homeservers don't hold a server name matching where they live (e.g. if the homeserver <code>example.com</code> that created the room alias <code>#mysuperroom:example.com</code> lives at <code>matrix.example.com</code>).
|
||||
<br>
|
||||
</br>
|
||||
If that is the case of the homeserver that created the room alias used for generating the badge, you will need to add the server's FQDN (fully qualified domain name) as a query parameter.
|
||||
<br>
|
||||
</br>
|
||||
The final badge URL should then look something like this <code>/matrix/mysuperroom:example.com.svg?server_fqdn=matrix.example.com</code>.
|
||||
</p>
|
||||
`
|
||||
@@ -119,6 +119,7 @@ export default class Matrix extends BaseJsonService {
|
||||
errorMessages: {
|
||||
401: 'auth failed',
|
||||
403: 'guests not allowed',
|
||||
429: 'rate limited by remote server',
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -137,6 +138,7 @@ export default class Matrix extends BaseJsonService {
|
||||
errorMessages: {
|
||||
401: 'bad auth token',
|
||||
404: 'room not found',
|
||||
429: 'rate limited by remote server',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user