diff --git a/README.md b/README.md index 678a2f81..3c626761 100644 --- a/README.md +++ b/README.md @@ -250,6 +250,7 @@ systemctl status github_metrics * `src/query.graphql` is the GraphQL query which is sent to GitHub API * `src/style.css` contains the style for the generated svg image metrics * `src/template.svg` contains the structure of the generated svg image metrics +* `src/plugins/*` contains various additional plugins which can add additional informations in generated metrics * `action/index.mjs` contains the GitHub action code * `action/dist/index.js` contains compiled the GitHub action code * `utils/*` contains various utilitaries for build @@ -295,3 +296,4 @@ This project was inspired by the following projects : * [anuraghazra/github-readme-stats](https://github.com/anuraghazra/github-readme-stats) * [jstrieb/github-stats](https://github.com/jstrieb/github-stats) +* [ankurparihar/readme-pagespeed-insights](https://github.com/ankurparihar/readme-pagespeed-insights) \ No newline at end of file diff --git a/action.yml b/action.yml index d30fd258..fb14394c 100644 --- a/action.yml +++ b/action.yml @@ -9,11 +9,13 @@ inputs: description: GitHub Personal Token (require "public_repo" permissions) required: true user: - description: Target GitHub user + description: Target GitHub username required: true filename: description: Name of SVG image output default: github-metrics.svg + pagespeed_token: + description: Pagespeed Personal Token (optional, will generate user's website performances if provided). See https://developers.google.com/speed/docs/insights/v5/get-started for more information. runs: using: node12 main: action/dist/index.js \ No newline at end of file diff --git a/action/index.mjs b/action/index.mjs index 6904a282..0260e976 100644 --- a/action/index.mjs +++ b/action/index.mjs @@ -29,8 +29,15 @@ const graphql = octokit.graphql.defaults({headers:{authorization: `token ${token}`}}) const rest = github.getOctokit(token) + //Additional plugins + const plugins = {}, q = {} + if (core.getInput("pagespeed_token")) { + plugins.pagespeed = {enabled:true, token:core.getInput("pagespeed_token")} + q.pagespeed = true + } + //Render metrics - const rendered = await metrics({login:user}, {template, style, query, graphql}) + const rendered = await metrics({login:user, q}, {template, style, query, graphql, plugins}) console.log(`Render | complete`) //Commit to repository diff --git a/package-lock.json b/package-lock.json index d3baefca..f8076b74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -153,6 +153,14 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, + "axios": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.20.0.tgz", + "integrity": "sha512-ANA4rr2BDcmmAQLOKft2fufrtuvlqR+cXNNinUmvfeSNCOF98PZL+7M/v1zIdGo7OLjEA9J2gXJL+j4zGsl0bA==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -417,6 +425,11 @@ "unpipe": "~1.0.0" } }, + "follow-redirects": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz", + "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==" + }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", diff --git a/package.json b/package.json index dd8e5332..8d556f4d 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@actions/core": "^1.2.5", "@actions/github": "^4.0.0", "@octokit/graphql": "^4.5.4", + "axios": "^0.20.0", "express": "^4.17.1", "express-rate-limit": "^5.1.3", "image-to-base64": "^2.1.1", diff --git a/settings.example.json b/settings.example.json index 6e0e2faf..3a0259d5 100644 --- a/settings.example.json +++ b/settings.example.json @@ -5,5 +5,11 @@ "maxusers":0, "//":"Maximum number of users, 0 for unlimited", "ratelimiter":null, "//":"Rate limiter (see express-rate-limit documentation for options)", "port":3000, "//":"Listening port", - "debug":false, "//":"Debug mode" + "debug":false, "//":"Debug mode", + "plugins":{ "//":"Additional plugins (optional)", + "pagespeed":{ "//":"Pagespeed configuration", + "enabled":false, "//":"Enable or disable Pagespeed metrics", + "token":"******", "//":"Pagespeed token" + } + } } \ No newline at end of file diff --git a/src/app.mjs b/src/app.mjs index 77c1030b..a7ff6ca9 100644 --- a/src/app.mjs +++ b/src/app.mjs @@ -17,8 +17,9 @@ //Load settings const settings = JSON.parse((await fs.promises.readFile(path.join("settings.json"))).toString()) - console.log(settings) - const {token, maxusers = 0, restricted = [], debug = false, cached = 30*60*1000, port = 3000, ratelimiter = null} = settings + const {token, maxusers = 0, restricted = [], debug = false, cached = 30*60*1000, port = 3000, ratelimiter = null, plugins = null} = settings + if (debug) + console.log(settings) //Load svg template, style and query let [template, style, query] = await load() const graphql = octokit.graphql.defaults({headers:{authorization: `token ${token}`}}) @@ -68,7 +69,7 @@ //Render if (debug) [template, style, query] = await load() - const rendered = await metrics({login}, {template, style, query, graphql}) + const rendered = await metrics({login, q:req.query}, {template, style, query, graphql, plugins}) //Cache if ((!debug)&&(cached)) cache.put(login, rendered, cached) diff --git a/src/metrics.mjs b/src/metrics.mjs index 8e0b877f..d9cc4aee 100644 --- a/src/metrics.mjs +++ b/src/metrics.mjs @@ -1,8 +1,9 @@ //Imports import imgb64 from "image-to-base64" + import Plugins from "./plugins/index.mjs" //Setup - export default async function metrics({login}, {template, style, query, graphql}) { + export default async function metrics({login, q}, {template, style, query, graphql, plugins}) { //Compute rendering try { @@ -15,8 +16,13 @@ //Init const languages = {colors:{}, total:0, stats:{}} - const computed = data.computed = {commits:0, languages, repositories:{watchers:0, stargazers:0, issues_open:0, issues_closed:0, pr_open:0, pr_merged:0, forks:0}} + const computed = data.computed = {commits:0, languages, repositories:{watchers:0, stargazers:0, issues_open:0, issues_closed:0, pr_open:0, pr_merged:0, forks:0}, plugins:{}} const avatar = imgb64(data.user.avatarUrl) + const pending = [] + + //Plugins + if (data.user.websiteUrl) + Plugins.pagespeed({url:data.user.websiteUrl, computed, pending, q}, plugins.pagespeed) //Iterate through user's repositories for (const repository of data.user.repositories.nodes) { @@ -59,6 +65,9 @@ //Avatar (base64) computed.avatar = await avatar || "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" + //Wait for pending promises + await Promise.all(pending) + //Eval rendering and return return eval(`\`${template}\``) } diff --git a/src/plugins/index.mjs b/src/plugins/index.mjs new file mode 100644 index 00000000..39e6a4b9 --- /dev/null +++ b/src/plugins/index.mjs @@ -0,0 +1,7 @@ +//Imports + import pagespeed from "./pagespeed/index.mjs" + +//Exports + export default { + pagespeed + } \ No newline at end of file diff --git a/src/plugins/lines/index.mjs b/src/plugins/lines/index.mjs new file mode 100644 index 00000000..dafd505d --- /dev/null +++ b/src/plugins/lines/index.mjs @@ -0,0 +1 @@ +//Placeholder for https://docs.github.com/en/rest/reference/repos#get-all-contributor-commit-activity \ No newline at end of file diff --git a/src/plugins/pagespeed/index.mjs b/src/plugins/pagespeed/index.mjs new file mode 100644 index 00000000..910dafdc --- /dev/null +++ b/src/plugins/pagespeed/index.mjs @@ -0,0 +1,32 @@ +//Imports + import axios from "axios" + +//Setup + export default function ({url, computed, pending, q}, {enabled = false, token = null} = {}) { + //Check if plugin is enabled and requirements are met + if (!enabled) + return computed.plugins.pagespeed = null + if (!url) + return computed.plugins.pagespeed = null + if (!q.pagespeed) + return computed.plugins.pagespeed = null + + //Plugin execution + pending.push(new Promise(async solve => { + computed.plugins.pagespeed = {url, scores:[{score:0.2, title:"x"}, {score:0.4, title:"x"}, {score:0.7, title:"x"}, {score:0.9, title:"x"}]} + return + + //Format url if needed + if (!/^https?:[/][/]/.test(url)) + url = `https://${url}` + //Load scores from API + const scores = new Map() + await Promise.all(["performance", "accessibility", "best-practices", "seo"].map(async category => { + const {score, title} = (await axios.get(`https://www.googleapis.com/pagespeedonline/v5/runPagespeed?category=${category}&url=${url}&key=${token}`)).data.lighthouseResult.categories[category] + scores.set(category, {score, title}) + })) + //Save results + computed.plugins.pagespeed = {url, scores:[scores.get("performance"), scores.get("accessibility"), scores.get("best-practices"), scores.get("seo")]} + solve() + })) + } \ No newline at end of file diff --git a/src/plugins/traffic/index.mjs b/src/plugins/traffic/index.mjs new file mode 100644 index 00000000..2402fad3 --- /dev/null +++ b/src/plugins/traffic/index.mjs @@ -0,0 +1 @@ +//Placeholder for https://docs.github.com/en/rest/reference/repos#get-page-views \ No newline at end of file diff --git a/src/query.graphql b/src/query.graphql index f79e0792..9dd21e7c 100644 --- a/src/query.graphql +++ b/src/query.graphql @@ -4,6 +4,7 @@ query Metrics { login createdAt avatarUrl + websiteUrl repositories(last: 100, isFork: false, ownerAffiliations: OWNER) { totalCount nodes { diff --git a/src/style.css b/src/style.css index bed06512..429c8595 100644 --- a/src/style.css +++ b/src/style.css @@ -1,86 +1,176 @@ -svg { - font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji; - font-size: 14px; - color: #777777; -} -svg.bar { - margin: 4px 0; -} -h1, h2, h3 { - margin: 8px 0 2px; - padding: 0; - color: #0366d6; - font-weight: normal; -} -h1 { - font-size: 20px; - font-weight: bold; -} -h2 { - font-size: 16px; -} -h2 svg { - fill: #0366d6; -} -h3 { - font-size: 14px; -} -.field { - display: flex; - align-items: center; - margin-bottom: 2px; -} -section > .field { - margin-left: 5px; - margin-right: 5px; -} -.field svg { - margin: 0 8px; - fill: #959da5; -} -.column { - display: flex; - flex-direction: column; - align-items: center; -} -.center { - justify-content: center; -} -.horizontal { - justify-content: space-around; -} -.horizontal-wrap { - flex-wrap: wrap; -} -.horizontal .field { - flex: 1 1 0; -} -.row { - display: flex; -} -.row section { - flex: 1 1 0; -} -.no-wrap { - white-space: nowrap; -} -.avatar { - background-color: #000000; - border-radius: 50%; - margin: 0 6px; -} -.calendar.field { - margin: 4px 0; - margin-left: 7px; -} -.calendar .day { - outline: 1px solid rgba(27,31,35,.04); - outline-offset: -1px; -} -footer { - margin-top: 8px; - text-align: right; - font-size: 8px; - font-style: italic; - opacity: 0.5; -} \ No newline at end of file +/* SVG global context */ + svg { + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji; + font-size: 14px; + color: #777777; + } + +/* Headers */ + h1, h2, h3 { + margin: 8px 0 2px; + padding: 0; + color: #0366d6; + font-weight: normal; + } + h1 svg, h2 svg, h3 svg { + fill: currentColor; + } + h1 { + font-size: 20px; + font-weight: bold; + } + h2 { + font-size: 16px; + } + h3 { + font-size: 14px; + } + +/* Fields */ + section > .field { + margin-left: 5px; + margin-right: 5px; + } + .field { + display: flex; + align-items: center; + margin-bottom: 2px; + } + .field svg { + margin: 0 8px; + fill: #959da5; + } + +/* Displays */ + .row { + display: flex; + } + .row section { + flex: 1 1 0; + } + .column { + display: flex; + flex-direction: column; + align-items: center; + } + .center { + justify-content: center; + } + .horizontal { + justify-content: space-around; + } + .horizontal-wrap { + flex-wrap: wrap; + } + .horizontal .field { + flex: 1 1 0; + } + .no-wrap { + white-space: nowrap; + } + .fill-width { + width: 100%; + } + +/* User avatar */ + .avatar { + background-color: #000000; + border-radius: 50%; + margin: 0 6px; + } + +/* Commit calendar */ + .calendar.field { + margin: 4px 0; + margin-left: 7px; + } + .calendar .day { + outline: 1px solid rgba(27,31,35,.04); + outline-offset: -1px; + } + +/* Progress bars */ + svg.bar { + margin: 4px 0; + } + +/* Footer */ + footer { + margin-top: 8px; + text-align: right; + font-size: 8px; + font-style: italic; + opacity: 0.5; + } + +/* Speed test categories */ + .categories { + display: flex; + align-items: center; + justify-content: space-around; + margin-top: 4px; + } + .categorie { + display: flex; + flex-direction: column; + align-items: center; + flex: 1 1 0; + } + +/* Gauges */ + .gauge { + stroke-linecap: round; + fill: none; + } + .gauge.high { + color: #18b663; + } + .gauge.average { + color: #fb8c00; + } + .gauge.low { + color: #e53935; + } + .gauge-base, .gauge-arc { + stroke: currentColor; + stroke-width: 10; + } + .gauge-base { + stroke-opacity: .2; + } + .gauge-arc { + fill: none; + stroke-dashoffset: 0; + animation-delay: 250ms; + animation: animation-gauge 1s ease forwards + } + .gauge text { + fill: currentColor; + font-size: 40px; + font-family: monospace; + text-anchor: middle; + font-weight: 600; + } + .gauge .title { + font-size: 18px; + color: #777777; + } + @keyframes animation-gauge { + from { + stroke-dasharray: 0 329; + } + } + +/* Fade animation */ + .af { + opacity: 0; + animation: animation-fade 1s ease forwards; + } + @keyframes animation-fade { + from { + opacity: 0; + } + to { + opacity: 1; + } + } \ No newline at end of file diff --git a/src/template.svg b/src/template.svg index 1be93064..0010fdaf 100644 --- a/src/template.svg +++ b/src/template.svg @@ -1,10 +1,10 @@ - + -
+

@@ -16,11 +16,11 @@
- + Joined GitHub ${data.computed.registration}
- + Followed by ${data.user.followers.totalCount} user${data.user.followers.totalCount > 1 ? "s" : ""}
@@ -49,46 +49,46 @@

- + Activity

- + ${data.computed.commits} Commit${data.computed.commits > 1 ? "s" : ""}
- + ${data.user.contributionsCollection.totalPullRequestReviewContributions} Pull request${data.user.contributionsCollection.totalPullRequestReviewContributions > 1 ? "s" : ""} reviewed
- + ${data.user.contributionsCollection.totalPullRequestContributions} Pull request${data.user.contributionsCollection.totalPullRequestContributions > 1 ? "s" : ""} opened
- + ${data.user.contributionsCollection.totalIssueContributions} Issue${data.user.contributionsCollection.totalIssueContributions > 1 ? "s" : ""} opened

- + Community stats

- + Following ${data.user.following.totalCount} user${data.user.followers.totalCount > 1 ? "s" : ""}
- + Sponsoring ${data.computed.sponsorships} repositor${data.computed.sponsorships > 1 ? "ies" : "y"}
- + Starred ${data.user.starredRepositories.totalCount} repositor${data.user.starredRepositories.totalCount > 1 ? "ies" : "y"}
- + Watching ${data.user.watching.totalCount} repositor${data.user.watching.totalCount > 1 ? "ies" : "y"}
@@ -97,28 +97,28 @@

- + ${data.user.repositories.totalCount} Repositor${data.user.repositories.totalCount > 1 ? "ies" : "y"}

- + ${data.computed.repositories.stargazers} Stargazer${data.computed.repositories.stargazers > 1 ? "s" : ""}
- + ${data.user.packages.totalCount} Package${data.user.packages.totalCount > 1 ? "s" : ""}
- + ${data.computed.repositories.forks} Fork${data.computed.repositories.forks > 1 ? "s" : ""}
- + ${data.computed.repositories.watchers} Watcher${data.computed.repositories.watchers > 1 ? "s" : ""}
@@ -139,11 +139,11 @@
- + ${data.computed.repositories.issues_closed} Closed
- + ${data.computed.repositories.issues_open} Open
@@ -161,11 +161,11 @@
- + ${data.computed.repositories.pr_merged} Merged
- + ${data.computed.repositories.pr_open} Open
@@ -187,13 +187,36 @@
${data.computed.languages.favorites.map(({name, color}) => `
- + ${name}
`).join("")}
+ ${computed.plugins.pagespeed ? ` +
+

+ + ${computed.plugins.pagespeed.url} +

+
+
+ ${computed.plugins.pagespeed.scores.map(({score, title}) => ` +
+ + + + ${Math.round(score*100)} + + ${title} +
+ `).join("")} +
+
+
` : "" + } +