Initial commit
This commit is contained in:
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
107
.gitignore
vendored
Normal file
107
.gitignore
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# User settings
|
||||
settings.json
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 lowlighter
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
297
README.md
Normal file
297
README.md
Normal file
@@ -0,0 +1,297 @@
|
||||
# 📊 GitHub metrics
|
||||
|
||||
Generates your own GitHub metrics as an SVG image to put them on your profile page or elsewhere !
|
||||
|
||||
Below is what it looks like :
|
||||
|
||||

|
||||
|
||||
## 📜 How to use ?
|
||||
|
||||
### ⚙️ Using GitHub Action on your profile repo (~5 min setup)
|
||||
|
||||
A GitHub Action which is run periodically at your convenience which generates and push an SVG image on your personal repository.
|
||||
|
||||
Assuming your username is `my-github-user`, you can embed your metrics like below :
|
||||
```markdown
|
||||

|
||||
```
|
||||
```html
|
||||
<img src="https://github.com/my-github-user/my-github-user/blob/master/github-metrics.svg" alt="My GitHub metrics">
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>💬 How to setup in 3 steps</summary>
|
||||
|
||||
### 1. Create a GitHub token
|
||||
|
||||
In your account settings, go to `Developer settings` and select `Personal access tokens` to create a new token.
|
||||
|
||||
You'll need to create a token with the `public_repo` right so this GitHub Action has enough permissions to push the updated SVG metrics on your personal repository.
|
||||
|
||||

|
||||
|
||||
### 2. Put your GitHub token in your personal repository secrets
|
||||
|
||||
Go to the `Settings` of your personal repository to create a new secret and paste your GitHub token here with the name `METRICS_TOKEN`.
|
||||
|
||||

|
||||
|
||||
### 3. Create a new GitHub Action workflow on your personal repo
|
||||
|
||||
Go to the `Actions` of your personal repository and create a new workflow.
|
||||
|
||||
Paste the following and don't forget to put your GitHub username.
|
||||
```yaml
|
||||
name: GitHub metrics as SVG image
|
||||
on:
|
||||
# Update metrics each 15 minutes. Edit this if you want to increase/decrease frequency
|
||||
schedule: [{cron: "*/15 * * * *"}]
|
||||
# Add this if you want to force update each time you commit on master branch
|
||||
push: {branches: "master"}
|
||||
jobs:
|
||||
github-metrics:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: lowlighter/metrics@latest
|
||||
# This line will prevent this GitHub action from running when it is updated by itself if you enabled trigger on master branch
|
||||
if: "!contains(github.event.head_commit.message, '[Skip GitHub Action]')"
|
||||
with:
|
||||
# Your GitHub token ("public_repo" is required to allow this action to update the metrics SVG image)
|
||||
token: ${{ secrets.METRICS_TOKEN }}
|
||||
# Your GitHub user name
|
||||
user: my-github-user
|
||||
```
|
||||
|
||||
On each run, a new SVG image will be generated and committed to your repository.
|
||||
Note that this will virtually increase your commits stats, so you could use a bot account instead.
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
### 💕 Using the shared instance (~2 min setup, but with limitations)
|
||||
|
||||
For conveniency, you can use the shared instance available at [metrics.lecoq.io](https://metrics.lecoq.io).
|
||||
|
||||
Assuming your username is `my-github-user`, you can embed your metrics like below :
|
||||
```markdown
|
||||

|
||||
```
|
||||
```html
|
||||
<img src="https://metrics.lecoq.io/my-github-user" alt="My GitHub metrics">
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>💬 Restrictions and fair use</summary>
|
||||
|
||||
Since GitHub API has rate limitations and to avoid abuse, the shared instance has the following limitations :
|
||||
* Images are cached for 1 day (meaning that your metrics won't be updated until the next day)
|
||||
* A maximum of 1000 users can use this service
|
||||
* You're limited to 3 requests per hour (cached metrics are not counted)
|
||||
|
||||
You should consider deploying your own instance or use GitHub Action if you're planning using this service.
|
||||
|
||||
</details>
|
||||
|
||||
### 🏗️ Deploying your own instance (~15 min setup, depending on your sysadmin knowledge)
|
||||
|
||||
Using your own instance is useful if you do not want to use GitHub Action or allow others users to use your instance.
|
||||
|
||||
A GitHub token is required to setup your instance, however since metrics images are not stored on your repositories you do not need to grant any additional permissions to your token, which reduce security issues.
|
||||
|
||||
You can restrict which users can generate metrics on your server and apply rate limiting (which is advised or else you'll hit the GitHub API rate limiter).
|
||||
|
||||
It is also easier to change `query.graphql`, `style.css` and `template.svg` if you want to gather additional stats, perform esthetical changes or edit the structure of the SVG image.
|
||||
|
||||
|
||||
<details>
|
||||
<summary>💬 How to setup in 5 steps</summary>
|
||||
|
||||
### 0. Prepare your server
|
||||
|
||||
You'll need to have a server at your disposal where you can install and configure stuff.
|
||||
|
||||
### 1. Create a GitHub token
|
||||
|
||||
In your account settings, go to `Developer settings` and select `Personal access tokens` to create a new token.
|
||||
|
||||
As explained above, you do not need to grant additional permissions to the token.
|
||||
|
||||

|
||||
|
||||
### 2. Install the dependancies
|
||||
|
||||
Connect to your server.
|
||||
|
||||
You'll need [NodeJS](https://nodejs.org/en/) (the latter version is better, for reference this was tested on v14.9.0).
|
||||
|
||||
Clone the repository
|
||||
|
||||
```shell
|
||||
git clone https://github.com/lowlighter/metrics.git
|
||||
```
|
||||
|
||||
Go inside project and install dependancies :
|
||||
```shell
|
||||
cd metrics/
|
||||
npm install
|
||||
```
|
||||
|
||||
Copy `settings.example.json` to `settings.json`
|
||||
```shell
|
||||
cp settings.example.json settings.json
|
||||
```
|
||||
|
||||
### 3. Configure your instance
|
||||
|
||||
Open and edit `settings.json` to configure your instance.
|
||||
|
||||
```javascript
|
||||
{
|
||||
//Your GitHub API token
|
||||
"token":"****************************************",
|
||||
|
||||
//The optionals parameters below allows you to avoid reaching the GitHub API rate limitation
|
||||
|
||||
//A set of whitelisted users which can generate metrics on your instance
|
||||
//Leave empty or undefined to disable
|
||||
//Defaults to unrestricted
|
||||
"restricted":["my-github-user"],
|
||||
|
||||
//Lifetime of each generated metrics
|
||||
//If an user's metrics are requested while lifetime is still up, a cached version will be served
|
||||
//Defaults to 60 minutes
|
||||
"cached":3600000,
|
||||
|
||||
//Maximum simultaneous number of user which can be cached
|
||||
//When this limit is reached, new users will receive a 503 error
|
||||
//Defaults to 0 (unlimited)
|
||||
"maxusers":0,
|
||||
|
||||
//Rate limiter
|
||||
//See https://www.npmjs.com/package/express-rate-limit
|
||||
//Disabled by default
|
||||
"ratelimiter":{
|
||||
"windowMs":60000,
|
||||
"max":100
|
||||
},
|
||||
|
||||
//Port on which your instance listen
|
||||
//Defaults to 3000
|
||||
"port":3000,
|
||||
|
||||
//Debug mode
|
||||
//When enabled, "query.graphql", "style.css" and "template.svg" will be reloaded at each request
|
||||
//Cache will be disabled
|
||||
//This is intendend for easier development which allows to see your changes quickly
|
||||
//Defaults to false
|
||||
"debug":false,
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Start your instance
|
||||
|
||||
Run the following command to start your instance :
|
||||
```shell
|
||||
npm start
|
||||
```
|
||||
|
||||
Open your browser and test your instance :
|
||||
```shell
|
||||
http://localhost:3000/my-github-user
|
||||
```
|
||||
|
||||
### 5. Setup as service on your instance (optional)
|
||||
|
||||
You should consider using a service to run your instance.
|
||||
It will allow to restart automatically on crash and on boot.
|
||||
|
||||
Create a new file in `/etc/systemd/system` :
|
||||
```shell
|
||||
vi /etc/systemd/system/github_metrics.service
|
||||
```
|
||||
|
||||
Paste the following and edit it with the correct paths :
|
||||
```
|
||||
[Unit]
|
||||
Description=GitHub metrics
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/path/to/metrics
|
||||
ExecStart=/usr/bin/node /path/to/metrics/index.mjs
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Reload services, enable it and start it :
|
||||
```shell
|
||||
systemctl daemon-reload
|
||||
systemctl enable github_metrics
|
||||
systemctl start github_metrics
|
||||
```
|
||||
|
||||
Check if your service is up and running :
|
||||
```shell
|
||||
systemctl status github_metrics
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 🗂️ Project structure
|
||||
|
||||
* `index.mjs` contains the entry points and the settings instance
|
||||
* `src/app.mjs` contains the server code which serves renders and apply rate limiting, restrictions, etc.
|
||||
* `src/metrics.mjs` contains metrics renderer
|
||||
* `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
|
||||
* `action/index.mjs` contains the GitHub action code
|
||||
* `action/dist/index.js` contains compiled the GitHub action code
|
||||
* `utils/*` contains various utilitaries for build
|
||||
|
||||
## ⚠️ HTTP errors code
|
||||
|
||||
The following errors code can be encountered if your using a server instance :
|
||||
|
||||
* 403 Forbidden : User is not whitelisted in `restricted` users list
|
||||
* 404 Not found : GitHub API did not found the requested user
|
||||
* 429 Too many requests : Thrown when rate limiter is trigerred
|
||||
* 500 Internal error : An error ocurred while generating metrics images (logs can be seen if you're the owner of the instance)
|
||||
* 503 Service unavailable : Maximum user capacity reached, only already cached images can be accessed for now
|
||||
|
||||
## 📚 Documentations
|
||||
|
||||
Below is a list of useful documentations links :
|
||||
|
||||
* [GitHub GraphQL API](https://docs.github.com/en/graphql)
|
||||
* [GitHub GraphQL Explorer](https://developer.github.com/v4/explorer/)
|
||||
|
||||
## 📦 Used packages
|
||||
|
||||
Below is a list of primary dependencies :
|
||||
|
||||
* [express/express.js](https://github.com/expressjs/express)
|
||||
* To serve, compute and render a GitHub user's metrics
|
||||
* [nfriedly/express-rate-limit](https://github.com/nfriedly/express-rate-limit)
|
||||
* To apply rate limiting on server and avoid spams and hitting GitHub API's own rate limit
|
||||
* [octokit/graphql.js](https://github.com/octokit/graphql.js/)
|
||||
* To perform request to GitHub GraphQL API
|
||||
* [ptarjan/node-cache](https://github.com/ptarjan/node-cache)
|
||||
* To cache generated content and reduce
|
||||
* [renanbastos93/image-to-base64](https://github.com/renanbastos93/image-to-base64)
|
||||
* To generate base64 representation of users' avatars
|
||||
|
||||
All icons were ripped across GitHub's site, but still remains the intellectual property of GitHub.
|
||||
See [GitHub Logos and Usage](https://github.com/logos) for more information.
|
||||
|
||||
## ✨ Inspirations
|
||||
|
||||
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)
|
||||
19
action.yml
Normal file
19
action.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
name: GitHub metrics as SVG image
|
||||
author: lowlighter
|
||||
description: Generate an user's GitHub metrics as SVG image format to embed somewhere else
|
||||
branding:
|
||||
icon: user-check
|
||||
color: gray-dark
|
||||
inputs:
|
||||
token:
|
||||
description: GitHub Personal Token (require "public_repo" permissions)
|
||||
required: true
|
||||
user:
|
||||
description: Target GitHub user
|
||||
required: true
|
||||
filename:
|
||||
description: Name of SVG image output
|
||||
default: github-metrics.svg
|
||||
runs:
|
||||
using: node12
|
||||
main: action/dist/index.js
|
||||
6417
action/dist/index.js
vendored
Normal file
6417
action/dist/index.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
61
action/index.mjs
Normal file
61
action/index.mjs
Normal file
@@ -0,0 +1,61 @@
|
||||
//Imports
|
||||
import path from "path"
|
||||
import * as _metrics from "./../src/metrics.mjs"
|
||||
import * as _octokit from "@octokit/graphql"
|
||||
import * as _core from "@actions/core"
|
||||
import * as _github from "@actions/github"
|
||||
|
||||
;((async function () {
|
||||
//Hack because ES modules are not correctly transpiled with ncc
|
||||
const [core, github, octokit, metrics] = [_core, _github, _octokit, _metrics].map(m => (m && m.default) ? m.default : m)
|
||||
//Runner
|
||||
try {
|
||||
//Initialization
|
||||
console.log(`GitHub metrics as SVG image`)
|
||||
console.log(`========================================================`)
|
||||
|
||||
//Load svg template, style and query
|
||||
const template = `<#include template.svg>`, style = `<#include style.css>`, query = `<#include query.graphql>`
|
||||
console.log(`Templates | loaded`)
|
||||
|
||||
//Initialization
|
||||
const [token, user, filename] = [core.getInput("token"), core.getInput("user"), core.getInput("filename", {default:"github-metrics.svg"})]
|
||||
const output = path.join(filename)
|
||||
console.log(`GitHub user | ${user}`)
|
||||
console.log(`Output file | ${output}`)
|
||||
console.log(`Github token | ${token ? "provided" : "missing"}`)
|
||||
if (!token)
|
||||
throw new Error("You must provide a valid GitHub token")
|
||||
const graphql = octokit.graphql.defaults({headers:{authorization: `token ${token}`}})
|
||||
const rest = github.getOctokit(token)
|
||||
|
||||
//Render metrics
|
||||
const rendered = await metrics({login:user}, {template, style, query, graphql})
|
||||
console.log(`Render | complete`)
|
||||
|
||||
//Commit to repository
|
||||
let sha = undefined
|
||||
try {
|
||||
const {data} = await rest.repos.getContent({
|
||||
owner:user,
|
||||
repo:user,
|
||||
path:filename,
|
||||
})
|
||||
sha = data.sha
|
||||
} catch (error) { }
|
||||
console.log(`Previous render sha | ${sha || "none"}`)
|
||||
await rest.repos.createOrUpdateFileContents({
|
||||
owner:user, repo:user, path:filename, sha, message:`Update ${filename} - [Skip GitHub Action]`,
|
||||
content:Buffer.from(rendered).toString("base64"),
|
||||
})
|
||||
console.log(`Commit to repo | ok`)
|
||||
|
||||
//Success
|
||||
console.log(`Success !`)
|
||||
//Errors
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
core.setFailed(error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
})()).catch(error => process.exit(1))
|
||||
BIN
docs/imgs/action_update.png
Normal file
BIN
docs/imgs/action_update.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
docs/imgs/personal_token.png
Normal file
BIN
docs/imgs/personal_token.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
docs/imgs/personal_token_alt.png
Normal file
BIN
docs/imgs/personal_token_alt.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
docs/imgs/repo_secrets.png
Normal file
BIN
docs/imgs/repo_secrets.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
5
index.mjs
Normal file
5
index.mjs
Normal file
@@ -0,0 +1,5 @@
|
||||
//Imports
|
||||
import app from "./src/app.mjs"
|
||||
|
||||
//Start app
|
||||
await app()
|
||||
1044
package-lock.json
generated
Normal file
1044
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
package.json
Normal file
36
package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "metrics",
|
||||
"version": "1.0.0",
|
||||
"description": "Generate an user's GitHub metrics as SVG image format to embed somewhere else",
|
||||
"main": "index.mjs",
|
||||
"scripts": {
|
||||
"start": "node index.mjs",
|
||||
"build-ncc": "npx ncc build action/index.mjs --out action/dist",
|
||||
"build-post": "node utils/post_build.mjs",
|
||||
"build": "npx run-s build-ncc build-post",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/lowlighter/metrics.git"
|
||||
},
|
||||
"author": "lowlighter",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/lowlighter/metrics/issues"
|
||||
},
|
||||
"homepage": "https://github.com/lowlighter/metrics#readme",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.2.5",
|
||||
"@actions/github": "^4.0.0",
|
||||
"@octokit/graphql": "^4.5.4",
|
||||
"express": "^4.17.1",
|
||||
"express-rate-limit": "^5.1.3",
|
||||
"image-to-base64": "^2.1.1",
|
||||
"memory-cache": "^0.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vercel/ncc": "^0.24.0",
|
||||
"npm-run-all": "^4.1.5"
|
||||
}
|
||||
}
|
||||
9
settings.example.json
Normal file
9
settings.example.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"token":"MY GITHUB API TOKEN", "//":"Your own GitHub API token (required)",
|
||||
"restricted":[], "//":"List of authorized users, leave empty for unrestricted",
|
||||
"cached":3600000, "//":"Cached time for generated images, 0 to disable",
|
||||
"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"
|
||||
}
|
||||
99
src/app.mjs
Normal file
99
src/app.mjs
Normal file
@@ -0,0 +1,99 @@
|
||||
//Imports
|
||||
import express from "express"
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import octokit from "@octokit/graphql"
|
||||
import cache from "memory-cache"
|
||||
import ratelimit from "express-rate-limit"
|
||||
import metrics from "./metrics.mjs"
|
||||
|
||||
//Load svg template, style and query
|
||||
async function load() {
|
||||
return await Promise.all(["template.svg", "style.css", "query.graphql"].map(async file => `${await fs.promises.readFile(path.join("src", file))}`))
|
||||
}
|
||||
|
||||
//Setup
|
||||
export default async function setup() {
|
||||
|
||||
//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
|
||||
//Load svg template, style and query
|
||||
let [template, style, query] = await load()
|
||||
const graphql = octokit.graphql.defaults({headers:{authorization: `token ${token}`}})
|
||||
|
||||
//Setup server
|
||||
const app = express()
|
||||
const middlewares = []
|
||||
//Rate limiter middleware
|
||||
if (ratelimiter) {
|
||||
app.set("trust proxy", 1)
|
||||
middlewares.push(ratelimit({
|
||||
skip(req, res) { return !!cache.get(req.params.login) },
|
||||
message:"Too many requests",
|
||||
...ratelimiter
|
||||
}))
|
||||
}
|
||||
//Cache headers middleware
|
||||
middlewares.push((req, res, next) => {
|
||||
res.header("Cache-Control", cached ? `public, max-age=${cached}` : "no-store, no-cache")
|
||||
next()
|
||||
})
|
||||
|
||||
//Base routes
|
||||
app.get("/", (req, res) => res.redirect("https://github.com/lowlighter/metrics"))
|
||||
app.get("/favicon.ico", (req, res) => res.sendStatus(204))
|
||||
|
||||
//Metrics
|
||||
app.get("/:login", ...middlewares, async (req, res) => {
|
||||
|
||||
//Request params
|
||||
const {login} = req.params
|
||||
if ((restricted.length)&&(!restricted.includes(login)))
|
||||
return res.sendStatus(403)
|
||||
|
||||
//Read cached data if possible
|
||||
if ((!debug)&&(cached)&&(cache.get(login))) {
|
||||
res.header("Content-Type", "image/svg+xml")
|
||||
res.send(cache.get(login))
|
||||
return
|
||||
}
|
||||
//Maximum simultaneous users
|
||||
if ((maxusers)&&(cache.size()+1 > maxusers))
|
||||
return res.sendStatus(503)
|
||||
|
||||
//Compute rendering
|
||||
try {
|
||||
//Render
|
||||
if (debug)
|
||||
[template, style, query] = await load()
|
||||
const rendered = await metrics({login}, {template, style, query, graphql})
|
||||
//Cache
|
||||
if ((!debug)&&(cached))
|
||||
cache.put(login, rendered, cached)
|
||||
//Send response
|
||||
res.header("Content-Type", "image/svg+xml")
|
||||
res.send(rendered)
|
||||
}
|
||||
//Internal error
|
||||
catch (error) {
|
||||
//Not found user
|
||||
if ((error instanceof Error)&&(/^user not found$/.test(error.message)))
|
||||
return res.sendStatus(404)
|
||||
//General error
|
||||
console.error(error)
|
||||
res.sendStatus(500)
|
||||
}
|
||||
})
|
||||
|
||||
//Listen
|
||||
app.listen(port, () => console.log([
|
||||
`Listening on port | ${port}`,
|
||||
`Debug mode | ${debug}`,
|
||||
`Restricted to users | ${restricted.size ? [...restricted].join(", ") : "(unrestricted)"}`,
|
||||
`Cached time | ${cached} seconds`,
|
||||
`Rate limiter | ${ratelimiter ? JSON.stringify(ratelimiter) : "(enabled)"}`,
|
||||
`Max simultaneous users | ${maxusers ? `${maxusers} users` : "(unrestricted)"}`
|
||||
].join("\n")))
|
||||
}
|
||||
67
src/metrics.mjs
Normal file
67
src/metrics.mjs
Normal file
@@ -0,0 +1,67 @@
|
||||
//Imports
|
||||
import imgb64 from "image-to-base64"
|
||||
|
||||
//Setup
|
||||
export default async function metrics({login}, {template, style, query, graphql}) {
|
||||
//Compute rendering
|
||||
try {
|
||||
|
||||
//Query data from GitHub API
|
||||
const data = await graphql(query
|
||||
.replace(/[$]login/, `"${login}"`)
|
||||
.replace(/[$]calendar.to/, `"${(new Date()).toISOString()}"`)
|
||||
.replace(/[$]calendar.from/, `"${(new Date(Date.now()-14*24*60*60*1000)).toISOString()}"`)
|
||||
)
|
||||
|
||||
//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 avatar = imgb64(data.user.avatarUrl)
|
||||
|
||||
//Iterate through user's repositories
|
||||
for (const repository of data.user.repositories.nodes) {
|
||||
//Simple properties with totalCount
|
||||
for (const property of ["watchers", "stargazers", "issues_open", "issues_closed", "pr_open", "pr_merged"])
|
||||
computed.repositories[property] += repository[property].totalCount
|
||||
//Forks
|
||||
computed.repositories.forks += repository.forkCount
|
||||
//Languages
|
||||
for (const {size, node:{color, name}} of Object.values(repository.languages.edges)) {
|
||||
languages.stats[name] = (languages.stats[name] || 0) + size
|
||||
languages.colors[name] = color || "#ededed"
|
||||
languages.total += size
|
||||
}
|
||||
}
|
||||
|
||||
//Compute count for issues and pull requests
|
||||
for (const property of ["issues", "pr"])
|
||||
computed.repositories[`${property}_count`] = computed.repositories[`${property}_open`] + computed.repositories[`${property}_${property === "pr" ? "merged" : "closed"}`]
|
||||
|
||||
//Compute total commits and sponsorships
|
||||
computed.commits = data.user.contributionsCollection.totalCommitContributions + data.user.contributionsCollection.restrictedContributionsCount
|
||||
computed.sponsorships = data.user.sponsorshipsAsSponsor.totalCount + data.user.sponsorshipsAsMaintainer.totalCount
|
||||
|
||||
//Compute registration date
|
||||
const diff = (Date.now()-(new Date(data.user.createdAt)).getTime())/(365*24*60*60*1000)
|
||||
const years = Math.floor(diff)
|
||||
const months = Math.ceil((diff-years)*12)
|
||||
computed.registration = years ? `${years} year${years > 1 ? "s" : ""} ago` : `${months} month${months > 1 ? "s" : ""} ago`
|
||||
|
||||
//Compute languages stats
|
||||
Object.keys(languages.stats).map(name => languages.stats[name] /= languages.total)
|
||||
languages.favorites = Object.entries(languages.stats).sort(([an, a], [bn, b]) => b - a).slice(0, 8).map(([name, value]) => ({name, value, color:languages.colors[name], x:0}))
|
||||
for (let i = 1; i < languages.favorites.length; i++)
|
||||
languages.favorites[i].x = languages.favorites[i-1].x + languages.favorites[i-1].value
|
||||
|
||||
//Compute calendar
|
||||
computed.calendar = data.user.calendar.contributionCalendar.weeks.flatMap(({contributionDays}) => contributionDays).slice(0, 14).reverse()
|
||||
|
||||
//Avatar (base64)
|
||||
computed.avatar = await avatar || "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
|
||||
|
||||
//Eval rendering and return
|
||||
return eval(`\`${template}\``)
|
||||
}
|
||||
//Internal error
|
||||
catch (error) { throw (((Array.isArray(error.errors))&&(error.errors[0].type === "NOT_FOUND")) ? new Error("user not found") : error) }
|
||||
}
|
||||
82
src/query.graphql
Normal file
82
src/query.graphql
Normal file
@@ -0,0 +1,82 @@
|
||||
query Metrics {
|
||||
user(login: $login) {
|
||||
name
|
||||
login
|
||||
createdAt
|
||||
avatarUrl
|
||||
repositories(last: 100, isFork: false, ownerAffiliations: OWNER) {
|
||||
totalCount
|
||||
nodes {
|
||||
watchers {
|
||||
totalCount
|
||||
}
|
||||
stargazers {
|
||||
totalCount
|
||||
}
|
||||
languages(first: 4) {
|
||||
edges {
|
||||
size
|
||||
node {
|
||||
color
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
issues_open: issues(states: OPEN) {
|
||||
totalCount
|
||||
}
|
||||
issues_closed: issues(states: CLOSED) {
|
||||
totalCount
|
||||
}
|
||||
pr_open: pullRequests(states: OPEN) {
|
||||
totalCount
|
||||
}
|
||||
pr_merged: pullRequests(states: MERGED) {
|
||||
totalCount
|
||||
}
|
||||
forkCount
|
||||
}
|
||||
}
|
||||
packages {
|
||||
totalCount
|
||||
}
|
||||
starredRepositories {
|
||||
totalCount
|
||||
}
|
||||
watching {
|
||||
totalCount
|
||||
}
|
||||
sponsorshipsAsSponsor {
|
||||
totalCount
|
||||
}
|
||||
sponsorshipsAsMaintainer {
|
||||
totalCount
|
||||
}
|
||||
contributionsCollection {
|
||||
totalRepositoriesWithContributedCommits
|
||||
totalCommitContributions
|
||||
restrictedContributionsCount
|
||||
totalIssueContributions
|
||||
totalPullRequestContributions
|
||||
totalPullRequestReviewContributions
|
||||
}
|
||||
calendar:contributionsCollection(from: $calendar.from, to: $calendar.to) {
|
||||
contributionCalendar {
|
||||
weeks {
|
||||
contributionDays {
|
||||
color
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
repositoriesContributedTo {
|
||||
totalCount
|
||||
}
|
||||
followers {
|
||||
totalCount
|
||||
}
|
||||
following {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
86
src/style.css
Normal file
86
src/style.css
Normal file
@@ -0,0 +1,86 @@
|
||||
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;
|
||||
}
|
||||
203
src/template.svg
Normal file
203
src/template.svg
Normal file
@@ -0,0 +1,203 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="480">
|
||||
<style>
|
||||
${style}
|
||||
</style>
|
||||
|
||||
<foreignObject x="0" y="0" width="100%" height="100%">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml">
|
||||
|
||||
<section>
|
||||
<h1 class="field">
|
||||
<img class="avatar" src="data:image/png;base64,${data.computed.avatar}" width="20" height="20" />
|
||||
<span>${data.user.name || data.user.login}</span>
|
||||
</h1>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<section>
|
||||
<div class="field">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zm.5 4.75a.75.75 0 00-1.5 0v3.5a.75.75 0 00.471.696l2.5 1a.75.75 0 00.557-1.392L8.5 7.742V4.75z"></path></svg>
|
||||
Joined GitHub ${data.computed.registration}
|
||||
</div>
|
||||
<div class="field">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M5.5 3.5a2 2 0 100 4 2 2 0 000-4zM2 5.5a3.5 3.5 0 115.898 2.549 5.507 5.507 0 013.034 4.084.75.75 0 11-1.482.235 4.001 4.001 0 00-7.9 0 .75.75 0 01-1.482-.236A5.507 5.507 0 013.102 8.05 3.49 3.49 0 012 5.5zM11 4a.75.75 0 100 1.5 1.5 1.5 0 01.666 2.844.75.75 0 00-.416.672v.352a.75.75 0 00.574.73c1.2.289 2.162 1.2 2.522 2.372a.75.75 0 101.434-.44 5.01 5.01 0 00-2.56-3.012A3 3 0 0011 4z"></path></svg>
|
||||
Followed by ${data.user.followers.totalCount} user${data.user.followers.totalCount > 1 ? "s" : ""}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="field calendar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${data.computed.calendar.length*15} 11" width="${data.computed.calendar.length*15}" height="16">
|
||||
<g>
|
||||
${data.computed.calendar.map(({color}, x) => `
|
||||
<rect class="day" x="${x*15}" y="0" width="11" height="11" fill="${color}" rx="2" ry="2" />
|
||||
`).join("")}
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="field">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1 2.5A2.5 2.5 0 013.5 0h8.75a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0V1.5h-8a1 1 0 00-1 1v6.708A2.492 2.492 0 013.5 9h3.25a.75.75 0 010 1.5H3.5a1 1 0 100 2h5.75a.75.75 0 010 1.5H3.5A2.5 2.5 0 011 11.5v-9zm13.23 7.79a.75.75 0 001.06-1.06l-2.505-2.505a.75.75 0 00-1.06 0L9.22 9.229a.75.75 0 001.06 1.061l1.225-1.224v6.184a.75.75 0 001.5 0V9.066l1.224 1.224z"></path></svg>
|
||||
Contributed to ${data.user.repositoriesContributedTo.totalCount} repositor${data.user.repositoriesContributedTo.totalCount > 1 ? "ies" : "y"}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<section>
|
||||
<h2 class="field">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M1.5 1.75a.75.75 0 00-1.5 0v12.5c0 .414.336.75.75.75h14.5a.75.75 0 000-1.5H1.5V1.75zm14.28 2.53a.75.75 0 00-1.06-1.06L10 7.94 7.53 5.47a.75.75 0 00-1.06 0L3.22 8.72a.75.75 0 001.06 1.06L7 7.06l2.47 2.47a.75.75 0 001.06 0l5.25-5.25z"></path></svg>
|
||||
Activity
|
||||
</h2>
|
||||
<div class="field">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M10.5 7.75a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0zm1.43.75a4.002 4.002 0 01-7.86 0H.75a.75.75 0 110-1.5h3.32a4.001 4.001 0 017.86 0h3.32a.75.75 0 110 1.5h-3.32z"></path></svg>
|
||||
${data.computed.commits} Commit${data.computed.commits > 1 ? "s" : ""}
|
||||
</div>
|
||||
<div class="field">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M2.5 1.75a.25.25 0 01.25-.25h8.5a.25.25 0 01.25.25v7.736a.75.75 0 101.5 0V1.75A1.75 1.75 0 0011.25 0h-8.5A1.75 1.75 0 001 1.75v11.5c0 .966.784 1.75 1.75 1.75h3.17a.75.75 0 000-1.5H2.75a.25.25 0 01-.25-.25V1.75zM4.75 4a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-4.5zM4 7.75A.75.75 0 014.75 7h2a.75.75 0 010 1.5h-2A.75.75 0 014 7.75zm11.774 3.537a.75.75 0 00-1.048-1.074L10.7 14.145 9.281 12.72a.75.75 0 00-1.062 1.058l1.943 1.95a.75.75 0 001.055.008l4.557-4.45z"></path></svg>
|
||||
${data.user.contributionsCollection.totalPullRequestReviewContributions} Pull request${data.user.contributionsCollection.totalPullRequestReviewContributions > 1 ? "s" : ""} reviewed
|
||||
</div>
|
||||
<div class="field">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"></path></svg>
|
||||
${data.user.contributionsCollection.totalPullRequestContributions} Pull request${data.user.contributionsCollection.totalPullRequestContributions > 1 ? "s" : ""} opened
|
||||
</div>
|
||||
<div class="field">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"></path></svg>
|
||||
${data.user.contributionsCollection.totalIssueContributions} Issue${data.user.contributionsCollection.totalIssueContributions > 1 ? "s" : ""} opened
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="field">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M1.75 0A1.75 1.75 0 000 1.75v12.5C0 15.216.784 16 1.75 16h12.5A1.75 1.75 0 0016 14.25V1.75A1.75 1.75 0 0014.25 0H1.75zM1.5 1.75a.25.25 0 01.25-.25h12.5a.25.25 0 01.25.25v12.5a.25.25 0 01-.25.25H1.75a.25.25 0 01-.25-.25V1.75zM11.75 3a.75.75 0 00-.75.75v7.5a.75.75 0 001.5 0v-7.5a.75.75 0 00-.75-.75zm-8.25.75a.75.75 0 011.5 0v5.5a.75.75 0 01-1.5 0v-5.5zM8 3a.75.75 0 00-.75.75v3.5a.75.75 0 001.5 0v-3.5A.75.75 0 008 3z"></path></svg>
|
||||
Community stats
|
||||
</h2>
|
||||
<div class="field">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M5.5 3.5a2 2 0 100 4 2 2 0 000-4zM2 5.5a3.5 3.5 0 115.898 2.549 5.507 5.507 0 013.034 4.084.75.75 0 11-1.482.235 4.001 4.001 0 00-7.9 0 .75.75 0 01-1.482-.236A5.507 5.507 0 013.102 8.05 3.49 3.49 0 012 5.5zM11 4a.75.75 0 100 1.5 1.5 1.5 0 01.666 2.844.75.75 0 00-.416.672v.352a.75.75 0 00.574.73c1.2.289 2.162 1.2 2.522 2.372a.75.75 0 101.434-.44 5.01 5.01 0 00-2.56-3.012A3 3 0 0011 4z"></path></svg>
|
||||
Following ${data.user.following.totalCount} user${data.user.followers.totalCount > 1 ? "s" : ""}
|
||||
</div>
|
||||
<div class="field">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.565 20.565 0 008 13.393a20.561 20.561 0 003.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.75.75 0 01-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5zM8 14.25l-.345.666-.002-.001-.006-.003-.018-.01a7.643 7.643 0 01-.31-.17 22.075 22.075 0 01-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.08 22.08 0 01-3.744 2.584l-.018.01-.006.003h-.002L8 14.25zm0 0l.345.666a.752.752 0 01-.69 0L8 14.25z"></path></svg>
|
||||
Sponsoring ${data.computed.sponsorships} repositor${data.computed.sponsorships > 1 ? "ies" : "y"}
|
||||
</div>
|
||||
<div class="field">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"></path></svg>
|
||||
Starred ${data.user.starredRepositories.totalCount} repositor${data.user.starredRepositories.totalCount > 1 ? "ies" : "y"}
|
||||
</div>
|
||||
<div class="field">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M1.679 7.932c.412-.621 1.242-1.75 2.366-2.717C5.175 4.242 6.527 3.5 8 3.5c1.473 0 2.824.742 3.955 1.715 1.124.967 1.954 2.096 2.366 2.717a.119.119 0 010 .136c-.412.621-1.242 1.75-2.366 2.717C10.825 11.758 9.473 12.5 8 12.5c-1.473 0-2.824-.742-3.955-1.715C2.92 9.818 2.09 8.69 1.679 8.068a.119.119 0 010-.136zM8 2c-1.981 0-3.67.992-4.933 2.078C1.797 5.169.88 6.423.43 7.1a1.619 1.619 0 000 1.798c.45.678 1.367 1.932 2.637 3.024C4.329 13.008 6.019 14 8 14c1.981 0 3.67-.992 4.933-2.078 1.27-1.091 2.187-2.345 2.637-3.023a1.619 1.619 0 000-1.798c-.45-.678-1.367-1.932-2.637-3.023C11.671 2.992 9.981 2 8 2zm0 8a2 2 0 100-4 2 2 0 000 4z"></path></svg>
|
||||
Watching ${data.user.watching.totalCount} repositor${data.user.watching.totalCount > 1 ? "ies" : "y"}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2 class="field">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"></path></svg>
|
||||
${data.user.repositories.totalCount} Repositor${data.user.repositories.totalCount > 1 ? "ies" : "y"}
|
||||
</h2>
|
||||
<div class="row">
|
||||
<section>
|
||||
<div class="field">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"></path></svg>
|
||||
${data.computed.repositories.stargazers} Stargazer${data.computed.repositories.stargazers > 1 ? "s" : ""}
|
||||
</div>
|
||||
<div class="field">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M8.878.392a1.75 1.75 0 00-1.756 0l-5.25 3.045A1.75 1.75 0 001 4.951v6.098c0 .624.332 1.2.872 1.514l5.25 3.045a1.75 1.75 0 001.756 0l5.25-3.045c.54-.313.872-.89.872-1.514V4.951c0-.624-.332-1.2-.872-1.514L8.878.392zM7.875 1.69a.25.25 0 01.25 0l4.63 2.685L8 7.133 3.245 4.375l4.63-2.685zM2.5 5.677v5.372c0 .09.047.171.125.216l4.625 2.683V8.432L2.5 5.677zm6.25 8.271l4.625-2.683a.25.25 0 00.125-.216V5.677L8.75 8.432v5.516z"></path></svg>
|
||||
${data.user.packages.totalCount} Package${data.user.packages.totalCount > 1 ? "s" : ""}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="field">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"></path></svg>
|
||||
${data.computed.repositories.forks} Fork${data.computed.repositories.forks > 1 ? "s" : ""}
|
||||
</div>
|
||||
<div class="field">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M1.679 7.932c.412-.621 1.242-1.75 2.366-2.717C5.175 4.242 6.527 3.5 8 3.5c1.473 0 2.824.742 3.955 1.715 1.124.967 1.954 2.096 2.366 2.717a.119.119 0 010 .136c-.412.621-1.242 1.75-2.366 2.717C10.825 11.758 9.473 12.5 8 12.5c-1.473 0-2.824-.742-3.955-1.715C2.92 9.818 2.09 8.69 1.679 8.068a.119.119 0 010-.136zM8 2c-1.981 0-3.67.992-4.933 2.078C1.797 5.169.88 6.423.43 7.1a1.619 1.619 0 000 1.798c.45.678 1.367 1.932 2.637 3.024C4.329 13.008 6.019 14 8 14c1.981 0 3.67-.992 4.933-2.078 1.27-1.091 2.187-2.345 2.637-3.023a1.619 1.619 0 000-1.798c-.45-.678-1.367-1.932-2.637-3.023C11.671 2.992 9.981 2 8 2zm0 8a2 2 0 100-4 2 2 0 000 4z"></path></svg>
|
||||
${data.computed.repositories.watchers} Watcher${data.computed.repositories.watchers > 1 ? "s" : ""}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<section class="column">
|
||||
<h3>Issues</h3>
|
||||
<svg class="bar" xmlns="http://www.w3.org/2000/svg" width="220" height="8">
|
||||
<mask id="issues-bar">
|
||||
<rect x="0" y="0" width="220" height="8" fill="white" rx="5"/>
|
||||
</mask>
|
||||
<rect mask="url(#issues-bar)" x="0" y="0" width="${data.computed.repositories.issues_count ? 0 : 220}" height="8" fill="#d1d5da"/>
|
||||
<rect mask="url(#issues-bar)" x="0" y="0" width="${(data.computed.repositories.issues_closed/data.computed.repositories.issues_count)*220 || 0}" height="8" fill="#d73a49"/>
|
||||
<rect mask="url(#issues-bar)" x="${(data.computed.repositories.issues_closed/data.computed.repositories.issues_count)*220 || 0}" y="0" width="${(1-data.computed.repositories.issues_closed/data.computed.repositories.issues_count)*220 || 0}" height="8" fill="#28a745"/>
|
||||
</svg>
|
||||
<div class="field horizontal" style="width:220">
|
||||
<div class="field center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill="#d73a49" fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 0110.65-5.003.75.75 0 00.959-1.153 8 8 0 102.592 8.33.75.75 0 10-1.444-.407A6.5 6.5 0 011.5 8zM8 12a1 1 0 100-2 1 1 0 000 2zm0-8a.75.75 0 01.75.75v3.5a.75.75 0 11-1.5 0v-3.5A.75.75 0 018 4zm4.78 4.28l3-3a.75.75 0 00-1.06-1.06l-2.47 2.47-.97-.97a.749.749 0 10-1.06 1.06l1.5 1.5a.75.75 0 001.06 0z"></path></svg>
|
||||
${data.computed.repositories.issues_closed} Closed
|
||||
</div>
|
||||
<div class="field center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill="#28a745" fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"></path></svg>
|
||||
${data.computed.repositories.issues_open} Open
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="column">
|
||||
<h3>Pull requests</h3>
|
||||
<svg class="bar" xmlns="http://www.w3.org/2000/svg" width="220" height="8">
|
||||
<mask id="pr-bar">
|
||||
<rect x="0" y="0" width="220" height="8" fill="white" rx="5"/>
|
||||
</mask>
|
||||
<rect mask="url(#pr-bar)" x="0" y="0" width="${data.computed.repositories.pr_count ? 0 : 220}" height="8" fill="#d1d5da"/>
|
||||
<rect mask="url(#pr-bar)" x="0" y="0" width="${(data.computed.repositories.pr_merged/data.computed.repositories.pr_count)*220 || 0}" height="8" fill="#6f42c1"/>
|
||||
<rect mask="url(#pr-bar)" x="${(data.computed.repositories.pr_merged/data.computed.repositories.pr_count)*220 || 0}" y="0" width="${(1-data.computed.repositories.pr_merged/data.computed.repositories.pr_count)*220 || 0}" height="8" fill="#28a745"/>
|
||||
</svg>
|
||||
<div class="field horizontal" style="width:220">
|
||||
<div class="field center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill="#6f42c1" fill-rule="evenodd" d="M5 3.254V3.25v.005a.75.75 0 110-.005v.004zm.45 1.9a2.25 2.25 0 10-1.95.218v5.256a2.25 2.25 0 101.5 0V7.123A5.735 5.735 0 009.25 9h1.378a2.251 2.251 0 100-1.5H9.25a4.25 4.25 0 01-3.8-2.346zM12.75 9a.75.75 0 100-1.5.75.75 0 000 1.5zm-8.5 4.5a.75.75 0 100-1.5.75.75 0 000 1.5z"></path></svg>
|
||||
${data.computed.repositories.pr_merged} Merged
|
||||
</div>
|
||||
<div class="field center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill="#28a745" fill-rule="evenodd" d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"></path></svg>
|
||||
${data.computed.repositories.pr_open} Open
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<section class="column">
|
||||
<h3>Most used languages</h3>
|
||||
<svg class="bar" xmlns="http://www.w3.org/2000/svg" width="460" height="8">
|
||||
<mask id="languages-bar">
|
||||
<rect x="0" y="0" width="460" height="8" fill="white" rx="5"/>
|
||||
</mask>
|
||||
<rect mask="url(#languages-bar)" x="0" y="0" width="${data.computed.languages.favorites.length ? 0 : 460}" height="8" fill="#d1d5da"/>
|
||||
${data.computed.languages.favorites.map(({name, value, color, x}) => `
|
||||
<rect mask="url(#languages-bar)" x="${x*460}" y="0" width="${value*460}" height="8" fill="${color}"/>
|
||||
`).join("")}
|
||||
</svg>
|
||||
<div class="field horizontal horizontal-wrap" style="width:460">
|
||||
${data.computed.languages.favorites.map(({name, color}) => `
|
||||
<div class="field center no-wrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill="${color}" fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"></path></svg>
|
||||
${name}
|
||||
</div>
|
||||
`).join("")}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
Last updated ${new Date()}
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</foreignObject>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 19 KiB |
12
utils/post_build.mjs
Normal file
12
utils/post_build.mjs
Normal file
@@ -0,0 +1,12 @@
|
||||
//Imports
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
|
||||
//Perform static includes
|
||||
let generated = `${await fs.promises.readFile(path.join("action/dist", "index.js"))}`
|
||||
for (const match of [...generated.match(/(?<=`)<#include (.+?)>(?=`)/g)]) {
|
||||
const file = match.match(/<#include (.+?)>/)[1]
|
||||
generated = generated.replace(`<#include ${file}>`, `${await fs.promises.readFile(path.join("src", file))}`.replace(/([$`])/g, "\\$1"))
|
||||
console.log(`Included ${file}`)
|
||||
}
|
||||
await fs.promises.writeFile(path.join("action/dist", "index.js"), generated)
|
||||
Reference in New Issue
Block a user