The great refactor (#82)

This commit is contained in:
Simon Lecoq
2021-01-30 12:31:09 +01:00
committed by GitHub
parent f8c6d19a4e
commit 682e43e10b
158 changed files with 6738 additions and 5022 deletions

View File

@@ -0,0 +1,87 @@
### 🧱 Core
Metrics also have general options that impact global metrics rendering.
[➡️ Available options](metadata.yml)
### 🌐 Set timezone
By default, dates are based on Greenwich meridian (GMT/UTC).
Set your timezone (see [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for a list of supported timezones) using `config_timezone` option.
#### Examples workflows
```yaml
- uses: lowlighter/metrics@latest
with:
# ... other options
config_timezone: Europe/Paris
```
### 📦 Ordering content
You can order metrics content by using `config_order` option.
It is not mandatory to specify all partials of used templates.
Omitted one will be appended using default order.
#### Examples workflows
```yaml
- uses: lowlighter/metrics@latest
with:
# ... other options
base: header
plugin_isocalendar: yes
plugin_languages: yes
plugin_stars: yes
config_order: base.header, isocalendar, languages, stars
```
### 🎞️ SVG CSS Animations
As rendered metrics use HTML and CSS, some templates have animations.
You can choose to disable them by using `config_animations` option.
#### Examples workflows
```yaml
- uses: lowlighter/metrics@latest
with:
# ... other options
committer_branch: my-branch
```
### 🔲 Adjust padding
Height of rendered metrics is computed after being rendered through an headless browser.
As it can depend on fonts and operating system, it is possible that final result is cropped or has blank space at the bottom.
You can adjust padding by using `config_padding` option.
Specify a single value to apply it to both height and with, and two values to use the first one for width and the second for height. Both positive and negative values are accepted, but you must specify a percentage.
#### Examples workflows
```yaml
- uses: lowlighter/metrics@latest
with:
# ... other options
config_padding: 6%, 10% # 6% width padding, 10% height padding
```
### 💱 Convert output to PNG/JPEG
It is possible to convert output from SVG to PNG or JPEG images by using `config_output` option.
Note that `png` does not support animations while `jpeg` does not support both animations and transparency.
#### Examples workflows
```yaml
- uses: lowlighter/metrics@latest
with:
# ... other options
config_output: png
```

View File

@@ -0,0 +1,130 @@
/**
* Core plugin is a special plugin because of historical reasons.
* It is used by templates to setup global configuration.
*/
//Setup
export default async function ({login, q, dflags}, {conf, data, rest, graphql, plugins, queries, account}, {pending, imports}) {
//Load inputs
imports.metadata.plugins.core.inputs({data, account, q})
//Init
const computed = data.computed = {commits:0, sponsorships:0, licenses:{favorite:"", used:{}}, token:{}, repositories:{watchers:0, stargazers:0, issues_open:0, issues_closed:0, pr_open:0, pr_merged:0, forks:0, forked:0, releases:0}}
const avatar = imports.imgb64(data.user.avatarUrl)
console.debug(`metrics/compute/${login} > formatting common metrics`)
//Timezone config
if (q["config.timezone"]) {
const timezone = data.config.timezone = {name:q["config.timezone"], offset:0}
try {
timezone.offset = Number(new Date().toLocaleString("fr", {timeZoneName:"short", timeZone:timezone.name}).match(/UTC[+](?<offset>\d+)/)?.groups?.offset*60*60*1000) || 0
console.debug(`metrics/compute/${login} > timezone set to ${timezone.name} (${timezone.offset > 0 ? "+" : ""}${Math.round(timezone.offset/(60*60*1000))} hours)`)
} catch {
timezone.error = `Failed to use timezone "${timezone.name}"`
console.debug(`metrics/compute/${login} > failed to use timezone "${timezone.name}"`)
}
}
//Animations
if ("config.animations" in q) {
data.animated = q["config.animations"]
console.debug(`metrics/compute/${login} > animations ${data.animated ? "enabled" : "disabled"}`)
}
//Plugins
for (const name of Object.keys(imports.plugins)) {
if (!plugins[name]?.enabled)
continue
pending.push((async () => {
try {
console.debug(`metrics/compute/${login}/plugins > ${name} > started`)
data.plugins[name] = await imports.plugins[name]({login, q, imports, data, computed, rest, graphql, queries, account}, plugins[name])
console.debug(`metrics/compute/${login}/plugins > ${name} > completed`)
}
catch (error) {
console.debug(`metrics/compute/${login}/plugins > ${name} > completed (error)`)
data.plugins[name] = error
}
finally {
const result = {name, result:data.plugins[name]}
console.debug(imports.util.inspect(result, {depth:Infinity, maxStringLength:256}))
return result
}
})())
}
//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", "releases"])
computed.repositories[property] += repository[property].totalCount
//Forks
computed.repositories.forks += repository.forkCount
if (repository.isFork)
computed.repositories.forked++
//License
if (repository.licenseInfo)
computed.licenses.used[repository.licenseInfo.spdxId] = (computed.licenses.used[repository.licenseInfo.spdxId] ?? 0) + 1
}
//Total disk usage
computed.diskUsage = `${imports.bytes(data.user.repositories.totalDiskUsage*1000)}`
//Compute licenses stats
computed.licenses.favorite = Object.entries(computed.licenses.used).sort(([an, a], [bn, b]) => b - a).slice(0, 1).map(([name, value]) => name) ?? ""
//Compute total commits
computed.commits += data.user.contributionsCollection.totalCommitContributions + data.user.contributionsCollection.restrictedContributionsCount
//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.floor((diff-years)*12)
computed.registration = years ? `${years} year${imports.s(years)} ago` : months ? `${months} month${imports.s(months)} ago` : `${Math.ceil(diff*365)} day${imports.s(Math.ceil(diff*365))} ago`
computed.cakeday = years > 1 ? [new Date(), new Date(data.user.createdAt)].map(date => date.toISOString().match(/(?<mmdd>\d{2}-\d{2})(?=T)/)?.groups?.mmdd).every((v, _, a) => v === a[0]) : false
//Compute calendar
computed.calendar = data.user.calendar.contributionCalendar.weeks.flatMap(({contributionDays}) => contributionDays).slice(0, 14).reverse()
//Avatar (base64)
computed.avatar = await avatar || "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
//Token scopes
computed.token.scopes = (await rest.request("HEAD /")).headers["x-oauth-scopes"].split(", ")
//Meta
data.meta = {version:conf.package.version, author:conf.package.author}
//Debug flags
if ((dflags.includes("--cakeday"))||(q["dflag.cakeday"])) {
console.debug(`metrics/compute/${login} > applying dflag --cakeday`)
computed.cakeday = true
}
if ((dflags.includes("--hireable"))||(q["dflag.hireable"])) {
console.debug(`metrics/compute/${login} > applying dflag --hireable`)
data.user.isHireable = true
}
if ((dflags.includes("--halloween"))||(q["dflag.halloween"])) {
console.debug(`metrics/compute/${login} > applying dflag --halloween`)
//Haloween color replacer
const halloween = content => content
.replace(/--color-calendar-graph/g, "--color-calendar-halloween-graph")
.replace(/#9be9a8/gi, "var(--color-calendar-halloween-graph-day-L1-bg)")
.replace(/#40c463/gi, "var(--color-calendar-halloween-graph-day-L2-bg)")
.replace(/#30a14e/gi, "var(--color-calendar-halloween-graph-day-L3-bg)")
.replace(/#216e39/gi, "var(--color-calendar-halloween-graph-day-L4-bg)")
//Update contribution calendar colors
computed.calendar.map(day => day.color = halloween(day.color))
//Update isocalendar colors
const waiting = [...pending]
pending.push((async () => {
await Promise.all(waiting)
if (data.plugins.isocalendar?.svg)
data.plugins.isocalendar.svg = halloween(data.plugins.isocalendar.svg)
return {name:"dflag.halloween", result:true}
})())
}
//Results
return null
}

View File

@@ -0,0 +1,164 @@
name: "🧱 Core"
cost: N/A
supports:
- user
- organization
- repository
inputs:
# User account personal token
# No additional scopes are needed unless you want to include private repositories metrics
# Some plugins may also require additional scopes
token:
description: GitHub Personal Token
type: token
required: true
# GitHub username
user:
description: GitHub username
type: string
default: "" # Defaults to "token" owner
# Set to "${{ secrets.GITHUB_TOKEN }}"
committer_token:
description: GitHub Token used to commit metrics
type: token
default: "" # Defaults to "token"
# Branch used to commit rendered metrics
committer_branch:
description: Branch used to commit rendered metrics
type: string
default: "" # Defaults to your repository default branch
# Rendered metrics output path, relative to repository's root
filename:
description: Rendered metrics output path
type: string
default: github-metrics.svg
# Optimize SVG image to reduce its filesize
# Some templates may not support this option
optimize:
description: SVG optimization
type: boolean
default: yes
# Setup additional templates from remote repositories
setup_community_templates:
description: Additional community templates to setup
type: array
format:
- comma-separated
- /(?<user>[-a-z0-9]+)[/](?<repo>[-a-z0-9]+)@(?<branch>[-a-z0-9]+):(?<template>[-a-z0-9]+)/
default: ""
# Template to use
# To use community template, prefix its name with "@"
template:
description: Template to use
type: string
default: classic
# Additional query parameters (JSON string)
# Some templates may require additional parameters which you can specify here
# Do not use this option to pass plugins parameters as they'll be overwritten by the other options
query:
description: Additional query parameters
type: json
default: "{}"
# Timezone used by metrics
# See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
config_timezone:
description: Timezone used
type: string
default: ""
# Specify in which order metrics content will be displayed
# If you omit some partials, they'll be appended at the end in default order
# See "partials/_.json" of each template for a list of supported partials
config_order:
description: Configure content order
type: array
format: comma-separated
default: ""
# Enable SVG CSS animations
config_animations:
description: SVG CSS animations
type: boolean
default: yes
# Configure padding for output image (percentage value)
# It can be used to add padding to generated metrics if rendering is cropped or has too much empty space
# Specify one value (for both width and height) or two values (one for width and one for height)
config_padding:
description: Image padding
type: array
format: comma-separated
default: 6%
# Metrics output format
config_output:
description: Output image format
type: string
default: svg
values:
- svg
- png # Does not support animations
- jpeg # Does not support animations and transparency
# ====================================================================================
# Options below are mostly used for testing
# Throw on plugins errors
# If disabled, metrics will handle errors gracefully with a message in rendered metrics
plugins_errors_fatal:
description: Die on plugins errors
type: boolean
default: no
# Debug mode
# Note that this will automatically be enabled if job fails
debug:
description: Debug logs
type: boolean
default: no
# Ensure SVG can be correctly parsed after generation
verify:
description: Verify SVG
type: boolean
default: no
# Debug flags
debug_flags:
description: Debug flags
type: array
format: space-separated
default: ""
values:
- --cakeday
- --hireable
- --halloween
# Dry-run mode (perform generation without pushing it)
dryrun:
description: Enable dry-run
type: boolean
default: no
# Use mocked data to bypass external APIs
use_mocked_data:
description: Use mocked data instead of live APIs
type: boolean
default: no
# Use a pre-built image from GitHub registry (experimental)
# See https://github.com/users/lowlighter/packages/container/package/metrics for more information
use_prebuilt_image:
description: Use pre-built image from GitHub registry
type: string
default: ""