forked from mirrored/github-readme-stats
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33ad0f4ee8 | ||
|
|
26a8a23421 | ||
|
|
ba7c2f8b55 | ||
|
|
888663a477 | ||
|
|
112000667c | ||
|
|
7aa502d453 | ||
|
|
077d40561a | ||
|
|
99d9d3cde0 | ||
|
|
cd5cbcdb09 | ||
|
|
b2bf4fa487 | ||
|
|
4b17300126 | ||
|
|
a17fa1cf5d | ||
|
|
8667ea9908 | ||
|
|
fbb49e3c80 | ||
|
|
eab1402417 | ||
|
|
cfa84232e2 | ||
|
|
d5d1f44e1c |
@@ -0,0 +1,10 @@
|
||||
import os
|
||||
|
||||
file = open('./vercel.json', 'r')
|
||||
str = file.read()
|
||||
file = open('./vercel.json', 'w')
|
||||
|
||||
str = str.replace('"maxDuration": 10', '"maxDuration": 30')
|
||||
|
||||
file.write(str)
|
||||
file.close()
|
||||
@@ -0,0 +1,20 @@
|
||||
name: Deployment Prep
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
config:
|
||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Deployment Prep
|
||||
run: python ./.github/workflows/deploy-prep.py
|
||||
- uses: stefanzweifel/git-auto-commit-action@v4
|
||||
with:
|
||||
branch: vercel
|
||||
create_branch: true
|
||||
push_options: "--force"
|
||||
@@ -5,6 +5,7 @@ on:
|
||||
jobs:
|
||||
e2eTests:
|
||||
if:
|
||||
github.repository == 'anuraghazra/github-readme-stats' &&
|
||||
github.event_name == 'deployment_status' &&
|
||||
github.event.deployment_status.state == 'success'
|
||||
name: Perform 2e2 tests
|
||||
|
||||
@@ -8,6 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
closeEmptyIssuesAndTemplates:
|
||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
||||
name: Close empty issues
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -4,6 +4,7 @@ on:
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v4
|
||||
|
||||
@@ -5,6 +5,7 @@ on:
|
||||
|
||||
jobs:
|
||||
closeOldThemePrs:
|
||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
||||
name: Close stale 'invalid' theme PRs
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
|
||||
@@ -5,6 +5,7 @@ on:
|
||||
|
||||
jobs:
|
||||
showAndLabelTopIssues:
|
||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
||||
name: Update top issues Dashboard.
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
name: Update supported languages
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 */30 * *"
|
||||
|
||||
jobs:
|
||||
updateLanguages:
|
||||
if: github.repository == 'anuraghazra/github-readme-stats'
|
||||
name: Update supported languages
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Run update-languages-json.js script
|
||||
run: npm run generate-langs-json
|
||||
|
||||
- name: Create Pull Request if upstream language file is changed
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
commit-message: "refactor: update languages JSON"
|
||||
branch: "update_langs/patch"
|
||||
delete-branch: true
|
||||
title: Update languages JSON
|
||||
body:
|
||||
"The
|
||||
[update-langs](https://github.com/anuraghazra/github-readme-stats/actions/workflows/update-langs.yaml)
|
||||
action found new/updated languages in the [upstream languages JSON
|
||||
file](https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml)."
|
||||
labels: "ci, lang-card"
|
||||
+3
-25
@@ -7,7 +7,6 @@ import {
|
||||
parseBoolean,
|
||||
renderError,
|
||||
} from "../src/common/utils.js";
|
||||
import { HttpException } from "../src/common/exceptions.js";
|
||||
import { fetchStats } from "../src/fetchers/stats-fetcher.js";
|
||||
import { isLocaleAvailable } from "../src/translations.js";
|
||||
|
||||
@@ -57,7 +56,7 @@ export default async (req, res) => {
|
||||
);
|
||||
|
||||
const cacheSeconds = clampValue(
|
||||
parseInt(cache_seconds || CONSTANTS.CARD_CACHE_SECONDS, 10),
|
||||
parseInt(cache_seconds || CONSTANTS.FOUR_HOURS, 10),
|
||||
CONSTANTS.FOUR_HOURS,
|
||||
CONSTANTS.ONE_DAY,
|
||||
);
|
||||
@@ -66,8 +65,7 @@ export default async (req, res) => {
|
||||
"Cache-Control",
|
||||
`max-age=${
|
||||
cacheSeconds / 2
|
||||
}, s-maxage=${cacheSeconds}, stale-while-revalidate=${CONSTANTS.ONE_DAY},
|
||||
stale-if-error=${CONSTANTS.ONE_HOUR}`,
|
||||
}, s-maxage=${cacheSeconds}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
|
||||
);
|
||||
|
||||
return res.send(
|
||||
@@ -95,27 +93,7 @@ export default async (req, res) => {
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
// Throw error if REST and GraphQL API calls fail. This way we can return a cached
|
||||
if (err instanceof HttpException) {
|
||||
// throw err;
|
||||
// throw new Error(err.message);
|
||||
// return res.status(404).;
|
||||
// return {statusCode: 404, body: err.message}
|
||||
return res
|
||||
.status(500)
|
||||
.send(
|
||||
renderError(err.errors[0].message, err.errors[0].secondaryMessage),
|
||||
);
|
||||
}
|
||||
|
||||
// Cache the error response less frequently.
|
||||
res.setHeader(
|
||||
"Cache-Control",
|
||||
`max-age=${CONSTANTS.ERROR_CACHE_SECONDS / 2}, s-maxage=${
|
||||
CONSTANTS.ERROR_CACHE_SECONDS
|
||||
}, stale-while-revalidate=${CONSTANTS.ONE_DAY}
|
||||
stale-if-error=${CONSTANTS.ONE_HOUR}`,
|
||||
);
|
||||
res.setHeader("Cache-Control", `no-cache, no-store, must-revalidate`); // Don't cache error responses.
|
||||
return res.send(renderError(err.message, err.secondaryMessage));
|
||||
}
|
||||
};
|
||||
|
||||
+2
-7
@@ -40,7 +40,7 @@ export default async (req, res) => {
|
||||
const repoData = await fetchRepo(username, repo);
|
||||
|
||||
let cacheSeconds = clampValue(
|
||||
parseInt(cache_seconds || CONSTANTS.CARD_CACHE_SECONDS, 10),
|
||||
parseInt(cache_seconds || CONSTANTS.FOUR_HOURS, 10),
|
||||
CONSTANTS.FOUR_HOURS,
|
||||
CONSTANTS.ONE_DAY,
|
||||
);
|
||||
@@ -80,12 +80,7 @@ export default async (req, res) => {
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
res.setHeader(
|
||||
"Cache-Control",
|
||||
`max-age=${CONSTANTS.ERROR_CACHE_SECONDS / 2}, s-maxage=${
|
||||
CONSTANTS.ERROR_CACHE_SECONDS
|
||||
}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
|
||||
); // Cache the error response less frequently.
|
||||
res.setHeader("Cache-Control", `no-cache, no-store, must-revalidate`); // Don't cache error responses.
|
||||
return res.send(renderError(err.message, err.secondaryMessage));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* @file Contains a simple cloud function that can be used to check which PATs are no
|
||||
* longer working. It returns a list of valid PATs, expired PATs and PATs with errors.
|
||||
*
|
||||
* @description This function is currently rate limited to 1 request per 10 minutes.
|
||||
*/
|
||||
|
||||
import { logger, request, dateDiff } from "../../src/common/utils.js";
|
||||
export const RATE_LIMIT_SECONDS = 60 * 5; // 1 request per 10 minutes
|
||||
|
||||
/**
|
||||
* Simple uptime check fetcher for the PATs.
|
||||
*
|
||||
* @param {import('axios').AxiosRequestHeaders} variables
|
||||
* @param {string} token
|
||||
*/
|
||||
const uptimeFetcher = (variables, token) => {
|
||||
return request(
|
||||
{
|
||||
query: `
|
||||
query {
|
||||
rateLimit {
|
||||
remaining
|
||||
resetAt
|
||||
},
|
||||
}`,
|
||||
variables,
|
||||
},
|
||||
{
|
||||
Authorization: `bearer ${token}`,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const getAllPATs = () => {
|
||||
return Object.keys(process.env).filter((key) => /PAT_\d*$/.exec(key));
|
||||
};
|
||||
|
||||
/**
|
||||
* Check whether any of the PATs is expired.
|
||||
*/
|
||||
const getPATInfo = async (fetcher, variables) => {
|
||||
const details = {};
|
||||
const PATs = getAllPATs();
|
||||
|
||||
for (const pat of PATs) {
|
||||
try {
|
||||
const response = await fetcher(variables, process.env[pat]);
|
||||
const errors = response.data.errors;
|
||||
const hasErrors = Boolean(errors);
|
||||
const errorType = errors?.[0]?.type;
|
||||
const isRateLimited =
|
||||
(hasErrors && errorType === "RATE_LIMITED") ||
|
||||
response.data.data?.rateLimit?.remaining === 0;
|
||||
|
||||
// Store PATs with errors.
|
||||
if (hasErrors && errorType !== "RATE_LIMITED") {
|
||||
details[pat] = {
|
||||
status: "error",
|
||||
error: {
|
||||
type: errors[0].type,
|
||||
message: errors[0].message,
|
||||
},
|
||||
};
|
||||
continue;
|
||||
} else if (isRateLimited) {
|
||||
const date1 = new Date();
|
||||
const date2 = new Date(response.data?.data?.rateLimit?.resetAt);
|
||||
details[pat] = {
|
||||
status: "exhausted",
|
||||
remaining: 0,
|
||||
resetIn: dateDiff(date2, date1) + " minutes",
|
||||
};
|
||||
} else {
|
||||
details[pat] = {
|
||||
status: "valid",
|
||||
remaining: response.data.data.rateLimit.remaining,
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
// Store the PAT if it is expired.
|
||||
const errorMessage = err.response?.data?.message?.toLowerCase();
|
||||
if (errorMessage === "bad credentials") {
|
||||
details[pat] = {
|
||||
status: "expired",
|
||||
};
|
||||
} else if (errorMessage === "sorry. your account was suspended.") {
|
||||
details[pat] = {
|
||||
status: "suspended",
|
||||
};
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const filterPATsByStatus = (status) => {
|
||||
return Object.keys(details).filter((pat) => details[pat].status === status);
|
||||
};
|
||||
|
||||
const sortedDetails = Object.keys(details)
|
||||
.sort()
|
||||
.reduce((obj, key) => {
|
||||
obj[key] = details[key];
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
validPATs: filterPATsByStatus("valid"),
|
||||
expiredPATs: filterPATsByStatus("expired"),
|
||||
exhaustedPATs: filterPATsByStatus("exhausted"),
|
||||
suspendedPATs: filterPATsByStatus("suspended"),
|
||||
errorPATs: filterPATsByStatus("error"),
|
||||
details: sortedDetails,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Cloud function that returns information about the used PATs.
|
||||
*/
|
||||
export default async (_, res) => {
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
try {
|
||||
// Add header to prevent abuse.
|
||||
const PATsInfo = await getPATInfo(uptimeFetcher, {});
|
||||
if (PATsInfo) {
|
||||
res.setHeader(
|
||||
"Cache-Control",
|
||||
`max-age=0, s-maxage=${RATE_LIMIT_SECONDS}`,
|
||||
);
|
||||
}
|
||||
res.send(JSON.stringify(PATsInfo, null, 2));
|
||||
} catch (err) {
|
||||
// Throw error if something went wrong.
|
||||
logger.error(err);
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
res.send("Something went wrong: " + err.message);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* @file Contains a simple cloud function that can be used to check if the PATs are still
|
||||
* functional.
|
||||
*
|
||||
* @description This function is currently rate limited to 1 request per 10 minutes.
|
||||
*/
|
||||
|
||||
import retryer from "../../src/common/retryer.js";
|
||||
import { logger, request } from "../../src/common/utils.js";
|
||||
|
||||
export const RATE_LIMIT_SECONDS = 60 * 10; // 1 request per 10 minutes
|
||||
|
||||
/**
|
||||
* Simple uptime check fetcher for the PATs.
|
||||
*
|
||||
* @param {import('axios').AxiosRequestHeaders} variables
|
||||
* @param {string} token
|
||||
*/
|
||||
const uptimeFetcher = (variables, token) => {
|
||||
return request(
|
||||
{
|
||||
query: `
|
||||
query {
|
||||
rateLimit {
|
||||
remaining
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables,
|
||||
},
|
||||
{
|
||||
Authorization: `bearer ${token}`,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates Json response that can be used for shields.io dynamic card generation.
|
||||
*
|
||||
* @param {*} up Whether the PATs are up or not.
|
||||
* @returns Dynamic shields.io JSON response object.
|
||||
*
|
||||
* @see https://shields.io/endpoint.
|
||||
*/
|
||||
const shieldsUptimeBadge = (up) => {
|
||||
const schemaVersion = 1;
|
||||
const isError = true;
|
||||
const label = "Public Instance";
|
||||
const message = up ? "up" : "down";
|
||||
const color = up ? "brightgreen" : "red";
|
||||
return {
|
||||
schemaVersion,
|
||||
label,
|
||||
message,
|
||||
color,
|
||||
isError,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Cloud function that returns whether the PATs are still functional.
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
let { type } = req.query;
|
||||
type = type ? type.toLowerCase() : "boolean";
|
||||
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
|
||||
try {
|
||||
let PATsValid = true;
|
||||
try {
|
||||
await retryer(uptimeFetcher, {});
|
||||
} catch (err) {
|
||||
PATsValid = false;
|
||||
}
|
||||
|
||||
if (PATsValid) {
|
||||
res.setHeader(
|
||||
"Cache-Control",
|
||||
`max-age=0, s-maxage=${RATE_LIMIT_SECONDS}`,
|
||||
);
|
||||
} else {
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case "shields":
|
||||
res.send(shieldsUptimeBadge(PATsValid));
|
||||
break;
|
||||
case "json":
|
||||
res.send({ up: PATsValid });
|
||||
break;
|
||||
default:
|
||||
res.send(PATsValid);
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
// Return fail boolean if something went wrong.
|
||||
logger.error(err);
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
res.send("Something went wrong: " + err.message);
|
||||
}
|
||||
};
|
||||
+4
-7
@@ -30,6 +30,7 @@ export default async (req, res) => {
|
||||
border_radius,
|
||||
border_color,
|
||||
disable_animations,
|
||||
hide_progress,
|
||||
} = req.query;
|
||||
res.setHeader("Content-Type", "image/svg+xml");
|
||||
|
||||
@@ -48,7 +49,7 @@ export default async (req, res) => {
|
||||
);
|
||||
|
||||
const cacheSeconds = clampValue(
|
||||
parseInt(cache_seconds || CONSTANTS.CARD_CACHE_SECONDS, 10),
|
||||
parseInt(cache_seconds || CONSTANTS.FOUR_HOURS, 10),
|
||||
CONSTANTS.FOUR_HOURS,
|
||||
CONSTANTS.ONE_DAY,
|
||||
);
|
||||
@@ -77,15 +78,11 @@ export default async (req, res) => {
|
||||
border_color,
|
||||
locale: locale ? locale.toLowerCase() : null,
|
||||
disable_animations: parseBoolean(disable_animations),
|
||||
hide_progress: parseBoolean(hide_progress),
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
res.setHeader(
|
||||
"Cache-Control",
|
||||
`max-age=${CONSTANTS.ERROR_CACHE_SECONDS / 2}, s-maxage=${
|
||||
CONSTANTS.ERROR_CACHE_SECONDS
|
||||
}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
|
||||
); // Cache the error response less frequently.
|
||||
res.setHeader("Cache-Control", `no-cache, no-store, must-revalidate`); // Don't cache error responses.
|
||||
return res.send(renderError(err.message, err.secondaryMessage));
|
||||
}
|
||||
};
|
||||
|
||||
+6
-7
@@ -43,11 +43,15 @@ export default async (req, res) => {
|
||||
const stats = await fetchWakatimeStats({ username, api_domain, range });
|
||||
|
||||
let cacheSeconds = clampValue(
|
||||
parseInt(cache_seconds || CONSTANTS.CARD_CACHE_SECONDS, 10),
|
||||
parseInt(cache_seconds || CONSTANTS.FOUR_HOURS, 10),
|
||||
CONSTANTS.FOUR_HOURS,
|
||||
CONSTANTS.ONE_DAY,
|
||||
);
|
||||
|
||||
if (!cache_seconds) {
|
||||
cacheSeconds = CONSTANTS.FOUR_HOURS;
|
||||
}
|
||||
|
||||
res.setHeader(
|
||||
"Cache-Control",
|
||||
`max-age=${
|
||||
@@ -76,12 +80,7 @@ export default async (req, res) => {
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
res.setHeader(
|
||||
"Cache-Control",
|
||||
`max-age=${CONSTANTS.ERROR_CACHE_SECONDS / 2}, s-maxage=${
|
||||
CONSTANTS.ERROR_CACHE_SECONDS
|
||||
}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
|
||||
); // Cache the error response less frequently.
|
||||
res.setHeader("Cache-Control", `no-cache, no-store, must-revalidate`); // Don't cache error responses.
|
||||
return res.send(renderError(err.message, err.secondaryMessage));
|
||||
}
|
||||
};
|
||||
|
||||
Generated
+1
-1
@@ -22,7 +22,7 @@
|
||||
"@testing-library/dom": "^8.17.1",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@uppercod/css-to-object": "^1.1.1",
|
||||
"axios-mock-adapter": "^1.18.1",
|
||||
"axios-mock-adapter": "^1.21.2",
|
||||
"color-contrast-checker": "^2.1.0",
|
||||
"hjson": "^3.2.2",
|
||||
"husky": "^8.0.0",
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@
|
||||
"@testing-library/dom": "^8.17.1",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@uppercod/css-to-object": "^1.1.1",
|
||||
"axios-mock-adapter": "^1.18.1",
|
||||
"axios-mock-adapter": "^1.21.2",
|
||||
"color-contrast-checker": "^2.1.0",
|
||||
"hjson": "^3.2.2",
|
||||
"husky": "^8.0.0",
|
||||
|
||||
@@ -80,6 +80,9 @@ Visit <https://indiafightscorona.giveindia.org> and make a small donation to hel
|
||||
|
||||
# Features
|
||||
|
||||
- [Getting Started](#getting-started)
|
||||
- [GitHub Actions](#github-actions-recommended)
|
||||
- [Community Deployment](#community-deployment)
|
||||
- [GitHub Stats Card](#github-stats-card)
|
||||
- [GitHub Extra Pins](#github-extra-pins)
|
||||
- [Top Languages Card](#top-languages-card)
|
||||
@@ -94,17 +97,141 @@ Visit <https://indiafightscorona.giveindia.org> and make a small donation to hel
|
||||
- [Wakatime Card Exclusive Option](#wakatime-card-exclusive-options)
|
||||
- [Deploy Yourself](#deploy-on-your-own-vercel-instance)
|
||||
- [Keep your fork up to date](#keep-your-fork-up-to-date)
|
||||
|
||||
# Getting Started
|
||||
|
||||
# GitHub Stats Card
|
||||
## GitHub Actions (Recommended)
|
||||
|
||||
Copy-paste this into your markdown content, and that is it. Simple!
|
||||
Using GitHub Actions over the community deployment is recommended because it provides more accurate stats and 100% uptime. Simply copy-paste the following into the `/.github/workflows/grs.yml` file in your README repo (`USERNAME/USERNAME`).
|
||||
|
||||
Change the `?username=` value to your GitHub username.
|
||||
```yml
|
||||
name: GitHub Readme Stats
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: 0 * * * *
|
||||
|
||||
jobs:
|
||||
grs:
|
||||
permissions: write-all
|
||||
runs-on: ubuntu-latest
|
||||
name: Generate Stats
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Generate Stats
|
||||
uses: Zo-Bro-23/grs-action@v1
|
||||
id: generate
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
options: 'username=${{ github.repository_owner }}'
|
||||
- name: Push
|
||||
uses: crazy-max/ghaction-github-pages@v3
|
||||
with:
|
||||
target_branch: grs
|
||||
build_dir: grs
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||
|
||||
If you want to generate multiple stats, such as top languages, wakatime, etc, you can use the following code.
|
||||
|
||||
```yml
|
||||
name: GitHub Readme Stats
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: 0 * * * *
|
||||
|
||||
jobs:
|
||||
stats:
|
||||
permissions: write-all
|
||||
runs-on: ubuntu-latest
|
||||
name: Stats Card
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Generate Stats
|
||||
uses: Zo-Bro-23/grs-action@v1
|
||||
id: generate
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
options: 'username=${{ github.repository_owner }}'
|
||||
- uses: actions/upload-artifact@master
|
||||
with:
|
||||
name: stats
|
||||
path: grs
|
||||
top-langs:
|
||||
permissions: write-all
|
||||
runs-on: ubuntu-latest
|
||||
name: Top Languages Card
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Generate Top Languages
|
||||
uses: Zo-Bro-23/grs-action@v1
|
||||
id: generate
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
options: 'username=${{ github.repository_owner }}'
|
||||
card: 'top-langs'
|
||||
- uses: actions/upload-artifact@master
|
||||
with:
|
||||
name: top-langs
|
||||
path: grs
|
||||
push:
|
||||
permissions: write-all
|
||||
runs-on: ubuntu-latest
|
||||
name: Push
|
||||
needs: [stats, top-langs]
|
||||
steps:
|
||||
- uses: actions/download-artifact@master
|
||||
with:
|
||||
name: stats
|
||||
path: grs
|
||||
- uses: actions/download-artifact@master
|
||||
with:
|
||||
name: top-langs
|
||||
path: grs
|
||||
- name: Push
|
||||
uses: crazy-max/ghaction-github-pages@v3
|
||||
with:
|
||||
target_branch: grs
|
||||
build_dir: grs
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||
|
||||
You can now include the stats in your README by using the following markdown code (replace `USERNAME` with your GitHub username).
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
You can also enable GitHub Pages on the `grs` branch (root) and use that URL (replace `GH_PAGES_URL` with your GitHub Pages URL).
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
The GitHub Action accepts the following inputs:
|
||||
- `token` - `REQUIRED` - Your GitHub Personal Access Token (PAT), or any other token that GitHub accepts.
|
||||
- `options` - `REQUIRED` - Any customization options. More below. (This is the same as the query string you would pass to the API if using the community deployment)
|
||||
- `card` - The type of card to generate. `repo`, `top-langs`, or `wakatime` (leave empty for stats card).
|
||||
- `path` - Output path for SVG file. Relative path; include filename with `.svg`.
|
||||
- `fetch_multipage` - Experimental feature that gives more accurate stats if you have >100 repos.
|
||||
|
||||
## Community Deployment
|
||||
Copy-paste the following markdown code. Change the `?username=` value to your GitHub username.
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
|
||||
Other cards can be included by using `/api/top-langs`, `/api/pin`, or `/api/wakatime`. More customization options can be added by using query parameters (`&option1=value1&option2=value2&...`).
|
||||
|
||||
# GitHub Stats Card
|
||||
|
||||
> **Note**
|
||||
> Available ranks are S+ (top 1%), S (top 25%), A++ (top 45%), A+ (top 60%), and B+ (everyone). The values are calculated by using the [cumulative distribution function](https://en.wikipedia.org/wiki/Cumulative_distribution_function) using commits, contributions, issues, stars, pull requests, followers, and owned repositories. The implementation can be investigated at [src/calculateRank.js](./src/calculateRank.js).
|
||||
|
||||
@@ -115,7 +242,7 @@ You can pass a query parameter `&hide=` to hide any specific stats with comma-se
|
||||
> Options: `&hide=stars,commits,prs,issues,contribs`
|
||||
|
||||
```md
|
||||

|
||||
?username=anuraghazra&hide=contribs,prs
|
||||
```
|
||||
|
||||
### Adding private contributions count to total commits count
|
||||
@@ -128,7 +255,7 @@ You can add the count of all your private contributions to the total commits cou
|
||||
> Options: `&count_private=true`
|
||||
|
||||
```md
|
||||

|
||||
?username=anuraghazra&count_private=true
|
||||
```
|
||||
|
||||
### Showing icons
|
||||
@@ -136,7 +263,7 @@ You can add the count of all your private contributions to the total commits cou
|
||||
To enable icons, you can pass `show_icons=true` in the query param, like so:
|
||||
|
||||
```md
|
||||

|
||||
?username=anuraghazra&show_icons=true
|
||||
```
|
||||
|
||||
### Themes
|
||||
@@ -146,7 +273,7 @@ With inbuilt themes, you can customize the look of the card without doing any [m
|
||||
Use `&theme=THEME_NAME` parameter like so :
|
||||
|
||||
```md
|
||||

|
||||
?username=anuraghazra&show_icons=true&theme=radical
|
||||
```
|
||||
|
||||
#### All inbuilt themes
|
||||
@@ -169,7 +296,7 @@ Since GitHub will re-upload the cards and serve them from their [CDN](https://do
|
||||
We have included a `transparent` theme that has a transparent background. This theme is optimized to look good on GitHub's dark and light default themes. You can enable this theme using the `&theme=transparent` parameter like so:
|
||||
|
||||
```md
|
||||

|
||||
?username=anuraghazra&show_icons=true&theme=transparent
|
||||
```
|
||||
|
||||
<details>
|
||||
@@ -184,7 +311,7 @@ We have included a `transparent` theme that has a transparent background. This t
|
||||
You can use the `bg_color` parameter to make any of [the available themes](./themes/README.md) transparent. This is done by setting the `bg_color` to a colour with a transparent alpha channel (i.e. `bg_color=00000000`):
|
||||
|
||||
```md
|
||||

|
||||
?username=anuraghazra&show_icons=true&bg_color=00000000
|
||||
```
|
||||
|
||||
<details>
|
||||
@@ -199,8 +326,8 @@ You can use the `bg_color` parameter to make any of [the available themes](./the
|
||||
You can use [GitHub's theme context](https://github.blog/changelog/2021-11-24-specify-theme-context-for-images-in-markdown/) tags to switch the theme based on the user GitHub theme automatically. This is done by appending `#gh-dark-mode-only` or `#gh-light-mode-only` to the end of an image URL. This tag will define whether the image specified in the markdown is only shown to viewers using a light or a dark GitHub theme:
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats#gh-dark-mode-only)
|
||||
[](https://github.com/anuraghazra/github-readme-stats#gh-light-mode-only)
|
||||
[](https://github.com/anuraghazra/github-readme-stats#gh-dark-mode-only)
|
||||
[](https://github.com/anuraghazra/github-readme-stats#gh-light-mode-only)
|
||||
```
|
||||
|
||||
<details>
|
||||
@@ -218,14 +345,14 @@ You can use [GitHub's new media feature](https://github.blog/changelog/2022-05-1
|
||||
```html
|
||||
<picture>
|
||||
<source
|
||||
srcset="https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true&theme=dark"
|
||||
srcset="https://DARK_SVG_URL/"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
/>
|
||||
<source
|
||||
srcset="https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true"
|
||||
srcset="https://LIGHT_SVG_URL/"
|
||||
media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
|
||||
/>
|
||||
<img src="https://github-readme-stats.vercel.app/api?username=anuraghazra&show_icons=true" />
|
||||
<img src="https://DEFAULT_SVG_URL/" />
|
||||
</picture>
|
||||
```
|
||||
|
||||
@@ -248,7 +375,7 @@ You can use [GitHub's new media feature](https://github.blog/changelog/2022-05-1
|
||||
|
||||
### Customization
|
||||
|
||||
You can customize the appearance of your `Stats Card` or `Repo Card` however you wish with URL parameters.
|
||||
You can customize the appearance of your cards however you wish with these parameters.
|
||||
|
||||
#### Common Options
|
||||
|
||||
@@ -264,7 +391,7 @@ You can customize the appearance of your `Stats Card` or `Repo Card` however you
|
||||
- `border_radius` - Corner rounding on the card. Default: `4.5`.
|
||||
|
||||
> **Warning**
|
||||
> We use caching to decrease the load on our servers (see <https://github.com/anuraghazra/github-readme-stats/issues/1471#issuecomment-1271551425>). Our cards have a default cache of 4 hours (14400 seconds). Also, note that the cache is clamped to a minimum of 4 hours and a maximum of 24 hours.
|
||||
> We use caching to decrease the load on our servers (see <https://github.com/anuraghazra/github-readme-stats/issues/1471#issuecomment-1271551425>). Our cards have a default cache of 4 hours (14400 seconds). Also, note that the cache is clamped to a minimum of 4 hours and a maximum of 24 hours. This is why we recommend using our GitHub Action :)
|
||||
|
||||
##### Gradient in bg_color
|
||||
|
||||
@@ -305,6 +432,7 @@ You can provide multiple comma-separated values in the bg_color option to render
|
||||
- `exclude_repo` - Exclude specified repositories _(Comma-separated values)_. Default: `[] (blank array)`.
|
||||
- `custom_title` - Sets a custom title for the card _(string)_. Default `Most Used Languages`.
|
||||
- `disable_animations` - Disables all animations in the card _(boolean)_. Default: `false`.
|
||||
- `hide_progress` - It uses the compact layout option, hides percentages, and removes the bars. Default: `false`.
|
||||
|
||||
> **Warning**
|
||||
> Language names should be URI-escaped, as specified in [Percent Encoding](https://en.wikipedia.org/wiki/Percent-encoding)
|
||||
@@ -333,13 +461,7 @@ Yay! You are no longer limited to 6 pinned repositories.
|
||||
|
||||
### Usage
|
||||
|
||||
Copy-paste this code into your readme and change the links.
|
||||
|
||||
Endpoint: `api/pin?username=anuraghazra&repo=github-readme-stats`
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
Use the `api/pin` endpoint with the API or the `card: 'repo'` option with the GitHub Action.
|
||||
|
||||
### Demo
|
||||
|
||||
@@ -354,24 +476,18 @@ Use [show_owner](#customization) variable to include the repo's owner username
|
||||
The top languages card shows a GitHub user's most frequently used top language.
|
||||
|
||||
> **Note**
|
||||
> Top Languages does not indicate my skill level or anything like that; it's a GitHub metric to determine which languages have the most code on GitHub. It is a new feature of github-readme-stats.
|
||||
> Top Languages does not indicate skill level or anything like that; it's a GitHub metric to determine which languages have the most code on GitHub.
|
||||
|
||||
### Usage
|
||||
|
||||
Copy-paste this code into your readme and change the links.
|
||||
|
||||
Endpoint: `api/top-langs?username=anuraghazra`
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
```
|
||||
Use the `api/top-langs` endpoint with the API or the `card: 'top-langs'` option with the GitHub Action.
|
||||
|
||||
### Exclude individual repositories
|
||||
|
||||
You can use the `&exclude_repo=repo1,repo2` parameter to exclude individual repositories.
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
?username=anuraghazra&exclude_repo=github-readme-stats,anuraghazra.github.io
|
||||
```
|
||||
|
||||
### Hide individual languages
|
||||
@@ -379,7 +495,7 @@ You can use the `&exclude_repo=repo1,repo2` parameter to exclude individual repo
|
||||
You can use `&hide=language1,language2` parameter to hide individual languages.
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
?username=anuraghazra&hide=javascript,html
|
||||
```
|
||||
|
||||
### Show more languages
|
||||
@@ -387,7 +503,7 @@ You can use `&hide=language1,language2` parameter to hide individual languages.
|
||||
You can use the `&langs_count=` option to increase or decrease the number of languages shown on the card. Valid values are integers between 1 and 10 (inclusive), and the default is 5.
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
?username=anuraghazra&langs_count=8
|
||||
```
|
||||
|
||||
### Compact Language Card Layout
|
||||
@@ -395,7 +511,15 @@ You can use the `&langs_count=` option to increase or decrease the number of lan
|
||||
You can use the `&layout=compact` option to change the card design.
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
?username=anuraghazra&layout=compact
|
||||
```
|
||||
|
||||
### Hide Progress Bars
|
||||
|
||||
You can use the `&hide_progress=true` option to hide the percentages and the progress bars (layout will be automatically set to `compact`).
|
||||
|
||||
```md
|
||||
?username=anuraghazra&hide_progress=true
|
||||
```
|
||||
|
||||
### Demo
|
||||
@@ -406,17 +530,33 @@ You can use the `&layout=compact` option to change the card design.
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
- Hidden progress bars
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
|
||||
# Wakatime Week Stats
|
||||
|
||||
### Usage
|
||||
|
||||
Use the `api/wkatime` endpoint with the API or the `card: 'wakatime'` option with the GitHub Action.
|
||||
|
||||
Change the `?username=` value to your [Wakatime](https://wakatime.com) username.
|
||||
|
||||
```md
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
?username=willianrod
|
||||
```
|
||||
|
||||
> **Note**:
|
||||
> Please be aware that we currently only show data from Wakatime profiles that are public.
|
||||
|
||||
### Compact Wakatime Card Layout
|
||||
|
||||
You can use the `&layout=compact` option to change the card design.
|
||||
|
||||
```md
|
||||
?username=willianrod&layout=compact
|
||||
```
|
||||
|
||||
### Demo
|
||||
|
||||
[](https://github.com/anuraghazra/github-readme-stats)
|
||||
@@ -489,14 +629,14 @@ By default, GitHub does not lay out the cards side by side. To do that, you can
|
||||
|
||||
```html
|
||||
<a href="https://github.com/anuraghazra/github-readme-stats">
|
||||
<img align="center" src="https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats" />
|
||||
<img align="center" src="https://SVG_URL/" />
|
||||
</a>
|
||||
<a href="https://github.com/anuraghazra/convoychat">
|
||||
<img align="center" src="https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra&repo=convoychat" />
|
||||
<img align="center" src="https://SVG_URL/" />
|
||||
</a>
|
||||
```
|
||||
|
||||
## Deploy on your own Vercel instance
|
||||
## Deploy your own Vercel instance
|
||||
|
||||
#### :film_projector: [Check Out Step By Step Video Tutorial By @codeSTACKr](https://youtu.be/n6d4KHSKqGk?t=107)
|
||||
|
||||
@@ -535,7 +675,7 @@ Since the GitHub API only allows 5k requests per hour, my `https://github-readme
|
||||
|
||||
### Keep your fork up to date
|
||||
|
||||
You can keep your fork, and thus your private Vercel instance up to date with the upstream using GitHubs' [Sync Fork button](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork). You can also use the [pull](https://github.com/wei/pull) package created by [@wei](https://github.com/wei) to automate this process.
|
||||
You can keep your fork, and thus your private Vercel instance up to date with the upstream using GitHub's [Sync Fork button](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork). You can also use the [pull](https://github.com/wei/pull) package created by [@wei](https://github.com/wei) to automate this process.
|
||||
|
||||
## :sparkling_heart: Support the project
|
||||
|
||||
|
||||
+25
-11
@@ -12,6 +12,11 @@ import {
|
||||
import { getStyles } from "../getStyles.js";
|
||||
import { statCardLocales } from "../translations.js";
|
||||
|
||||
const CARD_MIN_WIDTH = 287;
|
||||
const CARD_DEFAULT_WIDTH = 287;
|
||||
const RANK_CARD_MIN_WIDTH = 420;
|
||||
const RANK_CARD_DEFAULT_WIDTH = 450;
|
||||
|
||||
/**
|
||||
* Create a stats card text item.
|
||||
*
|
||||
@@ -218,11 +223,17 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
When hide_rank=false, the minimum card_width is 340 px + the icon width (if show_icons=true).
|
||||
Numbers are picked by looking at existing dimensions on production.
|
||||
*/
|
||||
const iconWidth = show_icons ? 16 : 0;
|
||||
const minCardWidth = hide_rank
|
||||
? clampValue(50 /* padding */ + calculateTextWidth() * 2, 270, Infinity)
|
||||
: 340 + iconWidth;
|
||||
const defaultCardWidth = hide_rank ? 270 : 495;
|
||||
const iconWidth = show_icons ? 16 + /* padding */ 1 : 0;
|
||||
const minCardWidth =
|
||||
(hide_rank
|
||||
? clampValue(
|
||||
50 /* padding */ + calculateTextWidth() * 2,
|
||||
CARD_MIN_WIDTH,
|
||||
Infinity,
|
||||
)
|
||||
: RANK_CARD_MIN_WIDTH) + iconWidth;
|
||||
const defaultCardWidth =
|
||||
(hide_rank ? CARD_DEFAULT_WIDTH : RANK_CARD_DEFAULT_WIDTH) + iconWidth;
|
||||
let width = isNaN(card_width) ? defaultCardWidth : card_width;
|
||||
if (width < minCardWidth) {
|
||||
width = minCardWidth;
|
||||
@@ -251,18 +262,21 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
|
||||
|
||||
/**
|
||||
* Calculates the right rank circle translation values such that the rank circle
|
||||
* keeps respecting the padding.
|
||||
* keeps respecting the following padding:
|
||||
*
|
||||
* width > 450: The default left padding of 50 px will be used.
|
||||
* width < 450: The left and right padding will shrink equally.
|
||||
* width > RANK_CARD_DEFAULT_WIDTH: The default right padding of 70 px will be used.
|
||||
* width < RANK_CARD_DEFAULT_WIDTH: The left and right padding will be enlarged
|
||||
* equally from a certain minimum at RANK_CARD_MIN_WIDTH.
|
||||
*
|
||||
* @returns {number} - Rank circle translation value.
|
||||
*/
|
||||
const calculateRankXTranslation = () => {
|
||||
if (width < 450) {
|
||||
return width - 95 + (45 * (450 - 340)) / 110;
|
||||
const minXTranslation = RANK_CARD_MIN_WIDTH + iconWidth - 70;
|
||||
if (width > RANK_CARD_DEFAULT_WIDTH) {
|
||||
const xMaxExpansion = minXTranslation + (450 - minCardWidth) / 2;
|
||||
return xMaxExpansion + width - RANK_CARD_DEFAULT_WIDTH;
|
||||
} else {
|
||||
return width - 95;
|
||||
return minXTranslation + (width - minCardWidth) / 2;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -76,10 +76,11 @@ const createProgressTextNode = ({ width, color, name, progress, index }) => {
|
||||
* @param {object} props Function properties.
|
||||
* @param {Lang} props.lang Programming language object.
|
||||
* @param {number} props.totalSize Total size of all languages.
|
||||
* @param {boolean} props.hideProgress Whether to hide percentage.
|
||||
* @param {number} props.index Index of the programming language.
|
||||
* @returns {string} Compact layout programming language SVG node.
|
||||
*/
|
||||
const createCompactLangNode = ({ lang, totalSize, index }) => {
|
||||
const createCompactLangNode = ({ lang, totalSize, hideProgress, index }) => {
|
||||
const percentage = ((lang.size / totalSize) * 100).toFixed(2);
|
||||
const staggerDelay = (index + 3) * 150;
|
||||
const color = lang.color || "#858585";
|
||||
@@ -88,7 +89,7 @@ const createCompactLangNode = ({ lang, totalSize, index }) => {
|
||||
<g class="stagger" style="animation-delay: ${staggerDelay}ms">
|
||||
<circle cx="5" cy="6" r="5" fill="${color}" />
|
||||
<text data-testid="lang-name" x="15" y="10" class='lang-name'>
|
||||
${lang.name} ${percentage}%
|
||||
${lang.name} ${hideProgress ? "" : percentage + "%"}
|
||||
</text>
|
||||
</g>
|
||||
`;
|
||||
@@ -100,9 +101,10 @@ const createCompactLangNode = ({ lang, totalSize, index }) => {
|
||||
* @param {object[]} props Function properties.
|
||||
* @param {Lang[]} props.langs Array of programming languages.
|
||||
* @param {number} props.totalSize Total size of all languages.
|
||||
* @param {boolean} props.hideProgress Whether to hide percentage.
|
||||
* @returns {string} Programming languages SVG node.
|
||||
*/
|
||||
const createLanguageTextNode = ({ langs, totalSize }) => {
|
||||
const createLanguageTextNode = ({ langs, totalSize, hideProgress }) => {
|
||||
const longestLang = getLongestLang(langs);
|
||||
const chunked = chunkArray(langs, langs.length / 2);
|
||||
const layouts = chunked.map((array) => {
|
||||
@@ -111,6 +113,7 @@ const createLanguageTextNode = ({ langs, totalSize }) => {
|
||||
createCompactLangNode({
|
||||
lang,
|
||||
totalSize,
|
||||
hideProgress,
|
||||
index,
|
||||
}),
|
||||
);
|
||||
@@ -160,9 +163,10 @@ const renderNormalLayout = (langs, width, totalLanguageSize) => {
|
||||
* @param {Lang[]} langs Array of programming languages.
|
||||
* @param {number} width Card width.
|
||||
* @param {number} totalLanguageSize Total size of all languages.
|
||||
* @param {boolean} hideProgress Whether to hide progress bar.
|
||||
* @returns {string} Compact layout card SVG object.
|
||||
*/
|
||||
const renderCompactLayout = (langs, width, totalLanguageSize) => {
|
||||
const renderCompactLayout = (langs, width, totalLanguageSize, hideProgress) => {
|
||||
const paddingRight = 50;
|
||||
const offsetWidth = width - paddingRight;
|
||||
// progressOffset holds the previous language's width and used to offset the next language
|
||||
@@ -193,15 +197,21 @@ const renderCompactLayout = (langs, width, totalLanguageSize) => {
|
||||
.join("");
|
||||
|
||||
return `
|
||||
<mask id="rect-mask">
|
||||
${
|
||||
!hideProgress
|
||||
? `
|
||||
<mask id="rect-mask">
|
||||
<rect x="0" y="0" width="${offsetWidth}" height="8" fill="white" rx="5"/>
|
||||
</mask>
|
||||
${compactProgressBar}
|
||||
|
||||
<g transform="translate(0, 25)">
|
||||
`
|
||||
: ""
|
||||
}
|
||||
<g transform="translate(0, ${hideProgress ? "0" : "25"})">
|
||||
${createLanguageTextNode({
|
||||
langs,
|
||||
totalSize: totalLanguageSize,
|
||||
hideProgress: hideProgress,
|
||||
})}
|
||||
</g>
|
||||
`;
|
||||
@@ -276,6 +286,7 @@ const renderTopLanguages = (topLangs, options = {}) => {
|
||||
text_color,
|
||||
bg_color,
|
||||
hide,
|
||||
hide_progress,
|
||||
theme,
|
||||
layout,
|
||||
custom_title,
|
||||
@@ -305,11 +316,17 @@ const renderTopLanguages = (topLangs, options = {}) => {
|
||||
let height = calculateNormalLayoutHeight(langs.length);
|
||||
|
||||
let finalLayout = "";
|
||||
if (layout === "compact") {
|
||||
if (layout === "compact" || hide_progress == true) {
|
||||
width = width + 50; // padding
|
||||
height = calculateCompactLayoutHeight(langs.length);
|
||||
height =
|
||||
calculateCompactLayoutHeight(langs.length) + (hide_progress ? -25 : 0);
|
||||
|
||||
finalLayout = renderCompactLayout(langs, width, totalLanguageSize);
|
||||
finalLayout = renderCompactLayout(
|
||||
langs,
|
||||
width,
|
||||
totalLanguageSize,
|
||||
hide_progress,
|
||||
);
|
||||
} else {
|
||||
finalLayout = renderNormalLayout(langs, width, totalLanguageSize);
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -38,6 +38,7 @@ export type TopLangOptions = CommonOptions & {
|
||||
custom_title: string;
|
||||
langs_count: number;
|
||||
disable_animations: boolean;
|
||||
hide_progress: boolean;
|
||||
};
|
||||
|
||||
type WakaTimeOptions = CommonOptions & {
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
/**
|
||||
* @file GRS Exception/Errors.
|
||||
*/
|
||||
|
||||
const SECONDARY_ERROR_MESSAGES = {
|
||||
MAX_RETRY:
|
||||
"Please add an env variable called PAT_1 with your github token in vercel",
|
||||
USER_NOT_FOUND: "Make sure the provided username is not an organization",
|
||||
GRAPHQL_ERROR: "Please try again later",
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom error class to handle custom GRS errors.
|
||||
*
|
||||
* @extends Error
|
||||
*/
|
||||
class CustomError extends Error {
|
||||
/**
|
||||
* @param {string} message Error message.
|
||||
* @param {string} type Error type.
|
||||
*/
|
||||
constructor(message, type) {
|
||||
super(message);
|
||||
this.type = type;
|
||||
this.secondaryMessage = SECONDARY_ERROR_MESSAGES[type] || type;
|
||||
}
|
||||
|
||||
static MAX_RETRY = "MAX_RETRY";
|
||||
static USER_NOT_FOUND = "USER_NOT_FOUND";
|
||||
static GRAPHQL_ERROR = "GRAPHQL_ERROR";
|
||||
}
|
||||
|
||||
/**
|
||||
* Missing query parameter class.
|
||||
*
|
||||
* @extends Error
|
||||
*/
|
||||
class MissingParamError extends Error {
|
||||
/**
|
||||
* @param {string[]} missedParams
|
||||
* @param {string?=} secondaryMessage
|
||||
*/
|
||||
constructor(missedParams, secondaryMessage) {
|
||||
const msg = `Missing params ${missedParams
|
||||
.map((p) => `"${p}"`)
|
||||
.join(", ")} make sure you pass the parameters in URL`;
|
||||
super(msg);
|
||||
this.missedParams = missedParams;
|
||||
this.secondaryMessage = secondaryMessage;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HttpException class.
|
||||
*
|
||||
* @extends Error
|
||||
*/
|
||||
class HttpException extends Error {
|
||||
/**
|
||||
* Create a HttpException.
|
||||
*
|
||||
* @param {number} statusCode - The status code.
|
||||
* @param {string} message - The error message.
|
||||
* @param {string[]} errors - The errors.
|
||||
*/
|
||||
constructor(
|
||||
statusCode,
|
||||
message = "Exception occurred during the processing of HTTP requests.",
|
||||
errors = [],
|
||||
) {
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
this.errors = errors;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
SECONDARY_ERROR_MESSAGES,
|
||||
CustomError,
|
||||
MissingParamError,
|
||||
HttpException,
|
||||
};
|
||||
+2
-1
@@ -21,9 +21,10 @@ export {
|
||||
wrapTextMultiline,
|
||||
logger,
|
||||
CONSTANTS,
|
||||
CustomError,
|
||||
MissingParamError,
|
||||
measureText,
|
||||
lowercaseTrim,
|
||||
chunkArray,
|
||||
parseEmojis,
|
||||
} from "./utils.js";
|
||||
export { CustomError, MissingParamError, HttpException } from "./exceptions.js";
|
||||
|
||||
@@ -102,6 +102,7 @@
|
||||
"Csound Score": "#1a1a1a",
|
||||
"Cuda": "#3A4E3A",
|
||||
"Curry": "#531242",
|
||||
"Cypher": "#34c0eb",
|
||||
"Cython": "#fedf5b",
|
||||
"D": "#ba595e",
|
||||
"DM": "#447265",
|
||||
@@ -124,6 +125,7 @@
|
||||
"Earthly": "#2af0ff",
|
||||
"Easybuild": "#069406",
|
||||
"Ecere Projects": "#913960",
|
||||
"Ecmarkup": "#eb8131",
|
||||
"EditorConfig": "#fff1f2",
|
||||
"Eiffel": "#4d6977",
|
||||
"Elixir": "#6e4a7e",
|
||||
@@ -215,6 +217,7 @@
|
||||
"Idris": "#b30000",
|
||||
"Ignore List": "#000000",
|
||||
"ImageJ Macro": "#99AAFF",
|
||||
"Imba": "#16cec6",
|
||||
"Inno Setup": "#264b99",
|
||||
"Io": "#a9188d",
|
||||
"Ioke": "#078193",
|
||||
@@ -286,6 +289,7 @@
|
||||
"Mathematica": "#dd1100",
|
||||
"Max": "#c4a79c",
|
||||
"Mercury": "#ff2b2b",
|
||||
"Mermaid": "#ff3670",
|
||||
"Meson": "#007800",
|
||||
"Metal": "#8f14e9",
|
||||
"MiniYAML": "#ff1111",
|
||||
@@ -318,6 +322,10 @@
|
||||
"Nu": "#c9df40",
|
||||
"NumPy": "#9C8AF9",
|
||||
"Nunjucks": "#3d8137",
|
||||
"OASv2-json": "#85ea2d",
|
||||
"OASv2-yaml": "#85ea2d",
|
||||
"OASv3-json": "#85ea2d",
|
||||
"OASv3-yaml": "#85ea2d",
|
||||
"OCaml": "#3be133",
|
||||
"ObjectScript": "#424893",
|
||||
"Objective-C": "#438eff",
|
||||
@@ -327,14 +335,18 @@
|
||||
"Omgrofl": "#cabbff",
|
||||
"Opal": "#f7ede0",
|
||||
"Open Policy Agent": "#7d9199",
|
||||
"OpenAPI Specification v2": "#85ea2d",
|
||||
"OpenAPI Specification v3": "#85ea2d",
|
||||
"OpenCL": "#ed2e2d",
|
||||
"OpenEdge ABL": "#5ce600",
|
||||
"OpenQASM": "#AA70FF",
|
||||
"OpenSCAD": "#e5cd45",
|
||||
"Option List": "#476732",
|
||||
"Org": "#77aa99",
|
||||
"Oxygene": "#cdd0e3",
|
||||
"Oz": "#fab738",
|
||||
"P4": "#7055b5",
|
||||
"PDDL": "#0d00ff",
|
||||
"PEG.js": "#234d6b",
|
||||
"PHP": "#4F5D95",
|
||||
"PLSQL": "#dad8d8",
|
||||
@@ -351,6 +363,7 @@
|
||||
"PigLatin": "#fcd7de",
|
||||
"Pike": "#005390",
|
||||
"PogoScript": "#d80074",
|
||||
"Polar": "#ae81ff",
|
||||
"Portugol": "#f8bd00",
|
||||
"PostCSS": "#dc3a0c",
|
||||
"PostScript": "#da291c",
|
||||
@@ -414,6 +427,7 @@
|
||||
"Sass": "#a53b70",
|
||||
"Scala": "#c22d40",
|
||||
"Scaml": "#bd181a",
|
||||
"Scenic": "#fdc700",
|
||||
"Scheme": "#1e4aec",
|
||||
"Scilab": "#ca0f21",
|
||||
"Self": "#0579aa",
|
||||
@@ -421,6 +435,7 @@
|
||||
"Shell": "#89e051",
|
||||
"ShellCheck Config": "#cecfcb",
|
||||
"Shen": "#120F14",
|
||||
"Simple File Verification": "#C9BFED",
|
||||
"Singularity": "#64E6AD",
|
||||
"Slash": "#007eff",
|
||||
"Slice": "#003fa2",
|
||||
@@ -428,6 +443,7 @@
|
||||
"SmPL": "#c94949",
|
||||
"Smalltalk": "#596706",
|
||||
"Smarty": "#f0c040",
|
||||
"Smithy": "#c44536",
|
||||
"Solidity": "#AA6746",
|
||||
"SourcePawn": "#f69e1d",
|
||||
"Squirrel": "#800000",
|
||||
@@ -478,6 +494,7 @@
|
||||
"Vim Script": "#199f4b",
|
||||
"Vim Snippet": "#199f4b",
|
||||
"Visual Basic .NET": "#945db7",
|
||||
"Visual Basic 6.0": "#2c6353",
|
||||
"Volt": "#1F1F1F",
|
||||
"Vue": "#41b883",
|
||||
"Vyper": "#2980b9",
|
||||
@@ -514,6 +531,7 @@
|
||||
"fish": "#4aae47",
|
||||
"hoon": "#00b171",
|
||||
"jq": "#c7254e",
|
||||
"just": "#384d54",
|
||||
"kvlang": "#1da6e0",
|
||||
"mIRC Script": "#3d57c3",
|
||||
"mcfunction": "#E22837",
|
||||
|
||||
+4
-13
@@ -1,8 +1,4 @@
|
||||
import { logger } from "./utils.js";
|
||||
import {
|
||||
CustomError,
|
||||
HttpException,
|
||||
} from "./exceptions.js";
|
||||
import { CustomError, logger } from "./utils.js";
|
||||
|
||||
// Script variables.
|
||||
const PATs = Object.keys(process.env).filter((key) =>
|
||||
@@ -20,13 +16,8 @@ const RETRIES = PATs ? PATs : 7;
|
||||
* @returns Promise<retryer>
|
||||
*/
|
||||
const retryer = async (fetcher, variables, retries = 0) => {
|
||||
// if (retries > RETRIES) {
|
||||
if (true) { // FIXME: Test out error
|
||||
throw new HttpException(
|
||||
statusCode=500,
|
||||
message=`Max GraphQL retries exceeded. Please add an env variable called PAT_1 with your github token in vercel.`,
|
||||
errors=[new CustomError("Maximum retries exceeded", CustomError.MAX_RETRY)]
|
||||
);
|
||||
if (retries > RETRIES) {
|
||||
throw new CustomError("Maximum retries exceeded", CustomError.MAX_RETRY);
|
||||
}
|
||||
try {
|
||||
// try to fetch with the first token since RETRIES is 0 index i'm adding +1
|
||||
@@ -69,5 +60,5 @@ const retryer = async (fetcher, variables, retries = 0) => {
|
||||
}
|
||||
};
|
||||
|
||||
export { retryer, RETRIES };
|
||||
export { retryer };
|
||||
export default retryer;
|
||||
|
||||
+63
-12
@@ -22,7 +22,8 @@ const renderError = (message, secondaryMessage = "") => {
|
||||
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #252525 }
|
||||
.gray { fill: #858585 }
|
||||
</style>
|
||||
<rect x="0.5" y="0.5" width="${ERROR_CARD_LENGTH - 1
|
||||
<rect x="0.5" y="0.5" width="${
|
||||
ERROR_CARD_LENGTH - 1
|
||||
}" height="99%" rx="4.5" fill="#FFFEFE" stroke="#E4E2E2"/>
|
||||
<text x="25" y="45" class="text">Something went wrong! file an issue at https://tiny.one/readme-stats</text>
|
||||
<text data-testid="message" x="25" y="55" class="text small">
|
||||
@@ -287,28 +288,62 @@ const wrapTextMultiline = (text, width = 59, maxLines = 3) => {
|
||||
return multiLineText;
|
||||
};
|
||||
|
||||
const noop = () => { };
|
||||
const noop = () => {};
|
||||
// return console instance based on the environment
|
||||
const logger =
|
||||
process.env.NODE_ENV !== "test" ? console : { log: noop, error: noop };
|
||||
|
||||
// Cache settings.
|
||||
const CARD_CACHE_SECONDS = 14400;
|
||||
const ERROR_CACHE_SECONDS = 600;
|
||||
|
||||
const CONSTANTS = {
|
||||
ONE_MINUTE: 60,
|
||||
FIVE_MINUTES: 300,
|
||||
TEN_MINUTES: 600,
|
||||
FIFTEEN_MINUTES: 900,
|
||||
THIRTY_MINUTES: 1800,
|
||||
TWO_HOURS: 7200,
|
||||
FOUR_HOURS: 14400,
|
||||
ONE_DAY: 86400,
|
||||
CARD_CACHE_SECONDS: CARD_CACHE_SECONDS,
|
||||
ERROR_CACHE_SECONDS: ERROR_CACHE_SECONDS,
|
||||
};
|
||||
|
||||
const SECONDARY_ERROR_MESSAGES = {
|
||||
MAX_RETRY:
|
||||
"Please add an env variable called PAT_1 with your github token in vercel",
|
||||
USER_NOT_FOUND: "Make sure the provided username is not an organization",
|
||||
GRAPHQL_ERROR: "Please try again later",
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom error class to handle custom GRS errors.
|
||||
*/
|
||||
class CustomError extends Error {
|
||||
/**
|
||||
* @param {string} message Error message.
|
||||
* @param {string} type Error type.
|
||||
*/
|
||||
constructor(message, type) {
|
||||
super(message);
|
||||
this.type = type;
|
||||
this.secondaryMessage = SECONDARY_ERROR_MESSAGES[type] || type;
|
||||
}
|
||||
|
||||
static MAX_RETRY = "MAX_RETRY";
|
||||
static USER_NOT_FOUND = "USER_NOT_FOUND";
|
||||
static GRAPHQL_ERROR = "GRAPHQL_ERROR";
|
||||
}
|
||||
|
||||
/**
|
||||
* Missing query parameter class.
|
||||
*/
|
||||
class MissingParamError extends Error {
|
||||
/**
|
||||
* @param {string[]} missedParams
|
||||
* @param {string?=} secondaryMessage
|
||||
*/
|
||||
constructor(missedParams, secondaryMessage) {
|
||||
const msg = `Missing params ${missedParams
|
||||
.map((p) => `"${p}"`)
|
||||
.join(", ")} make sure you pass the parameters in URL`;
|
||||
super(msg);
|
||||
this.missedParams = missedParams;
|
||||
this.secondaryMessage = secondaryMessage;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve text length.
|
||||
*
|
||||
@@ -389,6 +424,19 @@ const parseEmojis = (str) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get diff in minutes
|
||||
* @param {Date} d1
|
||||
* @param {Date} d2
|
||||
* @returns {number}
|
||||
*/
|
||||
const dateDiff = (d1, d2) => {
|
||||
const date1 = new Date(d1);
|
||||
const date2 = new Date(d2);
|
||||
const diff = date1.getTime() - date2.getTime();
|
||||
return Math.round(diff / (1000 * 60));
|
||||
};
|
||||
|
||||
export {
|
||||
ERROR_CARD_LENGTH,
|
||||
renderError,
|
||||
@@ -406,8 +454,11 @@ export {
|
||||
wrapTextMultiline,
|
||||
logger,
|
||||
CONSTANTS,
|
||||
CustomError,
|
||||
MissingParamError,
|
||||
measureText,
|
||||
lowercaseTrim,
|
||||
chunkArray,
|
||||
parseEmojis,
|
||||
dateDiff,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @ts-check
|
||||
import { retryer } from "../common/retryer.js";
|
||||
import { request } from "../common/utils.js";
|
||||
import { MissingParamError } from "../common/exceptions.js";
|
||||
import { MissingParamError, request } from "../common/utils.js";
|
||||
|
||||
/**
|
||||
* Repo data fetcher.
|
||||
|
||||
@@ -5,11 +5,12 @@ import githubUsernameRegex from "github-username-regex";
|
||||
import { calculateRank } from "../calculateRank.js";
|
||||
import { retryer } from "../common/retryer.js";
|
||||
import {
|
||||
CustomError,
|
||||
logger,
|
||||
MissingParamError,
|
||||
request,
|
||||
wrapTextMultiline,
|
||||
} from "../common/utils.js";
|
||||
import { CustomError, MissingParamError } from "../common/exceptions.js";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
// @ts-check
|
||||
import { retryer } from "../common/retryer.js";
|
||||
import {
|
||||
CustomError,
|
||||
logger,
|
||||
MissingParamError,
|
||||
request,
|
||||
wrapTextMultiline,
|
||||
} from "../common/utils.js";
|
||||
import { CustomError, MissingParamError } from "../common/exceptions.js";
|
||||
|
||||
/**
|
||||
* Top languages fetcher object.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios from "axios";
|
||||
import { MissingParamError } from "../common/exceptions.js";
|
||||
import { MissingParamError } from "../common/utils.js";
|
||||
|
||||
/**
|
||||
* WakaTime data fetcher.
|
||||
|
||||
+2
-7
@@ -171,18 +171,13 @@ describe("Test /api/", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should set shorter cache when error", async () => {
|
||||
it("should not store cache when error", async () => {
|
||||
const { req, res } = faker({}, error);
|
||||
await api(req, res);
|
||||
|
||||
expect(res.setHeader.mock.calls).toEqual([
|
||||
["Content-Type", "image/svg+xml"],
|
||||
[
|
||||
"Cache-Control",
|
||||
`max-age=${CONSTANTS.ERROR_CACHE_SECONDS / 2}, s-maxage=${
|
||||
CONSTANTS.ERROR_CACHE_SECONDS
|
||||
}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
|
||||
],
|
||||
["Cache-Control", `no-cache, no-store, must-revalidate`],
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* @file Tests for the status/pat-info cloud function.
|
||||
*/
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
import { jest } from "@jest/globals";
|
||||
import axios from "axios";
|
||||
import MockAdapter from "axios-mock-adapter";
|
||||
import patInfo, { RATE_LIMIT_SECONDS } from "../api/status/pat-info.js";
|
||||
|
||||
const mock = new MockAdapter(axios);
|
||||
|
||||
const successData = {
|
||||
data: {
|
||||
rateLimit: {
|
||||
remaining: 4986,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const faker = (query) => {
|
||||
const req = {
|
||||
query: { ...query },
|
||||
};
|
||||
const res = {
|
||||
setHeader: jest.fn(),
|
||||
send: jest.fn(),
|
||||
};
|
||||
|
||||
return { req, res };
|
||||
};
|
||||
|
||||
const rate_limit_error = {
|
||||
errors: [
|
||||
{
|
||||
type: "RATE_LIMITED",
|
||||
message: "API rate limit exceeded for user ID.",
|
||||
},
|
||||
],
|
||||
data: {
|
||||
rateLimit: {
|
||||
resetAt: Date.now(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const other_error = {
|
||||
errors: [
|
||||
{
|
||||
type: "SOME_ERROR",
|
||||
message: "This is a error",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const bad_credentials_error = {
|
||||
message: "Bad credentials",
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
mock.reset();
|
||||
});
|
||||
|
||||
describe("Test /api/status/pat-info", () => {
|
||||
beforeAll(() => {
|
||||
// reset patenv first so that dotenv doesn't populate them with local envs
|
||||
process.env = {};
|
||||
process.env.PAT_1 = "testPAT1";
|
||||
process.env.PAT_2 = "testPAT2";
|
||||
process.env.PAT_3 = "testPAT3";
|
||||
process.env.PAT_4 = "testPAT4";
|
||||
});
|
||||
|
||||
it("should return only 'validPATs' if all PATs are valid", async () => {
|
||||
mock
|
||||
.onPost("https://api.github.com/graphql")
|
||||
.replyOnce(200, rate_limit_error)
|
||||
.onPost("https://api.github.com/graphql")
|
||||
.reply(200, successData);
|
||||
|
||||
const { req, res } = faker({}, {});
|
||||
await patInfo(req, res);
|
||||
|
||||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
|
||||
expect(res.send).toBeCalledWith(
|
||||
JSON.stringify(
|
||||
{
|
||||
validPATs: ["PAT_2", "PAT_3", "PAT_4"],
|
||||
expiredPATs: [],
|
||||
exhaustedPATs: ["PAT_1"],
|
||||
suspendedPATs: [],
|
||||
errorPATs: [],
|
||||
details: {
|
||||
PAT_1: {
|
||||
status: "exhausted",
|
||||
remaining: 0,
|
||||
resetIn: "0 minutes",
|
||||
},
|
||||
PAT_2: {
|
||||
status: "valid",
|
||||
remaining: 4986,
|
||||
},
|
||||
PAT_3: {
|
||||
status: "valid",
|
||||
remaining: 4986,
|
||||
},
|
||||
PAT_4: {
|
||||
status: "valid",
|
||||
remaining: 4986,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("should return `errorPATs` if a PAT causes an error to be thrown", async () => {
|
||||
mock
|
||||
.onPost("https://api.github.com/graphql")
|
||||
.replyOnce(200, other_error)
|
||||
.onPost("https://api.github.com/graphql")
|
||||
.reply(200, successData);
|
||||
|
||||
const { req, res } = faker({}, {});
|
||||
await patInfo(req, res);
|
||||
|
||||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
|
||||
expect(res.send).toBeCalledWith(
|
||||
JSON.stringify(
|
||||
{
|
||||
validPATs: ["PAT_2", "PAT_3", "PAT_4"],
|
||||
expiredPATs: [],
|
||||
exhaustedPATs: [],
|
||||
suspendedPATs: [],
|
||||
errorPATs: ["PAT_1"],
|
||||
details: {
|
||||
PAT_1: {
|
||||
status: "error",
|
||||
error: {
|
||||
type: "SOME_ERROR",
|
||||
message: "This is a error",
|
||||
},
|
||||
},
|
||||
PAT_2: {
|
||||
status: "valid",
|
||||
remaining: 4986,
|
||||
},
|
||||
PAT_3: {
|
||||
status: "valid",
|
||||
remaining: 4986,
|
||||
},
|
||||
PAT_4: {
|
||||
status: "valid",
|
||||
remaining: 4986,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("should return `expiredPaths` if a PAT returns a 'Bad credentials' error", async () => {
|
||||
mock
|
||||
.onPost("https://api.github.com/graphql")
|
||||
.replyOnce(404, bad_credentials_error)
|
||||
.onPost("https://api.github.com/graphql")
|
||||
.reply(200, successData);
|
||||
|
||||
const { req, res } = faker({}, {});
|
||||
await patInfo(req, res);
|
||||
|
||||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
|
||||
expect(res.send).toBeCalledWith(
|
||||
JSON.stringify(
|
||||
{
|
||||
validPATs: ["PAT_2", "PAT_3", "PAT_4"],
|
||||
expiredPATs: ["PAT_1"],
|
||||
exhaustedPATs: [],
|
||||
suspendedPATs: [],
|
||||
errorPATs: [],
|
||||
details: {
|
||||
PAT_1: {
|
||||
status: "expired",
|
||||
},
|
||||
PAT_2: {
|
||||
status: "valid",
|
||||
remaining: 4986,
|
||||
},
|
||||
PAT_3: {
|
||||
status: "valid",
|
||||
remaining: 4986,
|
||||
},
|
||||
PAT_4: {
|
||||
status: "valid",
|
||||
remaining: 4986,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw an error if something goes wrong", async () => {
|
||||
mock.onPost("https://api.github.com/graphql").networkError();
|
||||
|
||||
const { req, res } = faker({}, {});
|
||||
await patInfo(req, res);
|
||||
|
||||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
|
||||
expect(res.send).toBeCalledWith("Something went wrong: Network Error");
|
||||
});
|
||||
|
||||
it("should have proper cache when no error is thrown", async () => {
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, successData);
|
||||
|
||||
const { req, res } = faker({}, {});
|
||||
await patInfo(req, res);
|
||||
|
||||
expect(res.setHeader.mock.calls).toEqual([
|
||||
["Content-Type", "application/json"],
|
||||
["Cache-Control", `max-age=0, s-maxage=${RATE_LIMIT_SECONDS}`],
|
||||
]);
|
||||
});
|
||||
|
||||
it("should have proper cache when error is thrown", async () => {
|
||||
mock.reset();
|
||||
mock.onPost("https://api.github.com/graphql").networkError();
|
||||
|
||||
const { req, res } = faker({}, {});
|
||||
await patInfo(req, res);
|
||||
|
||||
expect(res.setHeader.mock.calls).toEqual([
|
||||
["Content-Type", "application/json"],
|
||||
["Cache-Control", "no-store"],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -78,16 +78,17 @@ describe("Test renderStatsCard", () => {
|
||||
|
||||
it("should render with custom width set", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats);
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "495");
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "450");
|
||||
|
||||
document.body.innerHTML = renderStatsCard(stats, { card_width: 400 });
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "400");
|
||||
document.body.innerHTML = renderStatsCard(stats, { card_width: 500 });
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "500");
|
||||
});
|
||||
|
||||
it("should render with custom width set and limit minimum width", () => {
|
||||
document.body.innerHTML = renderStatsCard(stats, { card_width: 1 });
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "340");
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "420");
|
||||
|
||||
// Test default minimum card width without rank circle.
|
||||
document.body.innerHTML = renderStatsCard(stats, {
|
||||
card_width: 1,
|
||||
hide_rank: true,
|
||||
@@ -97,6 +98,7 @@ describe("Test renderStatsCard", () => {
|
||||
"305.81250000000006",
|
||||
);
|
||||
|
||||
// Test minimum card width with rank and icons.
|
||||
document.body.innerHTML = renderStatsCard(stats, {
|
||||
card_width: 1,
|
||||
hide_rank: true,
|
||||
@@ -104,22 +106,24 @@ describe("Test renderStatsCard", () => {
|
||||
});
|
||||
expect(document.querySelector("svg")).toHaveAttribute(
|
||||
"width",
|
||||
"305.81250000000006",
|
||||
"322.81250000000006",
|
||||
);
|
||||
|
||||
// Test minimum card width with icons but without rank.
|
||||
document.body.innerHTML = renderStatsCard(stats, {
|
||||
card_width: 1,
|
||||
hide_rank: false,
|
||||
show_icons: true,
|
||||
});
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "356");
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "437");
|
||||
|
||||
// Test minimum card width without icons or rank.
|
||||
document.body.innerHTML = renderStatsCard(stats, {
|
||||
card_width: 1,
|
||||
hide_rank: false,
|
||||
show_icons: false,
|
||||
});
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "340");
|
||||
expect(document.querySelector("svg")).toHaveAttribute("width", "420");
|
||||
});
|
||||
|
||||
it("should render default colors properly", () => {
|
||||
@@ -312,7 +316,7 @@ describe("Test renderStatsCard", () => {
|
||||
|
||||
expect(
|
||||
document.body.getElementsByTagName("svg")[0].getAttribute("width"),
|
||||
).toBe("270");
|
||||
).toBe("287");
|
||||
});
|
||||
|
||||
it("should render translations", () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { jest } from "@jest/globals";
|
||||
import "@testing-library/jest-dom";
|
||||
import { retryer, RETRIES } from "../src/common/retryer.js";
|
||||
import { retryer } from "../src/common/retryer.js";
|
||||
import { logger } from "../src/common/utils.js";
|
||||
|
||||
const fetcher = jest.fn((variables, token) => {
|
||||
@@ -45,7 +45,7 @@ describe("Test Retryer", () => {
|
||||
try {
|
||||
res = await retryer(fetcherFail, {});
|
||||
} catch (err) {
|
||||
expect(fetcherFail).toBeCalledTimes(RETRIES + 1);
|
||||
expect(fetcherFail).toBeCalledTimes(8);
|
||||
expect(err.message).toBe("Maximum retries exceeded");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* @file Tests for the status/up cloud function.
|
||||
*/
|
||||
import { jest } from "@jest/globals";
|
||||
import axios from "axios";
|
||||
import MockAdapter from "axios-mock-adapter";
|
||||
import up, { RATE_LIMIT_SECONDS } from "../api/status/up.js";
|
||||
|
||||
const mock = new MockAdapter(axios);
|
||||
|
||||
const successData = {
|
||||
rateLimit: {
|
||||
remaining: 4986,
|
||||
},
|
||||
};
|
||||
|
||||
const faker = (query) => {
|
||||
const req = {
|
||||
query: { ...query },
|
||||
};
|
||||
const res = {
|
||||
setHeader: jest.fn(),
|
||||
send: jest.fn(),
|
||||
};
|
||||
|
||||
return { req, res };
|
||||
};
|
||||
|
||||
const rate_limit_error = {
|
||||
errors: [
|
||||
{
|
||||
type: "RATE_LIMITED",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const bad_credentials_error = {
|
||||
message: "Bad credentials",
|
||||
};
|
||||
|
||||
const shields_up = {
|
||||
schemaVersion: 1,
|
||||
label: "Public Instance",
|
||||
isError: true,
|
||||
message: "up",
|
||||
color: "brightgreen",
|
||||
};
|
||||
const shields_down = {
|
||||
schemaVersion: 1,
|
||||
label: "Public Instance",
|
||||
isError: true,
|
||||
message: "down",
|
||||
color: "red",
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
mock.reset();
|
||||
});
|
||||
|
||||
describe("Test /api/status/up", () => {
|
||||
it("should return `true` if request was successful", async () => {
|
||||
mock.onPost("https://api.github.com/graphql").replyOnce(200, successData);
|
||||
|
||||
const { req, res } = faker({}, {});
|
||||
await up(req, res);
|
||||
|
||||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
|
||||
expect(res.send).toBeCalledWith(true);
|
||||
});
|
||||
|
||||
it("should return `false` if all PATs are rate limited", async () => {
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, rate_limit_error);
|
||||
|
||||
const { req, res } = faker({}, {});
|
||||
await up(req, res);
|
||||
|
||||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
|
||||
expect(res.send).toBeCalledWith(false);
|
||||
});
|
||||
|
||||
it("should return JSON `true` if request was successful and type='json'", async () => {
|
||||
mock.onPost("https://api.github.com/graphql").replyOnce(200, successData);
|
||||
|
||||
const { req, res } = faker({ type: "json" }, {});
|
||||
await up(req, res);
|
||||
|
||||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
|
||||
expect(res.send).toBeCalledWith({ up: true });
|
||||
});
|
||||
|
||||
it("should return JSON `false` if all PATs are rate limited and type='json'", async () => {
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, rate_limit_error);
|
||||
|
||||
const { req, res } = faker({ type: "json" }, {});
|
||||
await up(req, res);
|
||||
|
||||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
|
||||
expect(res.send).toBeCalledWith({ up: false });
|
||||
});
|
||||
|
||||
it("should return UP shields.io config if request was successful and type='shields'", async () => {
|
||||
mock.onPost("https://api.github.com/graphql").replyOnce(200, successData);
|
||||
|
||||
const { req, res } = faker({ type: "shields" }, {});
|
||||
await up(req, res);
|
||||
|
||||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
|
||||
expect(res.send).toBeCalledWith(shields_up);
|
||||
});
|
||||
|
||||
it("should return DOWN shields.io config if all PATs are rate limited and type='shields'", async () => {
|
||||
mock.onPost("https://api.github.com/graphql").reply(200, rate_limit_error);
|
||||
|
||||
const { req, res } = faker({ type: "shields" }, {});
|
||||
await up(req, res);
|
||||
|
||||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
|
||||
expect(res.send).toBeCalledWith(shields_down);
|
||||
});
|
||||
|
||||
it("should return `true` if the first PAT is rate limited but the second PATs works", async () => {
|
||||
mock
|
||||
.onPost("https://api.github.com/graphql")
|
||||
.replyOnce(200, rate_limit_error)
|
||||
.onPost("https://api.github.com/graphql")
|
||||
.replyOnce(200, successData);
|
||||
|
||||
const { req, res } = faker({}, {});
|
||||
await up(req, res);
|
||||
|
||||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
|
||||
expect(res.send).toBeCalledWith(true);
|
||||
});
|
||||
|
||||
it("should return `true` if the first PAT has 'Bad credentials' but the second PAT works", async () => {
|
||||
mock
|
||||
.onPost("https://api.github.com/graphql")
|
||||
.replyOnce(404, bad_credentials_error)
|
||||
.onPost("https://api.github.com/graphql")
|
||||
.replyOnce(200, successData);
|
||||
|
||||
const { req, res } = faker({}, {});
|
||||
await up(req, res);
|
||||
|
||||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
|
||||
expect(res.send).toBeCalledWith(true);
|
||||
});
|
||||
|
||||
it("should return `false` if all pats have 'Bad credentials'", async () => {
|
||||
mock
|
||||
.onPost("https://api.github.com/graphql")
|
||||
.reply(404, bad_credentials_error);
|
||||
|
||||
const { req, res } = faker({}, {});
|
||||
await up(req, res);
|
||||
|
||||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
|
||||
expect(res.send).toBeCalledWith(false);
|
||||
});
|
||||
|
||||
it("should throw an error if the request fails", async () => {
|
||||
mock.onPost("https://api.github.com/graphql").networkError();
|
||||
|
||||
const { req, res } = faker({}, {});
|
||||
await up(req, res);
|
||||
|
||||
expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
|
||||
expect(res.send).toBeCalledWith(false);
|
||||
});
|
||||
|
||||
it("should have proper cache when no error is thrown", async () => {
|
||||
mock.onPost("https://api.github.com/graphql").replyOnce(200, successData);
|
||||
|
||||
const { req, res } = faker({}, {});
|
||||
await up(req, res);
|
||||
|
||||
expect(res.setHeader.mock.calls).toEqual([
|
||||
["Content-Type", "application/json"],
|
||||
["Cache-Control", `max-age=0, s-maxage=${RATE_LIMIT_SECONDS}`],
|
||||
]);
|
||||
});
|
||||
|
||||
it("should have proper cache when error is thrown", async () => {
|
||||
mock.onPost("https://api.github.com/graphql").networkError();
|
||||
|
||||
const { req, res } = faker({}, {});
|
||||
await up(req, res);
|
||||
|
||||
expect(res.setHeader.mock.calls).toEqual([
|
||||
["Content-Type", "application/json"],
|
||||
["Cache-Control", "no-store"],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -374,6 +374,13 @@ export const themes = {
|
||||
border_color: "170F0C",
|
||||
bg_color: "170F0C",
|
||||
},
|
||||
rose: {
|
||||
title_color: "8d192b",
|
||||
text_color: "862931",
|
||||
icon_color: "B71F36",
|
||||
border_color: "e9d8d4",
|
||||
bg_color: "e9d8d4",
|
||||
},
|
||||
};
|
||||
|
||||
export default themes;
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"functions": {
|
||||
"api/*.js": {
|
||||
"memory": 128,
|
||||
"maxDuration": 30
|
||||
"maxDuration": 10
|
||||
}
|
||||
},
|
||||
"redirects": [
|
||||
|
||||
Reference in New Issue
Block a user