The great refactor (#82)
This commit is contained in:
25
source/plugins/README.md
Normal file
25
source/plugins/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
## 🧩 Plugins
|
||||
|
||||
Plugins are features which provide additional content and lets you customize your rendered metrics.
|
||||
See their respective documentation for more informations about how to setup them:
|
||||
|
||||
* [🗃️ Base content](/source/plugins/base/README.md)
|
||||
* [🧱 Core](/source/plugins/core/README.md)
|
||||
* [📰 Recent activity](/source/plugins/activity/README.md)
|
||||
* [🌸 Anilist](/source/plugins/anilist/README.md)
|
||||
* [🎟️ Follow-up of issues and pull requests](/source/plugins/followup/README.md)
|
||||
* [🎫 Gists](/source/plugins/gists/README.md)
|
||||
* [💡 Coding habits](/source/plugins/habits/README.md)
|
||||
* [📅 Isometric commit calendar](/source/plugins/isocalendar/README.md)
|
||||
* [🈷️ Most used languages](/source/plugins/languages/README.md)
|
||||
* [👨💻 Lines of code changed](/source/plugins/lines/README.md)
|
||||
* [🎼 Music plugin](/source/plugins/music/README.md)
|
||||
* [⏱️ Website performances](/source/plugins/pagespeed/README.md)
|
||||
* [🧑🤝🧑 People plugin](/source/plugins/people/README.md)
|
||||
* [✒️ Recent posts](/source/plugins/posts/README.md)
|
||||
* [🗂️ Projects](/source/plugins/projects/README.md)
|
||||
* [✨ Stargazers over last weeks](/source/plugins/stargazers/README.md)
|
||||
* [🌟 Recently starred repositories](/source/plugins/stars/README.md)
|
||||
* [📌 Starred topics](/source/plugins/topics/README.md)
|
||||
* [🧮 Repositories traffic](/source/plugins/traffic/README.md)
|
||||
* [🐤 Latest tweets](/source/plugins/tweets/README.md)
|
||||
44
source/plugins/activity/README.md
Normal file
44
source/plugins/activity/README.md
Normal file
@@ -0,0 +1,44 @@
|
||||
### 📰 Recent activity
|
||||
|
||||
The *activity* plugin displays your recent activity on GitHub.
|
||||
|
||||
<table>
|
||||
<td align="center">
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.activity.svg">
|
||||
<img width="900" height="1" alt="">
|
||||
</td>
|
||||
</table>
|
||||
|
||||
It uses data from [GitHub events](https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/github-event-types) and is able to track the following events:
|
||||
|
||||
| Event | Description |
|
||||
| ------------ | ----------------------------------------------- |
|
||||
| `push` | Push of commits |
|
||||
| `issue` | Opening/Reopening/Closing of issues |
|
||||
| `pr` | Opening/Closing of pull requests |
|
||||
| `ref/create` | Creation of git tags or git branches |
|
||||
| `ref/delete` | Deletion of git tags or git branches |
|
||||
| `release` | Publication of new releases |
|
||||
| `review` | Review of pull requests |
|
||||
| `comment` | Comments on commits, issues and pull requests |
|
||||
| `wiki` | Edition of wiki pages |
|
||||
| `fork` | Forking of repositories |
|
||||
| `star` | Starring of repositories |
|
||||
| `public` | Repositories made public |
|
||||
| `member` | Addition of new collaborator in repository |
|
||||
|
||||
Use a full `repo` scope token to display **private** events.
|
||||
|
||||
#### ℹ️ Examples workflows
|
||||
|
||||
[➡️ Available options for this plugin](metadata.yml)
|
||||
|
||||
```yaml
|
||||
- uses: lowlighter/metrics@latest
|
||||
with:
|
||||
# ... other options
|
||||
plugin_activity: yes
|
||||
plugin_activity_limit: 5 # Limit to 5 events
|
||||
plugin_activity_days: 14 # Keep only events from last 14 days (can be set to 0 to disable limitations)
|
||||
plugin_activity_filter: all # Show all events (use table above to filter events types)
|
||||
```
|
||||
@@ -1,24 +1,21 @@
|
||||
//Setup
|
||||
export default async function ({login, rest, q, account}, {enabled = false} = {}) {
|
||||
export default async function ({login, data, rest, q, account, imports}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.activity))
|
||||
return null
|
||||
|
||||
//Parameters override
|
||||
let {"activity.limit":limit = 5, "activity.days":days = 7, "activity.filter":filter = "all"} = q
|
||||
//Events
|
||||
limit = Math.max(1, Math.min(100, Number(limit)))
|
||||
//Days
|
||||
days = Number(days) > 0 ? Number(days) : Infinity
|
||||
//Filtered events
|
||||
filter = decodeURIComponent(filter).split(",").map(x => x.trim().toLocaleLowerCase()).filter(x => x)
|
||||
//Load inputs
|
||||
let {limit, days, filter} = imports.metadata.plugins.activity.inputs({data, q, account})
|
||||
if (!days)
|
||||
days = Infinity
|
||||
|
||||
//Get user recent activity
|
||||
console.debug(`metrics/compute/${login}/plugins > activity > querying api`)
|
||||
const {data:events} = await rest.activity.listEventsForAuthenticatedUser({username:login, per_page:100})
|
||||
console.debug(`metrics/compute/${login}/plugins > activity > ${events.length} events loaded`)
|
||||
|
||||
//Extract activity events
|
||||
const activity = events
|
||||
.filter(({actor}) => account === "organization" ? true : actor.login === login)
|
||||
|
||||
51
source/plugins/activity/metadata.yml
Normal file
51
source/plugins/activity/metadata.yml
Normal file
@@ -0,0 +1,51 @@
|
||||
name: "📰 Recent activity"
|
||||
cost: 1 REST request per 100 events
|
||||
supports:
|
||||
- user
|
||||
- organization
|
||||
inputs:
|
||||
|
||||
# Enable or disable plugin
|
||||
plugin_activity:
|
||||
description: Display recent activity
|
||||
type: boolean
|
||||
default: no
|
||||
|
||||
# Number of activity events to display
|
||||
plugin_activity_limit:
|
||||
description: Maximum number of events to display
|
||||
type: number
|
||||
default: 5
|
||||
min: 1
|
||||
max: 100
|
||||
|
||||
# Filter events by age
|
||||
# Set to 0 to disable age filtering
|
||||
plugin_activity_days:
|
||||
description: Maximum event age
|
||||
type: number
|
||||
default: 14
|
||||
min: 0
|
||||
max: 365
|
||||
|
||||
# Filter events by type
|
||||
plugin_activity_filter:
|
||||
description: Events types to keep
|
||||
type: array
|
||||
format: comma-separated
|
||||
default: all
|
||||
values:
|
||||
- all # Display all types of events
|
||||
- comment # Display commits, issues and pull requests comments
|
||||
- ref/create # Display tags and branches creations
|
||||
- ref/delete # Display tags and branches deletions
|
||||
- release # Display published releases
|
||||
- push # Display commits
|
||||
- issue # Display issues events
|
||||
- pr # Display pull requests events
|
||||
- review # Display pull request reviews
|
||||
- wiki # Display wiki editions
|
||||
- fork # Display forked repositories
|
||||
- star # Display starred repositories
|
||||
- member # Display collaborators additions
|
||||
- public # Display repositories made public
|
||||
40
source/plugins/anilist/README.md
Normal file
40
source/plugins/anilist/README.md
Normal file
@@ -0,0 +1,40 @@
|
||||
### 🌸 Anilist <sup>🚧 pre-release on <code>@master</code></sup>
|
||||
|
||||
The *anilist* plugin lets you display your favorites animes, mangas and characters from your [AniList](https://anilist.co) account.
|
||||
|
||||
<table>
|
||||
<td align="center">
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.anilist.svg">
|
||||
<details><summary>Manga version</summary>
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.anilist.manga.svg">
|
||||
</details>
|
||||
<details open><summary>Favorites characters version</summary>
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.anilist.characters.svg">
|
||||
</details>
|
||||
<img width="900" height="1" alt="">
|
||||
</td>
|
||||
</table>
|
||||
|
||||
This plugin is composed of the following sections, which can be displayed or hidden through `plugin_anilist_sections` option:
|
||||
- `favorites` will display your favorites mangas and animes
|
||||
- `watching` will display animes currently in your watching list
|
||||
- `reading` will display manga currently in your reading list
|
||||
- `characters` will display characters you liked
|
||||
|
||||
These sections can also be filtered by media type, which can be either `anime`, `manga` or both.
|
||||
|
||||
#### ℹ️ Examples workflows
|
||||
|
||||
[➡️ Available options for this plugin](metadata.yml)
|
||||
|
||||
```yaml
|
||||
- uses: lowlighter/metrics@latest
|
||||
with:
|
||||
# ... other options
|
||||
plugin_anilist: yes
|
||||
plugin_anilist_medias: anime, manga # Display both animes and mangas
|
||||
plugin_anilist_sections: favorites, characters # Display only favorites and characters sections
|
||||
plugin_anilist_limit: 2 # Limit to 2 entry per section (characters section excluded)
|
||||
plugin_anilist_shuffle: yes # Shuffle data for more varied outputs
|
||||
plugin_anilist_user: .user.login # Use same username as GitHub login
|
||||
```
|
||||
@@ -1,42 +1,33 @@
|
||||
//Setup
|
||||
export default async function ({login, imports, q}, {enabled = false} = {}) {
|
||||
export default async function ({login, data, queries, imports, q, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.anilist))
|
||||
return null
|
||||
//Parameters override
|
||||
let {"anilist.medias":medias = ["anime", "manga"], "anilist.sections":sections = ["favorites"], "anilist.limit":limit = 2, "anilist.shuffle":shuffle = true, "anilist.user":user = login} = q
|
||||
//Medias types
|
||||
medias = decodeURIComponent(medias).split(",").map(x => x.trim().toLocaleLowerCase()).filter(x => ["anime", "manga"].includes(x))
|
||||
//Sections
|
||||
sections = decodeURIComponent(sections).split(",").map(x => x.trim().toLocaleLowerCase()).filter(x => ["favorites", "watching", "reading", "characters"].includes(x))
|
||||
//Limit medias
|
||||
limit = Math.max(0, Number(limit))
|
||||
//GraphQL queries
|
||||
const query = {
|
||||
statistics:`${await imports.fs.readFile(`${imports.__module(import.meta.url)}/queries/statistics.graphql`)}`,
|
||||
characters:`${await imports.fs.readFile(`${imports.__module(import.meta.url)}/queries/characters.graphql`)}`,
|
||||
medias:`${await imports.fs.readFile(`${imports.__module(import.meta.url)}/queries/medias.graphql`)}`,
|
||||
favorites:`${await imports.fs.readFile(`${imports.__module(import.meta.url)}/queries/favorites.graphql`)}`,
|
||||
}
|
||||
|
||||
//Load inputs
|
||||
let {limit, medias, sections, shuffle, user} = imports.metadata.plugins.anilist.inputs({data, account, q})
|
||||
|
||||
//Initialization
|
||||
const result = {user:{stats:null, genres:[]}, lists:Object.fromEntries(medias.map(type => [type, {}])), characters:[], sections}
|
||||
|
||||
//User statistics
|
||||
{
|
||||
//Query API
|
||||
console.debug(`metrics/compute/${login}/plugins > anilist > querying api (user statistics)`)
|
||||
const {data:{data:{User:{statistics:stats}}}} = await imports.axios.post("https://graphql.anilist.co", {variables:{name:user}, query:query.statistics})
|
||||
const {data:{data:{User:{statistics:stats}}}} = await imports.axios.post("https://graphql.anilist.co", {variables:{name:user}, query:queries.anilist.statistics()})
|
||||
//Format and save results
|
||||
result.user.stats = stats
|
||||
result.user.genres = [...new Set([...stats.anime.genres.map(({genre}) => genre), ...stats.manga.genres.map(({genre}) => genre)])]
|
||||
}
|
||||
|
||||
//Medias lists
|
||||
if ((sections.includes("watching"))||(sections.includes("reading"))) {
|
||||
for (const type of medias) {
|
||||
//Query API
|
||||
console.debug(`metrics/compute/${login}/plugins > anilist > querying api (medias lists - ${type})`)
|
||||
const {data:{data:{MediaListCollection:{lists}}}} = await imports.axios.post("https://graphql.anilist.co", {variables:{name:user, type:type.toLocaleUpperCase()}, query:query.medias})
|
||||
const {data:{data:{MediaListCollection:{lists}}}} = await imports.axios.post("https://graphql.anilist.co", {variables:{name:user, type:type.toLocaleUpperCase()}, query:queries.anilist.medias()})
|
||||
//Format and save results
|
||||
for (const {name, entries} of lists) {
|
||||
//Format results
|
||||
@@ -50,6 +41,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Favorites anime/manga
|
||||
if (sections.includes("favorites")) {
|
||||
for (const type of medias) {
|
||||
@@ -60,7 +52,7 @@
|
||||
let next = false
|
||||
do {
|
||||
console.debug(`metrics/compute/${login}/plugins > anilist > querying api (favorites ${type}s - page ${page})`)
|
||||
const {data:{data:{User:{favourites:{[type]:{nodes, pageInfo:cursor}}}}}} = await imports.axios.post("https://graphql.anilist.co", {variables:{name:user, page}, query:query.favorites.replace(/[$]type/g, type)})
|
||||
const {data:{data:{User:{favourites:{[type]:{nodes, pageInfo:cursor}}}}}} = await imports.axios.post("https://graphql.anilist.co", {variables:{name:user, page}, query:queries.anilist.favorites({type})})
|
||||
page = cursor.currentPage
|
||||
next = cursor.hasNextPage
|
||||
list.push(...await Promise.all(nodes.map(media => format({media:{progess:null, score:null, media}, imports}))))
|
||||
@@ -74,6 +66,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Favorites characters
|
||||
if (sections.includes("characters")) {
|
||||
//Query API
|
||||
@@ -83,7 +76,7 @@
|
||||
let next = false
|
||||
do {
|
||||
console.debug(`metrics/compute/${login}/plugins > anilist > querying api (favorites characters - page ${page})`)
|
||||
const {data:{data:{User:{favourites:{characters:{nodes, pageInfo:cursor}}}}}} = await imports.axios.post("https://graphql.anilist.co", {variables:{name:user, page}, query:query.characters})
|
||||
const {data:{data:{User:{favourites:{characters:{nodes, pageInfo:cursor}}}}}} = await imports.axios.post("https://graphql.anilist.co", {variables:{name:user, page}, query:queries.anilist.characters()})
|
||||
page = cursor.currentPage
|
||||
next = cursor.hasNextPage
|
||||
for (const {name:{full:name}, image:{medium:artwork}} of nodes)
|
||||
@@ -92,6 +85,7 @@
|
||||
//Format and save results
|
||||
result.characters = shuffle ? imports.shuffle(characters) : characters
|
||||
}
|
||||
|
||||
//Results
|
||||
return result
|
||||
}
|
||||
|
||||
55
source/plugins/anilist/metadata.yml
Normal file
55
source/plugins/anilist/metadata.yml
Normal file
@@ -0,0 +1,55 @@
|
||||
name: "🌸 Anilist"
|
||||
cost: N/A
|
||||
supports:
|
||||
- user
|
||||
- organization
|
||||
inputs:
|
||||
|
||||
# Enable or disable plugin
|
||||
plugin_anilist:
|
||||
description: Display data from your AniList account
|
||||
type: boolean
|
||||
default: no
|
||||
|
||||
# Types of medias to display
|
||||
plugin_anilist_medias:
|
||||
description: Medias types to display
|
||||
type: array
|
||||
format: comma-separated
|
||||
default: anime, manga
|
||||
values:
|
||||
- anime
|
||||
- manga
|
||||
|
||||
# Sections to display
|
||||
# Values from "plugin_anilist_medias" may impact displayed sections
|
||||
plugin_anilist_sections:
|
||||
description: Sections to display
|
||||
type: array
|
||||
format: comma-separated
|
||||
default: favorites
|
||||
values:
|
||||
- favorites # Favorites animes and mangas (depending on plugin_anilist_medias values)
|
||||
- watching # Animes in your watching list
|
||||
- reading # Mangas in your reading list
|
||||
- characters # Favorites characters
|
||||
|
||||
# Number of entries to display per section (this does not impacts characters section)
|
||||
# Set to 0 to disable limitations
|
||||
plugin_anilist_limit:
|
||||
description: Maximum number of entries to display per section
|
||||
type: number
|
||||
default: 2
|
||||
min: 0
|
||||
|
||||
# Shuffle AniList data for varied outputs
|
||||
plugin_anilist_shuffle:
|
||||
description: Shuffle AniList data
|
||||
type: boolean
|
||||
default: yes
|
||||
|
||||
# Username on AniList
|
||||
plugin_anilist_user:
|
||||
type: string
|
||||
description: AniList login
|
||||
default: .user.login
|
||||
38
source/plugins/base/README.md
Normal file
38
source/plugins/base/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
### 🗃️ Base content
|
||||
|
||||
The *base* content is all metrics enabled by default.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.classic.svg">
|
||||
<img width="900" height="1" alt="">
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.organization.svg">
|
||||
<img width="900" height="1" alt="">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
It contains the following sections:
|
||||
* `header`, which usually contains your username, your two-week commits calendars and a few additional data
|
||||
* `activity`, which contains your recent activity (commits, pull requests, issues, etc.)
|
||||
* `community`, which contains your community stats (following, sponsors, organizations, etc.)
|
||||
* `repositories`, which contains your repositories stats (license, forks, stars, etc.)
|
||||
* `metadata`, which contains informations about generated metrics
|
||||
|
||||
These are all enabled by default, but you can explicitely opt out from them.
|
||||
|
||||
#### ℹ️ Examples workflows
|
||||
|
||||
[➡️ Available options for this plugin](metadata.yml)
|
||||
|
||||
```yaml
|
||||
- uses: lowlighter/metrics@latest
|
||||
with:
|
||||
# ... other options
|
||||
base: header, repositories # Only display "header" and "repositories" sections
|
||||
repositories: 100 # Query only last 100 repositories
|
||||
repositories_forks: no # Don't include forks
|
||||
```
|
||||
89
source/plugins/base/index.mjs
Normal file
89
source/plugins/base/index.mjs
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Base plugin is a special plugin because of historical reasons.
|
||||
* It populates initial data object directly instead of returning a result like others plugins
|
||||
*/
|
||||
|
||||
//Setup
|
||||
export default async function ({login, graphql, data, q, queries, imports}, conf) {
|
||||
//Load inputs
|
||||
console.debug(`metrics/compute/${login}/base > started`)
|
||||
let {repositories, repositories_forks:forks} = imports.metadata.plugins.base.inputs({data, q, account:"bypass"}, {repositories:conf.settings.repositories ?? 100})
|
||||
|
||||
//Base parts (legacy handling for web instance)
|
||||
const defaulted = ("base" in q) ? !!q.base : true
|
||||
for (const part of conf.settings.plugins.base.parts)
|
||||
data.base[part] = `base.${part}` in q ? !!q[ `base.${part}`] : defaulted
|
||||
|
||||
//Iterate through account types
|
||||
for (const account of ["user", "organization"]) {
|
||||
try {
|
||||
//Query data from GitHub API
|
||||
console.debug(`metrics/compute/${login}/base > account ${account}`)
|
||||
const queried = await graphql(queries.base[account]({login, "calendar.from":new Date(Date.now()-14*24*60*60*1000).toISOString(), "calendar.to":(new Date()).toISOString(), forks:forks ? "" : ", isFork: false"}))
|
||||
Object.assign(data, {user:queried[account]})
|
||||
postprocess?.[account]({login, data})
|
||||
//Query repositories from GitHub API
|
||||
{
|
||||
//Iterate through repositories
|
||||
let cursor = null
|
||||
let pushed = 0
|
||||
do {
|
||||
console.debug(`metrics/compute/${login}/base > retrieving repositories after ${cursor}`)
|
||||
const {[account]:{repositories:{edges, nodes}}} = await graphql(queries.base.repositories({login, account, after:cursor ? `after: "${cursor}"` : "", repositories:Math.min(repositories, {user:100, organization:25}[account]), forks:forks ? "" : ", isFork: false"}))
|
||||
cursor = edges?.[edges?.length-1]?.cursor
|
||||
data.user.repositories.nodes.push(...nodes)
|
||||
pushed = nodes.length
|
||||
} while ((pushed)&&(cursor)&&(data.user.repositories.nodes.length < repositories))
|
||||
//Limit repositories
|
||||
console.debug(`metrics/compute/${login}/base > keeping only ${repositories} repositories`)
|
||||
data.user.repositories.nodes.splice(repositories)
|
||||
console.debug(`metrics/compute/${login}/base > loaded ${data.user.repositories.nodes.length} repositories`)
|
||||
}
|
||||
//Success
|
||||
console.debug(`metrics/compute/${login}/base > graphql query > account ${account} > success`)
|
||||
return {}
|
||||
} catch (error) {
|
||||
console.debug(`metrics/compute/${login}/base > account ${account} > failed : ${error}`)
|
||||
console.debug(`metrics/compute/${login}/base > checking next account`)
|
||||
}
|
||||
}
|
||||
//Not found
|
||||
console.debug(`metrics/compute/${login}/base > no more account type`)
|
||||
throw new Error("user not found")
|
||||
}
|
||||
|
||||
//Query post-processing
|
||||
const postprocess = {
|
||||
//User
|
||||
user({login, data}) {
|
||||
console.debug(`metrics/compute/${login}/base > applying postprocessing`)
|
||||
data.account = "user"
|
||||
Object.assign(data.user, {
|
||||
isVerified:false,
|
||||
})
|
||||
},
|
||||
//Organization
|
||||
organization({login, data}) {
|
||||
console.debug(`metrics/compute/${login}/base > applying postprocessing`)
|
||||
data.account = "organization",
|
||||
Object.assign(data.user, {
|
||||
isHireable:false,
|
||||
starredRepositories:{totalCount:0},
|
||||
watching:{totalCount:0},
|
||||
contributionsCollection:{
|
||||
totalRepositoriesWithContributedCommits:0,
|
||||
totalCommitContributions:0,
|
||||
restrictedContributionsCount:0,
|
||||
totalIssueContributions:0,
|
||||
totalPullRequestContributions:0,
|
||||
totalPullRequestReviewContributions:0,
|
||||
},
|
||||
calendar:{contributionCalendar:{weeks:[]}},
|
||||
repositoriesContributedTo:{totalCount:0},
|
||||
followers:{totalCount:0},
|
||||
following:{totalCount:0},
|
||||
issueComments:{totalCount:0},
|
||||
organizations:{totalCount:0},
|
||||
})
|
||||
}
|
||||
}
|
||||
34
source/plugins/base/metadata.yml
Normal file
34
source/plugins/base/metadata.yml
Normal file
@@ -0,0 +1,34 @@
|
||||
name: "🗃️ Base content"
|
||||
cost: 1 GraphQL request
|
||||
supports:
|
||||
- user
|
||||
- organization
|
||||
- repository
|
||||
inputs:
|
||||
|
||||
# Base content
|
||||
base:
|
||||
description: Metrics base content
|
||||
type: array
|
||||
format: comma-separated
|
||||
default: header, activity, community, repositories, metadata
|
||||
values:
|
||||
- header # name, commits calendar, ...
|
||||
- activity # commits, issues/pull requests opened, ...
|
||||
- community # following, stars, sponsors, ...
|
||||
- repositories # license, stars, forks, ...
|
||||
- metadata # svg generation metadata
|
||||
|
||||
# Number of repositories to use to computes metrics
|
||||
# Using more will result in more accurate metrics, but you may hit GitHub rate-limit more easily if you use a lot of plugins
|
||||
repositories:
|
||||
description: Number of repositories to use
|
||||
type: number
|
||||
default: 100
|
||||
min: 0
|
||||
|
||||
# Include forked repositories into metrics
|
||||
repositories_forks:
|
||||
description: Include forks in metrics
|
||||
type: boolean
|
||||
default: no
|
||||
31
source/plugins/base/queries/organization.graphql
Normal file
31
source/plugins/base/queries/organization.graphql
Normal file
@@ -0,0 +1,31 @@
|
||||
query BaseOrganization {
|
||||
organization(login: "$login") {
|
||||
databaseId
|
||||
name
|
||||
login
|
||||
createdAt
|
||||
avatarUrl
|
||||
websiteUrl
|
||||
isVerified
|
||||
twitterUsername
|
||||
repositories(last: 0) {
|
||||
totalCount
|
||||
totalDiskUsage
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
packages {
|
||||
totalCount
|
||||
}
|
||||
sponsorshipsAsSponsor {
|
||||
totalCount
|
||||
}
|
||||
sponsorshipsAsMaintainer {
|
||||
totalCount
|
||||
}
|
||||
membersWithRole {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
50
source/plugins/base/queries/repositories.graphql
Normal file
50
source/plugins/base/queries/repositories.graphql
Normal file
@@ -0,0 +1,50 @@
|
||||
query BaseRepositories {
|
||||
$account(login: "$login") {
|
||||
repositories($after first: $repositories $forks, orderBy: {field: UPDATED_AT, direction: DESC}) {
|
||||
edges {
|
||||
cursor
|
||||
}
|
||||
nodes {
|
||||
name
|
||||
owner {
|
||||
login
|
||||
}
|
||||
isFork
|
||||
watchers {
|
||||
totalCount
|
||||
}
|
||||
stargazers {
|
||||
totalCount
|
||||
}
|
||||
languages(first: 8) {
|
||||
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
|
||||
}
|
||||
releases {
|
||||
totalCount
|
||||
}
|
||||
forkCount
|
||||
licenseInfo {
|
||||
spdxId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
48
source/plugins/base/queries/repository.graphql
Normal file
48
source/plugins/base/queries/repository.graphql
Normal file
@@ -0,0 +1,48 @@
|
||||
query BaseRepository {
|
||||
$account(login: "$login") {
|
||||
repository(name: "$repo") {
|
||||
name
|
||||
owner {
|
||||
login
|
||||
}
|
||||
isFork
|
||||
createdAt
|
||||
diskUsage
|
||||
homepageUrl
|
||||
watchers {
|
||||
totalCount
|
||||
}
|
||||
stargazers {
|
||||
totalCount
|
||||
}
|
||||
languages(first: 8) {
|
||||
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
|
||||
}
|
||||
releases {
|
||||
totalCount
|
||||
}
|
||||
forkCount
|
||||
licenseInfo {
|
||||
spdxId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
66
source/plugins/base/queries/user.graphql
Normal file
66
source/plugins/base/queries/user.graphql
Normal file
@@ -0,0 +1,66 @@
|
||||
query BaseUser {
|
||||
user(login: "$login") {
|
||||
databaseId
|
||||
name
|
||||
login
|
||||
createdAt
|
||||
avatarUrl
|
||||
websiteUrl
|
||||
isHireable
|
||||
twitterUsername
|
||||
repositories(last: 0 $forks) {
|
||||
totalCount
|
||||
totalDiskUsage
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
issueComments {
|
||||
totalCount
|
||||
}
|
||||
organizations {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
87
source/plugins/core/README.md
Normal file
87
source/plugins/core/README.md
Normal 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
|
||||
```
|
||||
130
source/plugins/core/index.mjs
Normal file
130
source/plugins/core/index.mjs
Normal 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
|
||||
}
|
||||
164
source/plugins/core/metadata.yml
Normal file
164
source/plugins/core/metadata.yml
Normal 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: ""
|
||||
22
source/plugins/followup/README.md
Normal file
22
source/plugins/followup/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
### 🎟️ Follow-up of issues and pull requests
|
||||
|
||||
The *followup* plugin displays the ratio of open/closed issues and the ratio of open/merged pull requests across all your repositories, which shows if they're well-maintained or not.
|
||||
|
||||
<table>
|
||||
<td align="center">
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.followup.svg">
|
||||
<img width="900" height="1" alt="">
|
||||
</td>
|
||||
</table>
|
||||
|
||||
#### ℹ️ Examples workflows
|
||||
|
||||
[➡️ Available options for this plugin](metadata.yml)
|
||||
|
||||
```yaml
|
||||
- uses: lowlighter/metrics@latest
|
||||
with:
|
||||
# ... other options
|
||||
plugin_followup: yes
|
||||
```
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
//Setup
|
||||
export default async function ({computed, q}, {enabled = false} = {}) {
|
||||
export default async function ({data, computed, imports, q, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.followup))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
imports.metadata.plugins.followup.inputs({data, account, q})
|
||||
|
||||
//Define getters
|
||||
const followup = {
|
||||
issues:{
|
||||
@@ -18,6 +22,7 @@
|
||||
get merged() { return computed.repositories.pr_merged }
|
||||
}
|
||||
}
|
||||
|
||||
//Results
|
||||
return followup
|
||||
}
|
||||
|
||||
13
source/plugins/followup/metadata.yml
Normal file
13
source/plugins/followup/metadata.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
name: "🎟️ Follow-up of issues and pull requests"
|
||||
cost: 0 API request
|
||||
supports:
|
||||
- user
|
||||
- organization
|
||||
- repository
|
||||
inputs:
|
||||
|
||||
# Enable or disable plugin
|
||||
plugin_followup:
|
||||
description: Display follow-up of repositories issues and pull requests
|
||||
type: boolean
|
||||
default: no
|
||||
21
source/plugins/gists/README.md
Normal file
21
source/plugins/gists/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
### 🎫 Gists
|
||||
|
||||
The *gists* plugin displays your [gists](https://gist.github.com) metrics.
|
||||
|
||||
<table>
|
||||
<td align="center">
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.gists.svg">
|
||||
<img width="900" height="1" alt="">
|
||||
</td>
|
||||
</table>
|
||||
|
||||
#### ℹ️ Examples workflows
|
||||
|
||||
[➡️ Available options for this plugin](metadata.yml)
|
||||
|
||||
```yaml
|
||||
- uses: lowlighter/metrics@latest
|
||||
with:
|
||||
# ... other options
|
||||
plugin_gists: yes
|
||||
```
|
||||
@@ -1,12 +1,14 @@
|
||||
//Setup
|
||||
export default async function ({login, graphql, q, queries, account}, {enabled = false} = {}) {
|
||||
export default async function ({login, data, graphql, q, imports, queries, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.gists))
|
||||
return null
|
||||
if (account === "organization")
|
||||
throw {error:{message:"Not available for organizations"}}
|
||||
|
||||
//Load inputs
|
||||
imports.metadata.plugins.gists.inputs({data, account, q})
|
||||
|
||||
//Query gists from GitHub API
|
||||
const gists = []
|
||||
{
|
||||
@@ -23,6 +25,7 @@
|
||||
} while ((pushed)&&(cursor))
|
||||
console.debug(`metrics/compute/${login}/plugins > gists > loaded ${gists.length} gists`)
|
||||
}
|
||||
|
||||
//Iterate through gists
|
||||
console.debug(`metrics/compute/${login}/plugins > gists > processing ${gists.length} gists`)
|
||||
let stargazers = 0, forks = 0, comments = 0, files = 0
|
||||
@@ -36,6 +39,7 @@
|
||||
comments += gist.comments.totalCount
|
||||
files += gist.files.length
|
||||
}
|
||||
|
||||
//Results
|
||||
return {totalCount:gists.totalCount, stargazers, forks, files, comments}
|
||||
}
|
||||
|
||||
11
source/plugins/gists/metadata.yml
Normal file
11
source/plugins/gists/metadata.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
name: "🎫 Gists"
|
||||
cost: 1 GraphQL request per 100 gists
|
||||
supports:
|
||||
- user
|
||||
inputs:
|
||||
|
||||
# Enable or disable plugin
|
||||
plugin_gists:
|
||||
description: Display gists metrics
|
||||
type: boolean
|
||||
default: no
|
||||
23
source/plugins/gists/queries/gists.graphql
Normal file
23
source/plugins/gists/queries/gists.graphql
Normal file
@@ -0,0 +1,23 @@
|
||||
query GistsDefault {
|
||||
user(login: "$login") {
|
||||
gists($after first: 100, orderBy: {field: UPDATED_AT, direction: DESC}) {
|
||||
edges {
|
||||
cursor
|
||||
}
|
||||
totalCount
|
||||
nodes {
|
||||
stargazerCount
|
||||
isFork
|
||||
forks {
|
||||
totalCount
|
||||
}
|
||||
files {
|
||||
name
|
||||
}
|
||||
comments {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
source/plugins/habits/README.md
Normal file
38
source/plugins/habits/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
### 💡 Coding habits
|
||||
|
||||
The coding *habits* plugin display metrics based on your recent activity, such as active hours or languages recently used.
|
||||
|
||||
<table>
|
||||
<td align="center">
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.habits.facts.svg">
|
||||
<details open><summary>Charts version</summary>
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.habits.charts.svg">
|
||||
</details>
|
||||
<img width="900" height="1" alt="">
|
||||
</td>
|
||||
</table>
|
||||
|
||||
Using more events will improve accuracy of these metrics, although it'll increase the number of GitHub requests used.
|
||||
|
||||
Active hours and days are computed through your commit history, while indent style is deduced from your recent diffs.
|
||||
Recent languages activity is also computed from your recent diffs, using [github/linguist](https://github.com/github/linguist).
|
||||
|
||||
Use a full `repo` scope token to access **private** events.
|
||||
|
||||
By default, dates use Greenwich meridian (GMT/UTC). Be sure to set your timezone (see [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for a list of supported timezones) for accurate metrics.
|
||||
|
||||
#### ℹ️ Examples workflows
|
||||
|
||||
[➡️ Available options for this plugin](metadata.yml)
|
||||
|
||||
```yaml
|
||||
- uses: lowlighter/metrics@latest
|
||||
with:
|
||||
# ... other options
|
||||
plugin_habits: yes
|
||||
plugin_habits_from: 200 # Use 200 events to compute habits
|
||||
plugin_habits_days: 14 # Keep only events from last 14 days
|
||||
plugin_habits_facts: yes # Display facts section
|
||||
plugin_habits_charts: yes # Display charts section
|
||||
config_timezone: Europe/Paris # Set timezone
|
||||
```
|
||||
@@ -1,20 +1,19 @@
|
||||
//Setup
|
||||
export default async function ({login, rest, imports, data, q, account}, {enabled = false, from:defaults = 100} = {}) {
|
||||
export default async function ({login, data, rest, imports, q, account}, {enabled = false, ...defaults} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.habits))
|
||||
return null
|
||||
//Parameters override
|
||||
let {"habits.from":from = defaults.from ?? 500, "habits.days":days = 14, "habits.facts":facts = true, "habits.charts":charts = false} = q
|
||||
//Events
|
||||
from = Math.max(1, Math.min(1000, Number(from)))
|
||||
//Days
|
||||
days = Math.max(1, Math.min(30, Number(days)))
|
||||
|
||||
//Load inputs
|
||||
let {from, days, facts, charts} = imports.metadata.plugins.habits.inputs({data, account, q}, defaults)
|
||||
|
||||
//Initialization
|
||||
const habits = {facts, charts, commits:{hour:NaN, hours:{}, day:NaN, days:{}}, indents:{style:"", spaces:0, tabs:0}, linguist:{available:false, ordered:[], languages:{}}}
|
||||
const pages = Math.ceil(from/100)
|
||||
const offset = data.config.timezone?.offset ?? 0
|
||||
|
||||
//Get user recent activity
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > querying api`)
|
||||
const events = []
|
||||
@@ -25,12 +24,14 @@
|
||||
}
|
||||
} catch { console.debug(`metrics/compute/${login}/plugins > habits > no more page to load`) }
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > ${events.length} events loaded`)
|
||||
|
||||
//Get user recent commits
|
||||
const commits = events
|
||||
.filter(({type}) => type === "PushEvent")
|
||||
.filter(({actor}) => account === "organization" ? true : actor.login === login)
|
||||
.filter(({created_at}) => new Date(created_at) > new Date(Date.now()-days*24*60*60*1000))
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > filtered out ${commits.length} push events over last ${days} days`)
|
||||
|
||||
//Retrieve edited files and filter edited lines (those starting with +/-) from patches
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > loading patches`)
|
||||
const patches = [...await Promise.allSettled(commits
|
||||
@@ -41,6 +42,7 @@
|
||||
.map(({value}) => value)
|
||||
.flatMap(files => files.map(file => ({name:imports.paths.basename(file.filename), patch:file.patch ?? ""})))
|
||||
.map(({name, patch}) => ({name, patch:patch.split("\n").filter(line => /^[-+]/.test(line)).map(line => line.substring(1)).join("\n")}))
|
||||
|
||||
//Commit day
|
||||
{
|
||||
//Compute commit days
|
||||
@@ -52,6 +54,7 @@
|
||||
//Compute day with most commits
|
||||
habits.commits.day = days.length ? ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][Object.entries(habits.commits.days).sort(([an, a], [bn, b]) => b - a).map(([day, occurence]) => day)[0]] ?? NaN : NaN
|
||||
}
|
||||
|
||||
//Commit hour
|
||||
{
|
||||
//Compute commit hours
|
||||
@@ -63,6 +66,7 @@
|
||||
//Compute hour with most commits
|
||||
habits.commits.hour = hours.length ? `${Object.entries(habits.commits.hours).sort(([an, a], [bn, b]) => b - a).map(([hour, occurence]) => hour)[0]}`.padStart(2, "0") : NaN
|
||||
}
|
||||
|
||||
//Indent style
|
||||
{
|
||||
//Attempt to guess whether tabs or spaces are used in patches
|
||||
@@ -72,6 +76,7 @@
|
||||
.forEach(indent => habits.indents[/^\t/.test(indent) ? "tabs" : "spaces"]++)
|
||||
habits.indents.style = habits.indents.spaces > habits.indents.tabs ? "spaces" : habits.indents.tabs > habits.indents.spaces ? "tabs" : ""
|
||||
}
|
||||
|
||||
//Linguist
|
||||
if (charts) {
|
||||
//Check if linguist exists
|
||||
@@ -87,7 +92,7 @@
|
||||
await Promise.all(patches.map(async ({name, patch}, i) => await imports.fs.writeFile(imports.paths.join(path, `${i}${imports.paths.extname(name)}`), patch)))
|
||||
//Create temporary git repository
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > creating temp git repository`)
|
||||
await imports.run(`git init && git add . && git config user.name "linguist" && git config user.email "null@github.com" && git commit -m "linguist"`, {cwd:path}).catch(console.debug)
|
||||
await imports.run(`git init && git add . && git config user.name "linguist" && git config user.email "<>" && git commit -m "linguist"`, {cwd:path}).catch(console.debug)
|
||||
await imports.run(`git status`, {cwd:path})
|
||||
//Spawn linguist process
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > running linguist`)
|
||||
@@ -100,6 +105,7 @@
|
||||
else
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > linguist not available`)
|
||||
}
|
||||
|
||||
//Results
|
||||
return habits
|
||||
}
|
||||
|
||||
43
source/plugins/habits/metadata.yml
Normal file
43
source/plugins/habits/metadata.yml
Normal file
@@ -0,0 +1,43 @@
|
||||
name: "💡 Coding habits"
|
||||
cost: 1 REST request per 100 events + 1 REST request pet commit
|
||||
supports:
|
||||
- user
|
||||
- organization
|
||||
inputs:
|
||||
|
||||
# Enable or disable plugin
|
||||
plugin_habits:
|
||||
description: Display coding habits metrics
|
||||
type: boolean
|
||||
default: no
|
||||
|
||||
# Number of events to use to computes habits
|
||||
# Using more will result in more accurate metrics, but you may hit GitHub rate-limit more easily
|
||||
plugin_habits_from:
|
||||
description: Number of events to use
|
||||
type: number
|
||||
default: 200
|
||||
min: 1
|
||||
max: 1000
|
||||
|
||||
# Filter used events to compute habits by age
|
||||
plugin_habits_days:
|
||||
description: Maximum event age
|
||||
type: number
|
||||
default: 14
|
||||
min: 1
|
||||
max: 30
|
||||
|
||||
# Display tidbits about your most active hours/days, indents used (spaces/tabs), etc.
|
||||
# This is deduced from your recent activity
|
||||
plugin_habits_facts:
|
||||
description: Display coding habits collected facts based on recent activity
|
||||
type: boolean
|
||||
default: yes
|
||||
|
||||
# Display charts of most active time of the day and most active day of the week
|
||||
# Also display languages recently used (this is not the same as plugin_languages, as the latter is an all-time stats)
|
||||
plugin_habits_charts:
|
||||
description: Display coding habits charts based on recent activity
|
||||
type: boolean
|
||||
default: no
|
||||
25
source/plugins/isocalendar/README.md
Normal file
25
source/plugins/isocalendar/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
### 📅 Isometric commit calendar
|
||||
|
||||
The *isocalendar* plugin displays an isometric view of your commits calendar, along with a few additional stats like current streak and commit average per day.
|
||||
|
||||
<table>
|
||||
<td align="center">
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.isocalendar.svg">
|
||||
<details><summary>Full year version</summary>
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.isocalendar.fullyear.svg">
|
||||
</details>
|
||||
<img width="900" height="1" alt="">
|
||||
</td>
|
||||
</table>
|
||||
|
||||
#### ℹ️ Examples workflows
|
||||
|
||||
[➡️ Available options for this plugin](metadata.yml)
|
||||
|
||||
```yaml
|
||||
- uses: lowlighter/metrics@latest
|
||||
with:
|
||||
# ... other options
|
||||
plugin_isocalendar: yes
|
||||
plugin_isocalendar_duration: full-year # Display full year instead of half year
|
||||
```
|
||||
@@ -1,16 +1,14 @@
|
||||
//Setup
|
||||
export default async function ({login, graphql, q, queries, account}, {enabled = false} = {}) {
|
||||
export default async function ({login, data, graphql, q, imports, queries, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.isocalendar))
|
||||
return null
|
||||
if (account === "organization")
|
||||
throw {error:{message:"Not available for organizations"}}
|
||||
//Parameters override
|
||||
let {"isocalendar.duration":duration = "half-year"} = q
|
||||
//Duration in days
|
||||
duration = ["full-year", "half-year"].includes(duration) ? duration : "full-year"
|
||||
|
||||
//Load inputs
|
||||
let {duration} = imports.metadata.plugins.isocalendar.inputs({data, account, q})
|
||||
|
||||
//Compute start day
|
||||
const now = new Date()
|
||||
const start = new Date(now)
|
||||
@@ -18,23 +16,27 @@
|
||||
start.setFullYear(now.getFullYear()-1)
|
||||
else
|
||||
start.setHours(-24*180)
|
||||
|
||||
//Compute padding to ensure last row is complete
|
||||
const padding = new Date(start)
|
||||
padding.setHours(-14*24)
|
||||
|
||||
//Retrieve contribution calendar from graphql api
|
||||
console.debug(`metrics/compute/${login}/plugins > isocalendar > querying api`)
|
||||
const calendar = {}
|
||||
for (const [name, from, to] of [["padding", padding, start], ["weeks", start, now]]) {
|
||||
console.debug(`metrics/compute/${login}/plugins > isocalendar > loading ${name} from "${from.toISOString()}" to "${to.toISOString()}"`)
|
||||
const {user:{calendar:{contributionCalendar:{weeks}}}} = await graphql(queries.calendar({login, from:from.toISOString(), to:to.toISOString()}))
|
||||
const {user:{calendar:{contributionCalendar:{weeks}}}} = await graphql(queries.isocalendar.calendar({login, from:from.toISOString(), to:to.toISOString()}))
|
||||
calendar[name] = weeks
|
||||
}
|
||||
|
||||
//Apply padding
|
||||
console.debug(`metrics/compute/${login}/plugins > isocalendar > applying padding`)
|
||||
const firstweek = calendar.weeks[0].contributionDays
|
||||
const padded = calendar.padding.flatMap(({contributionDays}) => contributionDays).filter(({date}) => !firstweek.map(({date}) => date).includes(date))
|
||||
while (firstweek.length < 7)
|
||||
firstweek.unshift(padded.pop())
|
||||
|
||||
//Compute the highest contributions in a day, streaks and average commits per day
|
||||
console.debug(`metrics/compute/${login}/plugins > isocalendar > computing stats`)
|
||||
let max = 0, streak = {max:0, current:0}, values = [], average = 0
|
||||
@@ -47,6 +49,7 @@
|
||||
}
|
||||
}
|
||||
average = (values.reduce((a, b) => a + b, 0)/values.length).toFixed(2).replace(/[.]0+$/, "")
|
||||
|
||||
//Compute SVG
|
||||
console.debug(`metrics/compute/${login}/plugins > isocalendar > computing svg render`)
|
||||
const size = 6
|
||||
@@ -80,6 +83,7 @@
|
||||
i++
|
||||
}
|
||||
svg += `</g></svg>`
|
||||
|
||||
//Results
|
||||
return {streak, max, average, svg, duration}
|
||||
}
|
||||
|
||||
20
source/plugins/isocalendar/metadata.yml
Normal file
20
source/plugins/isocalendar/metadata.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
name: "📅 Isometric commit calendar"
|
||||
cost: 2-3 REST requests
|
||||
supports:
|
||||
- user
|
||||
inputs:
|
||||
|
||||
# Enable or disable plugin
|
||||
plugin_isocalendar:
|
||||
description: Display an isometric view of your commits calendar
|
||||
type: boolean
|
||||
default: no
|
||||
|
||||
# Set time window shown by isometric calendar
|
||||
plugin_isocalendar_duration:
|
||||
description: Set time window shown by isometric calendar
|
||||
type: string
|
||||
default: half-year
|
||||
values:
|
||||
- half-year
|
||||
- full-year
|
||||
15
source/plugins/isocalendar/queries/calendar.graphql
Normal file
15
source/plugins/isocalendar/queries/calendar.graphql
Normal file
@@ -0,0 +1,15 @@
|
||||
query IsocalendarCalendar {
|
||||
user(login: "$login") {
|
||||
calendar:contributionsCollection(from: "$from", to: "$to") {
|
||||
contributionCalendar {
|
||||
weeks {
|
||||
contributionDays {
|
||||
contributionCount
|
||||
color
|
||||
date
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
source/plugins/languages/README.md
Normal file
29
source/plugins/languages/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
### 🈷️ Most used languages <sup>🚧 <code>plugin_languages_colors</code> on <code>@master</code></sup>
|
||||
|
||||
The *languages* plugin displays which programming languages you use the most across all your repositories.
|
||||
|
||||
<table>
|
||||
<td align="center">
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.languages.svg">
|
||||
<img width="900" height="1" alt="">
|
||||
</td>
|
||||
</table>
|
||||
|
||||
It is possible to use custom colors for languages instead of those provided by GitHub by using `plugin_languages_colors` option.
|
||||
You can specify either an index with a color, or a language name (case insensitive) with a color.
|
||||
Colors can be either in hexadecimal format or a [named color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value).
|
||||
It is also possible to use a predefined set of colors from [colorsets.json](colorsets.json)
|
||||
|
||||
#### ℹ️ Examples workflows
|
||||
|
||||
[➡️ Available options for this plugin](metadata.yml)
|
||||
|
||||
```yaml
|
||||
- uses: lowlighter/metrics@latest
|
||||
with:
|
||||
# ... other options
|
||||
plugin_languages: yes
|
||||
plugin_languages_ignored: html, css # List of languages to ignore
|
||||
plugin_languages_skipped: my-test-repo # List of repositories to skip
|
||||
plugin_languages_colors: "0:orange, javascript:#ff0000, ..." # Make most used languages orange and JavaScript red
|
||||
```
|
||||
@@ -1,22 +1,21 @@
|
||||
//Setup
|
||||
export default async function ({login, data, imports, q}, {enabled = false} = {}) {
|
||||
export default async function ({login, data, imports, q, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.languages))
|
||||
return null
|
||||
//Parameters override
|
||||
let {"languages.ignored":ignored = "", "languages.skipped":skipped = "", "languages.colors":colors = ""} = q
|
||||
//Ignored languages
|
||||
ignored = decodeURIComponent(ignored).split(",").map(x => x.trim().toLocaleLowerCase()).filter(x => x)
|
||||
//Skipped repositories
|
||||
skipped = decodeURIComponent(skipped).split(",").map(x => x.trim().toLocaleLowerCase()).filter(x => x)
|
||||
//Custom colors
|
||||
const colorsets = JSON.parse(`${await imports.fs.readFile(`${imports.__module(import.meta.url)}/colorsets.json`)}`)
|
||||
if (`${colors}` in colorsets)
|
||||
colors = colorsets[`${colors}`]
|
||||
colors = Object.fromEntries(decodeURIComponent(colors).split(",").map(x => x.trim().toLocaleLowerCase()).filter(x => x).map(x => x.split(":").map(x => x.trim())))
|
||||
console.debug(`metrics/compute/${login}/plugins > languages > custom colors ${JSON.stringify(colors)}`)
|
||||
|
||||
//Load inputs
|
||||
let {ignored, skipped, colors} = imports.metadata.plugins.languages.inputs({data, account, q})
|
||||
|
||||
//Custom colors
|
||||
const colorsets = JSON.parse(`${await imports.fs.readFile(`${imports.__module(import.meta.url)}/colorsets.json`)}`)
|
||||
if (`${colors}` in colorsets)
|
||||
colors = colorsets[`${colors}`]
|
||||
colors = Object.fromEntries(decodeURIComponent(colors).split(",").map(x => x.trim().toLocaleLowerCase()).filter(x => x).map(x => x.split(":").map(x => x.trim())))
|
||||
console.debug(`metrics/compute/${login}/plugins > languages > custom colors ${JSON.stringify(colors)}`)
|
||||
|
||||
//Iterate through user's repositories and retrieve languages data
|
||||
console.debug(`metrics/compute/${login}/plugins > languages > processing ${data.user.repositories.nodes.length} repositories`)
|
||||
const languages = {colors:{}, total:0, stats:{}}
|
||||
@@ -39,6 +38,7 @@
|
||||
languages.total += size
|
||||
}
|
||||
}
|
||||
|
||||
//Compute languages stats
|
||||
console.debug(`metrics/compute/${login}/plugins > languages > computing stats`)
|
||||
Object.keys(languages.stats).map(name => languages.stats[name] /= languages.total)
|
||||
@@ -48,6 +48,7 @@
|
||||
if ((colors[i])&&(!colors[languages.favorites[i].name.toLocaleLowerCase()]))
|
||||
languages.favorites[i].color = colors[i]
|
||||
}
|
||||
|
||||
//Results
|
||||
return languages
|
||||
}
|
||||
|
||||
40
source/plugins/languages/metadata.yml
Normal file
40
source/plugins/languages/metadata.yml
Normal file
@@ -0,0 +1,40 @@
|
||||
name: "🈷️ Most used languages"
|
||||
cost: 0 API request
|
||||
supports:
|
||||
- user
|
||||
- organization
|
||||
- repository
|
||||
inputs:
|
||||
|
||||
# Enable or disable plugin
|
||||
plugin_languages:
|
||||
description: Display most used languages metrics
|
||||
type: boolean
|
||||
default: no
|
||||
|
||||
# List of languages that will be ignored
|
||||
plugin_languages_ignored:
|
||||
description: Languages to ignore
|
||||
type: array
|
||||
format: comma-separated
|
||||
default: ""
|
||||
|
||||
# List of repositories that will be skipped
|
||||
plugin_languages_skipped:
|
||||
description: Repositories to skip
|
||||
type: array
|
||||
format: comma-separated
|
||||
default: ""
|
||||
|
||||
# Overrides
|
||||
# Use `${n}:${color}` to change the color of the n-th most used language (e.g. "0:red" to make your most used language red)
|
||||
# Use `${language}:${color}` to change the color of named language (e.g. "javascript:red" to make JavaScript language red, language case is ignored)
|
||||
# Use a value from `colorsets.json` to use a predefined set of colors
|
||||
# Both hexadecimal and named colors are supported
|
||||
plugin_languages_colors:
|
||||
description: Custom languages colors
|
||||
type: array
|
||||
format:
|
||||
- comma-separated
|
||||
- /((?<index>[0-9])|(?<language>[-+a-z0-9#])):(?<color>#?[-a-z0-9]+)/
|
||||
default: github
|
||||
21
source/plugins/lines/README.md
Normal file
21
source/plugins/lines/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
### 👨💻 Lines of code changed
|
||||
|
||||
The *lines* of code plugin displays the number of lines of code you have added and removed across all of your repositories.
|
||||
|
||||
<table>
|
||||
<td align="center">
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.lines.svg">
|
||||
<img width="900" height="1" alt="">
|
||||
</td>
|
||||
</table>
|
||||
|
||||
#### ℹ️ Examples workflows
|
||||
|
||||
[➡️ Available options for this plugin](metadata.yml)
|
||||
|
||||
```yaml
|
||||
- uses: lowlighter/metrics@latest
|
||||
with:
|
||||
# ... other options
|
||||
plugin_lines: yes
|
||||
```
|
||||
@@ -1,11 +1,14 @@
|
||||
//Setup
|
||||
export default async function ({login, data, rest, q}, {enabled = false} = {}) {
|
||||
export default async function ({login, data, imports, rest, q, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.lines))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
imports.metadata.plugins.lines.inputs({data, account, q})
|
||||
|
||||
//Context
|
||||
let context = {mode:"user"}
|
||||
if (q.repo) {
|
||||
@@ -15,10 +18,12 @@
|
||||
|
||||
//Repositories
|
||||
const repositories = data.user.repositories.nodes.map(({name:repo, owner:{login:owner}}) => ({repo, owner})) ?? []
|
||||
|
||||
//Get contributors stats from repositories
|
||||
console.debug(`metrics/compute/${login}/plugins > lines > querying api`)
|
||||
const lines = {added:0, deleted:0}
|
||||
const response = await Promise.all(repositories.map(async ({repo, owner}) => await rest.repos.getContributorsStats({owner, repo})))
|
||||
|
||||
//Compute changed lines
|
||||
console.debug(`metrics/compute/${login}/plugins > lines > computing total diff`)
|
||||
response.map(({data:repository}) => {
|
||||
@@ -31,6 +36,7 @@
|
||||
if (contributor)
|
||||
contributor.weeks.forEach(({a, d}) => (lines.added += a, lines.deleted += d))
|
||||
})
|
||||
|
||||
//Results
|
||||
return lines
|
||||
}
|
||||
|
||||
13
source/plugins/lines/metadata.yml
Normal file
13
source/plugins/lines/metadata.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
name: "👨💻 Lines of code changed"
|
||||
cost: 1 REST request per repository
|
||||
supports:
|
||||
- user
|
||||
- organization
|
||||
- repository
|
||||
inputs:
|
||||
|
||||
# Enable or disable plugin
|
||||
plugin_lines:
|
||||
description: Display lines of code metrics
|
||||
type: boolean
|
||||
default: no
|
||||
197
source/plugins/music/README.md
Normal file
197
source/plugins/music/README.md
Normal file
@@ -0,0 +1,197 @@
|
||||
### 🎼 Music plugin <sup>🚧 <code>lastfm</code> on <code>@master</code></sup>
|
||||
|
||||
The *music* plugin lets you display :
|
||||
|
||||
<table>
|
||||
<td align="center">
|
||||
<details open><summary>🎼 Favorite tracks version</summary>
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.music.playlist.svg">
|
||||
</details>
|
||||
<details open><summary>Recently listened version</summary>
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.music.recent.svg">
|
||||
</details>
|
||||
<img width="900" height="1" alt="">
|
||||
</td>
|
||||
</table>
|
||||
|
||||
It can work in the following modes:
|
||||
|
||||
### Playlist mode
|
||||
|
||||
Select randomly a few tracks from a given playlist to share your favorites tracks with your visitors.
|
||||
|
||||
Select a music provider below for instructions.
|
||||
|
||||
<details>
|
||||
<summary>Apple Music</summary>
|
||||
|
||||
Extract the *embed* URL of the playlist you want to share.
|
||||
|
||||
To do so, connect to [music.apple.com](https://music.apple.com/) and select the playlist you want to share.
|
||||
From `...` menu, select `Share` and `Copy embed code`.
|
||||
|
||||

|
||||
|
||||
Extract the source link from the code pasted in your clipboard:
|
||||
```html
|
||||
<iframe allow="" frameborder="" height="" style="" sandbox="" src="https://embed.music.apple.com/**/playlist/********"></iframe>
|
||||
```
|
||||
|
||||
And use this value in `plugin_music_playlist` option.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Spotify</summary>
|
||||
|
||||
Extract the *embed* URL of the playlist you want to share.
|
||||
|
||||
To do so, Open Spotify and select the playlist you want to share.
|
||||
From `...` menu, select `Share` and `Copy embed code`.
|
||||
|
||||

|
||||
|
||||
Extract the source link from the code pasted in your clipboard:
|
||||
```html
|
||||
<iframe src="https://open.spotify.com/embed/playlist/********" width="" height="" frameborder="0" allowtransparency="" allow=""></iframe>
|
||||
```
|
||||
|
||||
And use this value in `plugin_music_playlist` option.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Last.fm</summary>
|
||||
|
||||
This mode is not supported for now.
|
||||
|
||||
</details>
|
||||
|
||||
#### ℹ️ Examples workflows
|
||||
|
||||
[➡️ Available options for this plugin](metadata.yml)
|
||||
|
||||
```yaml
|
||||
- uses: lowlighter/metrics@latest
|
||||
with:
|
||||
# ... other options
|
||||
plugin_music: yes
|
||||
plugin_music_limit: 4 # Limit to 4 entries
|
||||
plugin_music_playlist: https://******** # Use extracted playlist link
|
||||
# (plugin_music_provider and plugin_music_mode will be set automatically)
|
||||
```
|
||||
|
||||
### Recently played mode
|
||||
|
||||
Display tracks you have played recently.
|
||||
|
||||
Select a music provider below for additional instructions.
|
||||
|
||||
<details>
|
||||
<summary>Apple Music</summary>
|
||||
|
||||
This mode is not supported for now.
|
||||
|
||||
I tried to find a way with *smart playlists*, *shortcuts* and other stuff but could not figure a workaround to do it without paying the $99 fee for the developer program.
|
||||
|
||||
So unfortunately this isn't available for now.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Spotify</summary>
|
||||
|
||||
Spotify does not have *personal tokens*, so it makes the process a bit longer because you're required to follow the [authorization workflow](https://developer.spotify.com/documentation/general/guides/authorization-guide/)... Follow the instructions below for a *TL;DR* to obtain a `refresh_token`.
|
||||
|
||||
Sign in to the [developer dashboard](https://developer.spotify.com/dashboard/) and create a new app.
|
||||
Keep your `client_id` and `client_secret` and let this tab open for now.
|
||||
|
||||

|
||||
|
||||
Open the settings and add a new *Redirect url*. Normally it is used to setup callbacks for apps, but just put `https://localhost` instead (it is mandatory as per the [authorization guide](https://developer.spotify.com/documentation/general/guides/authorization-guide/), even if not used).
|
||||
|
||||
Forge the authorization url with your `client_id` and the encoded `redirect_uri` you whitelisted, and access it from your browser:
|
||||
|
||||
```
|
||||
https://accounts.spotify.com/authorize?client_id=********&response_type=code&scope=user-read-recently-played&redirect_uri=https%3A%2F%2Flocalhost
|
||||
```
|
||||
|
||||
When prompted, authorize your application.
|
||||
|
||||

|
||||
|
||||
Once redirected to `redirect_uri`, extract the generated authorization `code` from your url bar.
|
||||
|
||||

|
||||
|
||||
Go back to your developer dashboard tab, and open the web console of your browser to paste the following JavaScript code, with your own `client_id`, `client_secret`, authorization `code` and `redirect_uri`.
|
||||
|
||||
```js
|
||||
(async () => {
|
||||
console.log(await (await fetch("https://accounts.spotify.com/api/token", {
|
||||
method:"POST",
|
||||
headers:{"Content-Type":"application/x-www-form-urlencoded"},
|
||||
body:new URLSearchParams({
|
||||
grant_type:"authorization_code",
|
||||
redirect_uri:"https://localhost",
|
||||
client_id:"********",
|
||||
client_secret:"********",
|
||||
code:"********",
|
||||
})
|
||||
})).json())
|
||||
})()
|
||||
```
|
||||
|
||||
It should return a JSON response with the following content:
|
||||
```json
|
||||
{
|
||||
"access_token":"********",
|
||||
"expires_in": 3600,
|
||||
"scope":"user-read-recently-played",
|
||||
"token_type":"Bearer",
|
||||
"refresh_token":"********"
|
||||
}
|
||||
```
|
||||
|
||||
Register your `client_id`, `client_secret` and `refresh_token` in secrets to finish setup.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Last.fm</summary>
|
||||
|
||||
Obtain a Last.fm API key.
|
||||
|
||||
To do so, you can simply [create an API account](https://www.last.fm/api/account/create) or [use an existing one](https://www.last.fm/api/accounts).
|
||||
|
||||
Register your API key to finish setup.
|
||||
|
||||
</details>
|
||||
|
||||
#### ℹ️ Examples workflows
|
||||
|
||||
[➡️ Available options for this plugin](metadata.yml)
|
||||
|
||||
```yaml
|
||||
- uses: lowlighter/metrics@latest
|
||||
with:
|
||||
# ... other options
|
||||
plugin_music: yes
|
||||
plugin_music_provider: spotify # Use Spotify as provider
|
||||
plugin_music_mode: recent # Set plugin mode
|
||||
plugin_music_limit: 4 # Limit to 4 entries
|
||||
plugin_music_token: "${{ secrets.SPOTIFY_CLIENT_ID }}, ${{ secrets.SPOTIFY_CLIENT_SECRET }}, ${{ secrets.SPOTIFY_REFRESH_TOKEN }}"
|
||||
```
|
||||
|
||||
```yaml
|
||||
- uses: lowlighter/metrics@latest
|
||||
with:
|
||||
# ... other options
|
||||
plugin_music: yes
|
||||
plugin_music_provider: lastfm # Use Last.fm as provider
|
||||
plugin_music_mode: recent # Set plugin mode
|
||||
plugin_music_limit: 4 # Limit to 4 entries
|
||||
plugin_music_user: .user.login # Use same username as GitHub login
|
||||
plugin_music_token: ${{ secrets.LASTFM_API_KEY }}
|
||||
|
||||
```
|
||||
@@ -21,20 +21,22 @@
|
||||
}
|
||||
|
||||
//Setup
|
||||
export default async function ({login, imports, q}, {enabled = false, token = ""} = {}) {
|
||||
export default async function ({login, imports, data, q, account}, {enabled = false, token = ""} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.music))
|
||||
return null
|
||||
|
||||
//Initialization
|
||||
const raw = {
|
||||
get provider() { return providers[provider]?.name ?? "" },
|
||||
get mode() { return modes[mode] ?? "Unconfigured music plugin"},
|
||||
}
|
||||
let tracks = null
|
||||
//Parameters override
|
||||
let {"music.provider":provider = "", "music.mode":mode = "", "music.playlist":playlist = null, "music.limit":limit = 4, "music.user":user = login} = q
|
||||
|
||||
//Load inputs
|
||||
let {provider, mode, playlist, limit, user} = imports.metadata.plugins.music.inputs({data, account, q})
|
||||
//Auto-guess parameters
|
||||
if ((playlist)&&(!mode))
|
||||
mode = "playlist"
|
||||
@@ -59,6 +61,7 @@
|
||||
}
|
||||
//Limit
|
||||
limit = Math.max(1, Math.min(100, Number(limit)))
|
||||
|
||||
//Handle mode
|
||||
console.debug(`metrics/compute/${login}/plugins > music > processing mode ${mode} with provider ${provider}`)
|
||||
switch (mode) {
|
||||
@@ -197,6 +200,7 @@
|
||||
default:
|
||||
throw {error:{message:`Unsupported mode "${mode}"`}, ...raw}
|
||||
}
|
||||
|
||||
//Format tracks
|
||||
if (Array.isArray(tracks)) {
|
||||
//Limit tracklist
|
||||
@@ -213,6 +217,7 @@
|
||||
//Save results
|
||||
return {...raw, tracks}
|
||||
}
|
||||
|
||||
//Unhandled error
|
||||
throw {error:{message:`An error occured (could not retrieve tracks)`}}
|
||||
}
|
||||
|
||||
63
source/plugins/music/metadata.yml
Normal file
63
source/plugins/music/metadata.yml
Normal file
@@ -0,0 +1,63 @@
|
||||
name: "🎼 Music plugin"
|
||||
cost: N/A
|
||||
supports:
|
||||
- user
|
||||
- organization
|
||||
inputs:
|
||||
|
||||
# Enable or disable plugin
|
||||
plugin_music:
|
||||
description: Display your music tracks
|
||||
type: boolean
|
||||
default: no
|
||||
|
||||
# Name of music provider
|
||||
# This is optional for "playlist" mode (it can be deduced automatically from "plugin_music_playlist" url)
|
||||
# This is required in other modes
|
||||
plugin_music_provider:
|
||||
description: Music provider
|
||||
type: string
|
||||
default: ""
|
||||
values:
|
||||
- apple # Apple Music
|
||||
- spotify # Spotify
|
||||
- lastfm # Last.fm
|
||||
|
||||
# Music provider token
|
||||
# This may be required depending on music provider used and plugin mode
|
||||
# - "apple" : not required
|
||||
# - "spotify" : required for "recent" mode, format is "client_id, client_secret, refresh_token"
|
||||
# - "lastfm" : required, format is "api_key"
|
||||
plugin_music_token:
|
||||
description: Music provider personal token
|
||||
type: token
|
||||
default: ""
|
||||
|
||||
# Plugin mode
|
||||
plugin_music_mode:
|
||||
description: Plugin mode
|
||||
type: string
|
||||
default: "" # Defaults to "recent" or to "playlist" if "plugin_music_playlist" is specified
|
||||
values:
|
||||
- playlist # Display tracks from an embed playlist randomly
|
||||
- recent # Display recently listened tracks
|
||||
|
||||
# Embed playlist url (i.e. url used by music player iframes)
|
||||
plugin_music_playlist:
|
||||
description: Embed playlist url
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
# Number of music tracks to display
|
||||
plugin_music_limit:
|
||||
description: Maximum number of tracks to display
|
||||
type: number
|
||||
default: 4
|
||||
min: 1
|
||||
max: 100
|
||||
|
||||
# Username on music provider service
|
||||
plugin_music_user:
|
||||
description: Music provider username
|
||||
type: string
|
||||
default: .user.login
|
||||
40
source/plugins/pagespeed/README.md
Normal file
40
source/plugins/pagespeed/README.md
Normal file
@@ -0,0 +1,40 @@
|
||||
|
||||
### ⏱️ Website performances
|
||||
|
||||
The *pagespeed* plugin adds the performance statistics of the website attached on your account:
|
||||
|
||||
<table>
|
||||
<td align="center">
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.pagespeed.svg">
|
||||
<details><summary>Detailed version</summary>
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.pagespeed.detailed.svg">
|
||||
</details>
|
||||
<details><summary>With screenshot version</summary>
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.pagespeed.screenshot.svg">
|
||||
</details>
|
||||
<img width="900" height="1" alt="">
|
||||
</td>
|
||||
</table>
|
||||
|
||||
These metrics are computed through [Google's PageSpeed API](https://developers.google.com/speed/docs/insights/v5/get-started), which yields the same results as [web.dev](https://web.dev).
|
||||
|
||||
See [performance scoring](https://web.dev/performance-scoring/) and [score calculator](https://googlechrome.github.io/lighthouse/scorecalc/) for more informations about how PageSpeed compute these statistics.
|
||||
|
||||
Although not mandatory, you can generate an API key for PageSpeed API [here](https://developers.google.com/speed/docs/insights/v5/get-started) to avoid hitting rate limiter.
|
||||
|
||||
Expect 10 to 30 seconds to generate the results.
|
||||
|
||||
#### ℹ️ Examples workflows
|
||||
|
||||
[➡️ Available options for this plugin](metadata.yml)
|
||||
|
||||
```yaml
|
||||
- uses: lowlighter/metrics@latest
|
||||
with:
|
||||
# ... other options
|
||||
plugin_pagespeed: yes
|
||||
plugin_pagespeed_token: ${{ secrets.PAGESPEED_TOKEN }} # Optional but recommended
|
||||
plugin_pagespeed_detailed: yes # Print detailed audit metrics
|
||||
plugin_pagespeed_screenshot: no # Display a screenshot of your website
|
||||
plugin_pagespeed_url: .user.website # Website to audit (defaults to your GitHub linked website)
|
||||
```
|
||||
@@ -1,18 +1,18 @@
|
||||
//Setup
|
||||
export default async function ({login, imports, data, q}, {enabled = false, token = null} = {}) {
|
||||
export default async function ({login, imports, data, q, account}, {enabled = false, token = null} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.pagespeed)||((!data.user.websiteUrl)&&(!q["pagespeed.url"])))
|
||||
return null
|
||||
//Parameters override
|
||||
let {"pagespeed.detailed":detailed = false, "pagespeed.screenshot":screenshot = false, "pagespeed.url":url = data.user.websiteUrl} = q
|
||||
//Duration in days
|
||||
detailed = !!detailed
|
||||
|
||||
//Load inputs
|
||||
let {detailed, screenshot, url} = imports.metadata.plugins.pagespeed.inputs({data, account, q})
|
||||
//Format url if needed
|
||||
if (!/^https?:[/][/]/.test(url))
|
||||
url = `https://${url}`
|
||||
const result = {url, detailed, scores:[], metrics:{}}
|
||||
|
||||
//Load scores from API
|
||||
console.debug(`metrics/compute/${login}/plugins > pagespeed > querying api for ${url}`)
|
||||
const scores = new Map()
|
||||
@@ -31,6 +31,7 @@
|
||||
}
|
||||
}))
|
||||
result.scores = [scores.get("performance"), scores.get("accessibility"), scores.get("best-practices"), scores.get("seo")]
|
||||
|
||||
//Detailed metrics
|
||||
if (detailed) {
|
||||
console.debug(`metrics/compute/${login}/plugins > pagespeed > performing detailed audit`)
|
||||
@@ -39,6 +40,7 @@
|
||||
Object.assign(result.metrics, ...request.data.lighthouseResult.audits.metrics.details.items)
|
||||
console.debug(`metrics/compute/${login}/plugins > pagespeed > performed detailed audit (status code ${request.status})`)
|
||||
}
|
||||
|
||||
//Results
|
||||
return result
|
||||
}
|
||||
|
||||
42
source/plugins/pagespeed/metadata.yml
Normal file
42
source/plugins/pagespeed/metadata.yml
Normal file
@@ -0,0 +1,42 @@
|
||||
name: "⏱️ Website performances"
|
||||
cost: N/A
|
||||
supports:
|
||||
- user
|
||||
- organization
|
||||
- repository
|
||||
inputs:
|
||||
|
||||
# Enable or disable plugin
|
||||
plugin_pagespeed:
|
||||
description: Display a website Google PageSpeed metrics
|
||||
type: boolean
|
||||
default: no
|
||||
|
||||
# Website to audit with PageSpeed
|
||||
plugin_pagespeed_url:
|
||||
description: Audited website
|
||||
type: string
|
||||
default: .user.website
|
||||
|
||||
# Display the following additional metrics from audited website:
|
||||
# First Contentful Paint, Speed Index, Largest Contentful Paint, Time to Interactive, Total Blocking Time, Cumulative Layout Shift
|
||||
# See https://web.dev/performance-scoring/ and https://googlechrome.github.io/lighthouse/scorecalc/ for more informations
|
||||
plugin_pagespeed_detailed:
|
||||
description: Detailed audit result
|
||||
type: boolean
|
||||
default: no
|
||||
|
||||
# Display a screenshot of audited website
|
||||
# May increases significantly filesize
|
||||
plugin_pagespeed_screenshot:
|
||||
description: Display a screenshot of your website
|
||||
type: boolean
|
||||
default: no
|
||||
|
||||
# PageSpeed API token
|
||||
# This is optional, but providing it will avoid hitting rate-limiter
|
||||
# See https://developers.google.com/speed/docs/insights/v5/get-started for more informations
|
||||
plugin_pagespeed_token:
|
||||
description: PageSpeed token
|
||||
type: token
|
||||
default: ""
|
||||
52
source/plugins/people/README.md
Normal file
52
source/plugins/people/README.md
Normal file
@@ -0,0 +1,52 @@
|
||||
### 🧑🤝🧑 People plugin <sup>🚧 <code>plugin_people_thanks</code>, repository version and "sponsors" on <code>@master</code></sup>
|
||||
|
||||
The *people* plugin can display people you're following or sponsoring, and also users who're following or sponsoring you.
|
||||
In repository mode, it's possible to display sponsors, stargazers, watchers.
|
||||
|
||||
<table>
|
||||
<td align="center">
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.people.followers.svg">
|
||||
<details><summary>Followed people version</summary>
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.people.following.svg">
|
||||
</details>
|
||||
<details><summary>Special thanks version</summary>
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.people.thanks.svg">
|
||||
</details>
|
||||
<details><summary>Repository template version</summary>
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.people.repository.svg">
|
||||
</details>
|
||||
<img width="900" height="1" alt="">
|
||||
</td>
|
||||
</table>
|
||||
|
||||
The following types are supported:
|
||||
|
||||
| Type | Alias | User metrics | Repository metrics |
|
||||
| --------------- | ------------------------------------ | :----------------: | :----------------: |
|
||||
| `followers` | | ✔️ | ❌ |
|
||||
| `following` | `followed` | ✔️ | ❌ |
|
||||
| `sponsoring` | `sponsored`, `sponsorshipsAsSponsor` | ✔️ | ❌ |
|
||||
| `sponsors` | `sponsorshipsAsMaintainer` | ✔️ | ✔️ |
|
||||
| `contributors` | | ❌ | ✔️ |
|
||||
| `stargazers` | | ❌ | ✔️ |
|
||||
| `watchers` | | ❌ | ✔️ |
|
||||
| `thanks` | | ✔️ | ✔️ |
|
||||
|
||||
Sections will be ordered the same as specified in `plugin_people_types`.
|
||||
`sponsors` for repositories will output the same as the owner's sponsors.
|
||||
|
||||
#### ℹ️ Examples workflows
|
||||
|
||||
[➡️ Available options for this plugin](metadata.yml)
|
||||
|
||||
```yaml
|
||||
- uses: lowlighter/metrics@latest
|
||||
with:
|
||||
# ... other options
|
||||
plugin_people: yes
|
||||
plugin_people_types: followers, thanks # Display followers and "thanks" sections
|
||||
plugin_people_limit: 28 # Limit to 28 entries per section
|
||||
plugin_people_size: 28 # Size in pixels of displayed avatars
|
||||
plugin_people_identicons: no # Use avatars (do not use identicons)
|
||||
plugin_people_thanks: lowlighter, octocat # Users that will be displayed in "thanks" sections
|
||||
```
|
||||
@@ -20,14 +20,10 @@
|
||||
context = {...context, mode:"repository", types:["contributors", "stargazers", "watchers", "sponsorshipsAsMaintainer", "thanks"], default:"stargazers, watchers", owner, repo}
|
||||
}
|
||||
|
||||
//Parameters override
|
||||
let {"people.limit":limit = 28, "people.types":types = context.default, "people.size":size = 28, "people.identicons":identicons = false, "people.thanks":thanks = []} = q
|
||||
//Limit
|
||||
limit = Math.max(1, limit)
|
||||
//Repositories projects
|
||||
types = [...new Set(decodeURIComponent(types ?? "").split(",").map(type => type.trim()).map(type => (context.alias[type] ?? type)).filter(type => context.types.includes(type)) ?? [])]
|
||||
//Special thanks
|
||||
thanks = decodeURIComponent(thanks ?? "").split(",").map(user => user.trim()).filter(user => user)
|
||||
//Load inputs
|
||||
let {limit, types, size, identicons, thanks} = imports.metadata.plugins.people.inputs({data, account, q}, {types:context.default})
|
||||
//Filter types
|
||||
types = [...new Set([...types].map(type => (context.alias[type] ?? type)).filter(type => context.types.includes(type)) ?? [])]
|
||||
|
||||
//Retrieve followers from graphql api
|
||||
console.debug(`metrics/compute/${login}/plugins > people > querying api`)
|
||||
@@ -52,8 +48,8 @@
|
||||
do {
|
||||
console.debug(`metrics/compute/${login}/plugins > people > retrieving ${type} after ${cursor}`)
|
||||
const {[type]:{edges}} = (
|
||||
type in context.sponsorships ? (await graphql(queries["people.sponsors"]({login:context.owner ?? login, type, size, after:cursor ? `after: "${cursor}"` : "", target:context.sponsorships[type], account})))[account] :
|
||||
context.mode === "repository" ? (await graphql(queries["people.repository"]({login:context.owner, repository:context.repo, type, size, after:cursor ? `after: "${cursor}"` : "", account})))[account].repository :
|
||||
type in context.sponsorships ? (await graphql(queries.people.sponsors({login:context.owner ?? login, type, size, after:cursor ? `after: "${cursor}"` : "", target:context.sponsorships[type], account})))[account] :
|
||||
context.mode === "repository" ? (await graphql(queries.people.repository({login:context.owner, repository:context.repo, type, size, after:cursor ? `after: "${cursor}"` : "", account})))[account].repository :
|
||||
(await graphql(queries.people({login, type, size, after:cursor ? `after: "${cursor}"` : ""}))).user
|
||||
)
|
||||
cursor = edges?.[edges?.length-1]?.cursor
|
||||
|
||||
61
source/plugins/people/metadata.yml
Normal file
61
source/plugins/people/metadata.yml
Normal file
@@ -0,0 +1,61 @@
|
||||
name: "🧑🤝🧑 People plugin"
|
||||
cost: 1 GraphQL request per 100 users + 1 REST request per user in "plugin_people_thanks"
|
||||
supports:
|
||||
- user
|
||||
- organization
|
||||
- repository
|
||||
inputs:
|
||||
|
||||
# Enable or disable plugin
|
||||
plugin_people:
|
||||
description: Display GitHub users from various affiliations
|
||||
type: boolean
|
||||
default: no
|
||||
|
||||
# Number of users to display per section
|
||||
plugin_people_limit:
|
||||
description: Maximum number of user to display
|
||||
type: number
|
||||
default: 28
|
||||
min: 0
|
||||
|
||||
# Size of displayed user's avatar
|
||||
plugin_people_size:
|
||||
description: Size of displayed GitHub users' avatars
|
||||
type: number
|
||||
default: 28
|
||||
min: 8
|
||||
max: 64
|
||||
|
||||
# List of section to display
|
||||
# Ordering will be kept
|
||||
plugin_people_types:
|
||||
description: Affiliations to display
|
||||
type: array
|
||||
format: comma-separated
|
||||
default: followers, following
|
||||
values:
|
||||
- followers # For user metrics
|
||||
- following # For user metrics
|
||||
- followed # For user metrics, alias for "following"
|
||||
- sponsoring # For user metrics
|
||||
- sponsored # Alias for "sponsored"
|
||||
- sponsors # For both user and repository metrics
|
||||
- contributors # For repository metrics
|
||||
- stargazers # For repository metrics
|
||||
- watchers # For repository metrics
|
||||
- thanks # For both user and repository metrics, see "plugin_people_thanks" below
|
||||
|
||||
# When displaying "thanks" section, specified users list will be displayed
|
||||
# This is useful to craft "Special thanks" badges
|
||||
plugin_people_thanks:
|
||||
description: GitHub users to personally thanks
|
||||
type: array
|
||||
format: comma-separated
|
||||
default: ""
|
||||
|
||||
# Use GitHub identicons instead of users' avatar (for privacy purposes)
|
||||
plugin_people_identicons:
|
||||
description: Use identicons instead of avatars
|
||||
type: boolean
|
||||
default: no
|
||||
14
source/plugins/people/queries/people.graphql
Normal file
14
source/plugins/people/queries/people.graphql
Normal file
@@ -0,0 +1,14 @@
|
||||
query PeopleDefault {
|
||||
user(login: "$login") {
|
||||
login
|
||||
$type($after first: 100) {
|
||||
edges {
|
||||
cursor
|
||||
node {
|
||||
login
|
||||
avatarUrl(size: $size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
source/plugins/people/queries/repository.graphql
Normal file
15
source/plugins/people/queries/repository.graphql
Normal file
@@ -0,0 +1,15 @@
|
||||
query PeopleRepository {
|
||||
$account(login: "$login") {
|
||||
repository(name: "$repository") {
|
||||
$type($after first: 100) {
|
||||
edges {
|
||||
cursor
|
||||
node {
|
||||
login
|
||||
avatarUrl(size: $size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
source/plugins/people/queries/sponsors.graphql
Normal file
18
source/plugins/people/queries/sponsors.graphql
Normal file
@@ -0,0 +1,18 @@
|
||||
query PeopleSponsors {
|
||||
$account(login: "$login") {
|
||||
login
|
||||
$type($after first: 100) {
|
||||
edges {
|
||||
cursor
|
||||
node {
|
||||
$target {
|
||||
... on User {
|
||||
login
|
||||
avatarUrl(size: $size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
source/plugins/posts/README.md
Normal file
23
source/plugins/posts/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
### ✒️ Recent posts
|
||||
|
||||
The recent *posts* plugin displays recent articles you wrote on an external source, like [dev.to](https://dev.to).
|
||||
|
||||
<table>
|
||||
<td align="center">
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.posts.svg">
|
||||
<img width="900" height="1" alt="">
|
||||
</td>
|
||||
</table>
|
||||
|
||||
#### ℹ️ Examples workflows
|
||||
|
||||
[➡️ Available options for this plugin](metadata.yml)
|
||||
|
||||
```yaml
|
||||
- uses: lowlighter/metrics@latest
|
||||
with:
|
||||
# ... other options
|
||||
plugin_posts: yes
|
||||
plugin_posts_source: dev.to # External source
|
||||
plugin_people_user: .github.user # Use same username as GitHub login
|
||||
```
|
||||
@@ -1,14 +1,14 @@
|
||||
//Setup
|
||||
export default async function ({login, imports, q}, {enabled = false} = {}) {
|
||||
export default async function ({login, data, imports, q, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.posts))
|
||||
return null
|
||||
//Parameters override
|
||||
let {"posts.source":source = "", "posts.limit":limit = 4, "posts.user":user = login} = q
|
||||
//Limit
|
||||
limit = Math.max(1, Math.min(30, Number(limit)))
|
||||
|
||||
//Load inputs
|
||||
let {source, limit, user} = imports.metadata.plugins.posts.inputs({data, account, q})
|
||||
|
||||
//Retrieve posts
|
||||
console.debug(`metrics/compute/${login}/plugins > posts > processing with source ${source}`)
|
||||
let posts = null
|
||||
@@ -23,6 +23,7 @@
|
||||
default:
|
||||
throw {error:{message:`Unsupported source "${source}"`}}
|
||||
}
|
||||
|
||||
//Format posts
|
||||
if (Array.isArray(posts)) {
|
||||
//Limit tracklist
|
||||
@@ -33,6 +34,7 @@
|
||||
//Results
|
||||
return {source, list:posts}
|
||||
}
|
||||
|
||||
//Unhandled error
|
||||
throw {error:{message:`An error occured (could not retrieve posts)`}}
|
||||
}
|
||||
|
||||
35
source/plugins/posts/metadata.yml
Normal file
35
source/plugins/posts/metadata.yml
Normal file
@@ -0,0 +1,35 @@
|
||||
name: "✒️ Recent posts"
|
||||
cost: N/A
|
||||
supports:
|
||||
- user
|
||||
- organization
|
||||
inputs:
|
||||
|
||||
# Enable or disable plugin
|
||||
plugin_posts:
|
||||
description: Display recent posts
|
||||
type: boolean
|
||||
default: no
|
||||
|
||||
# Posts external source
|
||||
plugin_posts_source:
|
||||
description: Posts external source
|
||||
type: string
|
||||
default: ""
|
||||
values:
|
||||
- dev.to # Dev.to
|
||||
|
||||
# Number of posts to display
|
||||
plugin_posts_limit:
|
||||
description: Maximum number of posts to display
|
||||
type: number
|
||||
default: 4
|
||||
min: 1
|
||||
max: 30
|
||||
|
||||
# Username on external posts source
|
||||
plugin_posts_user:
|
||||
description: Posts external source username
|
||||
type: string
|
||||
default: .user.login
|
||||
|
||||
55
source/plugins/projects/README.md
Normal file
55
source/plugins/projects/README.md
Normal file
@@ -0,0 +1,55 @@
|
||||
### 🗂️ Projects <sup>🚧 <code>plugin_projects_descriptions</code> on <code>@master</code></sup>
|
||||
|
||||
⚠️ This plugin requires a personal token with public_repo scope.
|
||||
|
||||
The *projects* plugin displays the progress of your profile projects.
|
||||
|
||||
<table>
|
||||
<td align="center">
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.projects.svg">
|
||||
<img width="900" height="1" alt="">
|
||||
</td>
|
||||
</table>
|
||||
|
||||
Because of GitHub REST API limitation, provided token requires `public_repo` scope to access projects informations.
|
||||
|
||||
Note that by default, projects have progress tracking disabled.
|
||||
To enable it, open the `≡ Menu` and edit the project to opt-in to `Track project progress` (it can be a bit confusing since it's actually not in the project settings).
|
||||
|
||||

|
||||
|
||||
<details>
|
||||
<summary>💬 Create a personal project on GitHub</summary>
|
||||
|
||||
On your profile, select the `Projects` tab:
|
||||

|
||||
|
||||
Fill the informations and set visibility to *public*:
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>💬 Use repositories projects</summary>
|
||||
|
||||
It is possible to display projects related to repositories along with personal projects.
|
||||
|
||||
To do so, open your repository project and retrieve the last URL endpoint, in the format `:user/:repository/projects/:project_id` (for example, `lowlighter/metrics/projects/1`) and add it in the `plugin_projects_repositories` option. Enable `Track project progress` in the project settings to display a progress bar in generated metrics.
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
#### ℹ️ Examples workflows
|
||||
|
||||
[➡️ Available options for this plugin](metadata.yml)
|
||||
|
||||
```yaml
|
||||
- uses: lowlighter/metrics@latest
|
||||
with:
|
||||
# ... other options
|
||||
plugin_projects: yes
|
||||
plugin_projects_repositories: lowlighter/metrics/projects/1 # Display #1 project of lowlighter/metrics repository
|
||||
plugin_projects_limit: 4 # Limit to 4 entries
|
||||
plugin_projects_descriptions: yes # Display projects descriptions
|
||||
```
|
||||
@@ -1,25 +1,26 @@
|
||||
//Setup
|
||||
export default async function ({login, graphql, q, queries, account}, {enabled = false} = {}) {
|
||||
export default async function ({login, data, imports, graphql, q, queries, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.projects))
|
||||
return null
|
||||
//Parameters override
|
||||
let {"projects.limit":limit = 4, "projects.repositories":repositories = "", "projects.descriptions":descriptions = false} = q
|
||||
|
||||
//Load inputs
|
||||
let {limit, repositories, descriptions} = imports.metadata.plugins.projects.inputs({data, account, q})
|
||||
//Repositories projects
|
||||
repositories = decodeURIComponent(repositories ?? "").split(",").map(repository => repository.trim()).filter(repository => /[-\w]+[/][-\w]+[/]projects[/]\d+/.test(repository)) ?? []
|
||||
//Limit
|
||||
limit = Math.max(repositories.length, Math.min(100, Number(limit)))
|
||||
repositories = repositories.filter(repository => /[-\w]+[/][-\w]+[/]projects[/]\d+/.test(repository))
|
||||
|
||||
//Retrieve user owned projects from graphql api
|
||||
console.debug(`metrics/compute/${login}/plugins > projects > querying api`)
|
||||
const {[account]:{projects}} = await graphql(queries.projects({login, limit, account}))
|
||||
const {[account]:{projects}} = await graphql(queries.projects.user({login, limit, account}))
|
||||
|
||||
//Retrieve repositories projects from graphql api
|
||||
for (const identifier of repositories) {
|
||||
//Querying repository project
|
||||
console.debug(`metrics/compute/${login}/plugins > projects > querying api for ${identifier}`)
|
||||
const {user, repository, id} = identifier.match(/(?<user>[-\w]+)[/](?<repository>[-\w]+)[/]projects[/](?<id>\d+)/)?.groups
|
||||
const {[account]:{repository:{project}}} = await graphql(queries["projects.repository"]({user, repository, id, account}))
|
||||
const {[account]:{repository:{project}}} = await graphql(queries.projects.repository({user, repository, id, account}))
|
||||
//Adding it to projects list
|
||||
console.debug(`metrics/compute/${login}/plugins > projects > registering ${identifier}`)
|
||||
project.name = `${project.name} (${user}/${repository})`
|
||||
@@ -43,9 +44,11 @@
|
||||
//Append
|
||||
list.push({name:project.name, updated, description:project.body, progress:{enabled, todo, doing, done, total:todo+doing+done}})
|
||||
}
|
||||
|
||||
//Limit
|
||||
console.debug(`metrics/compute/${login}/plugins > projects > keeping only ${limit} projects`)
|
||||
list.splice(limit)
|
||||
|
||||
//Results
|
||||
return {list, totalCount:projects.totalCount, descriptions}
|
||||
}
|
||||
|
||||
39
source/plugins/projects/metadata.yml
Normal file
39
source/plugins/projects/metadata.yml
Normal file
@@ -0,0 +1,39 @@
|
||||
name: "🗂️ Projects"
|
||||
cost: 1 GraphQL request + 1 GraphQL request per repository project
|
||||
supports:
|
||||
- user
|
||||
- organization
|
||||
- repository
|
||||
inputs:
|
||||
|
||||
# Enable or disable plugin
|
||||
plugin_projects:
|
||||
description: Display active projects
|
||||
type: boolean
|
||||
default: no
|
||||
|
||||
# Number of projects to display
|
||||
# Set to 0 to only display "plugin_projects_repositories" projects
|
||||
# Projects listed in "plugin_projects_repositories" are not affected by this option
|
||||
plugin_projects_limit:
|
||||
description: Maximum number of projects to display
|
||||
type: number
|
||||
default: 4
|
||||
min: 0
|
||||
max: 100
|
||||
|
||||
# List of repository projects to display, using the following format:
|
||||
# :user/:repo/projects/:project_id
|
||||
plugin_projects_repositories:
|
||||
description: List of repository project identifiers to disaplay
|
||||
type: array
|
||||
format:
|
||||
- comma-separated
|
||||
- /(?<user>[-a-z0-9]+)[/](?<repo>[-a-z0-9]+)[/]projects[/](?<id>[0-9]+)/
|
||||
default: ""
|
||||
|
||||
# Display projects descriptions
|
||||
plugin_projects_descriptions:
|
||||
description: Display projects descriptions
|
||||
type: boolean
|
||||
default: no
|
||||
17
source/plugins/projects/queries/repository.graphql
Normal file
17
source/plugins/projects/queries/repository.graphql
Normal file
@@ -0,0 +1,17 @@
|
||||
query ProjectsRepository {
|
||||
$account(login: "$user") {
|
||||
repository(name: "$repository") {
|
||||
project(number: $id) {
|
||||
name
|
||||
body
|
||||
updatedAt
|
||||
progress {
|
||||
doneCount
|
||||
inProgressCount
|
||||
todoCount
|
||||
enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
source/plugins/projects/queries/user.graphql
Normal file
18
source/plugins/projects/queries/user.graphql
Normal file
@@ -0,0 +1,18 @@
|
||||
query ProjectsUser {
|
||||
$account(login: "$login") {
|
||||
projects(last: $limit, states: OPEN, orderBy: {field: UPDATED_AT, direction: DESC}) {
|
||||
totalCount
|
||||
nodes {
|
||||
name
|
||||
body
|
||||
updatedAt
|
||||
progress {
|
||||
doneCount
|
||||
inProgressCount
|
||||
todoCount
|
||||
enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
source/plugins/stargazers/README.md
Normal file
21
source/plugins/stargazers/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
### ✨ Stargazers over last weeks
|
||||
|
||||
The *stargazers* plugin displays your stargazers evolution across all of your repositories over the last two weeks.
|
||||
|
||||
<table>
|
||||
<td align="center">
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.stargazers.svg">
|
||||
<img width="900" height="1" alt="">
|
||||
</td>
|
||||
</table>
|
||||
|
||||
#### ℹ️ Examples workflows
|
||||
|
||||
[➡️ Available options for this plugin](metadata.yml)
|
||||
|
||||
```yaml
|
||||
- uses: lowlighter/metrics@latest
|
||||
with:
|
||||
# ... other options
|
||||
plugin_stargazers: yes
|
||||
```
|
||||
@@ -1,10 +1,14 @@
|
||||
//Setup
|
||||
export default async function ({login, graphql, data, q, queries}, {enabled = false} = {}) {
|
||||
export default async function ({login, graphql, data, imports, q, queries, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.stargazers))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
imports.metadata.plugins.stargazers.inputs({data, account, q})
|
||||
|
||||
//Retrieve stargazers from graphql api
|
||||
console.debug(`metrics/compute/${login}/plugins > stargazers > querying api`)
|
||||
const repositories = data.user.repositories.nodes.map(({name:repository, owner:{login:owner}}) => ({repository, owner})) ?? []
|
||||
@@ -25,6 +29,7 @@
|
||||
console.debug(`metrics/compute/${login}/plugins > stargazers > loaded ${dates.length} stargazers for ${repository}`)
|
||||
}
|
||||
console.debug(`metrics/compute/${login}/plugins > stargazers > loaded ${dates.length} stargazers in total`)
|
||||
|
||||
//Compute stargazers increments
|
||||
const days = 14
|
||||
const increments = {dates:Object.fromEntries([...new Array(days).fill(null).map((_, i) => [new Date(Date.now()-i*24*60*60*1000).toISOString().slice(0, 10), 0]).reverse()]), max:NaN, min:NaN}
|
||||
@@ -34,6 +39,7 @@
|
||||
.map(date => increments.dates[date]++)
|
||||
increments.min = Math.min(...Object.values(increments.dates))
|
||||
increments.max = Math.max(...Object.values(increments.dates))
|
||||
|
||||
//Compute total stargazers
|
||||
let stargazers = data.computed.repositories.stargazers
|
||||
const total = {dates:{...increments.dates}, max:NaN, min:NaN}
|
||||
@@ -47,8 +53,10 @@
|
||||
}
|
||||
total.min = Math.min(...Object.values(total.dates))
|
||||
total.max = Math.max(...Object.values(total.dates))
|
||||
|
||||
//Months name
|
||||
const months = ["", "Jan.", "Feb.", "Mar.", "Apr.", "May", "June", "July", "Aug.", "Sep.", "Oct.", "Nov.", "Dec."]
|
||||
|
||||
//Results
|
||||
return {total, increments, months}
|
||||
}
|
||||
|
||||
13
source/plugins/stargazers/metadata.yml
Normal file
13
source/plugins/stargazers/metadata.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
name: "✨ Stargazers over last weeks"
|
||||
cost: 1 GraphQL request per 100 stargazers
|
||||
supports:
|
||||
- user
|
||||
- organization
|
||||
- repository
|
||||
inputs:
|
||||
|
||||
# Enable or disable plugin
|
||||
plugin_stargazers:
|
||||
description: Display stargazers metrics
|
||||
type: boolean
|
||||
default: no
|
||||
10
source/plugins/stargazers/queries/stargazers.graphql
Normal file
10
source/plugins/stargazers/queries/stargazers.graphql
Normal file
@@ -0,0 +1,10 @@
|
||||
query StargazersDefault {
|
||||
repository(name: "$repository", owner: "$login") {
|
||||
stargazers($after first: 100, orderBy: {field: STARRED_AT, direction: ASC}) {
|
||||
edges {
|
||||
starredAt
|
||||
cursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
source/plugins/stars/README.md
Normal file
22
source/plugins/stars/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
### 🌟 Recently starred repositories
|
||||
|
||||
The *stars* plugin displays your recently starred repositories.
|
||||
|
||||
<table>
|
||||
<td align="center">
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.stars.svg">
|
||||
<img width="900" height="1" alt="">
|
||||
</td>
|
||||
</table>
|
||||
|
||||
#### ℹ️ Examples workflows
|
||||
|
||||
[➡️ Available options for this plugin](metadata.yml)
|
||||
|
||||
```yaml
|
||||
- uses: lowlighter/metrics@latest
|
||||
with:
|
||||
# ... other options
|
||||
plugin_stars: yes
|
||||
plugin_stars_limit: 4 # Limit to 4 entries
|
||||
```
|
||||
@@ -1,19 +1,18 @@
|
||||
//Setup
|
||||
export default async function ({login, graphql, q, queries, account}, {enabled = false} = {}) {
|
||||
export default async function ({login, data, graphql, q, queries, imports, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.stars))
|
||||
return null
|
||||
if (account === "organization")
|
||||
throw {error:{message:"Not available for organizations"}}
|
||||
//Parameters override
|
||||
let {"stars.limit":limit = 4} = q
|
||||
//Limit
|
||||
limit = Math.max(1, Math.min(100, Number(limit)))
|
||||
|
||||
//Load inputs
|
||||
let {limit} = imports.metadata.plugins.stars.inputs({data, account, q})
|
||||
|
||||
//Retrieve user stars from graphql api
|
||||
console.debug(`metrics/compute/${login}/plugins > stars > querying api`)
|
||||
const {user:{starredRepositories:{edges:repositories}}} = await graphql(queries.starred({login, limit}))
|
||||
const {user:{starredRepositories:{edges:repositories}}} = await graphql(queries.stars({login, limit}))
|
||||
|
||||
//Format starred repositories
|
||||
for (const edge of repositories) {
|
||||
//Format date
|
||||
@@ -25,6 +24,7 @@
|
||||
updated = `${Math.floor(time)} day${time >= 2 ? "s" : ""} ago`
|
||||
edge.starred = updated
|
||||
}
|
||||
|
||||
//Results
|
||||
return {repositories}
|
||||
}
|
||||
|
||||
19
source/plugins/stars/metadata.yml
Normal file
19
source/plugins/stars/metadata.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
name: "🌟 Recently starred repositories"
|
||||
cost: 1 GraphQL request
|
||||
supports:
|
||||
- user
|
||||
inputs:
|
||||
|
||||
# Enable or disable plugin
|
||||
plugin_stars:
|
||||
description: Display recently starred repositories
|
||||
type: boolean
|
||||
default: no
|
||||
|
||||
# Number of stars to display
|
||||
plugin_stars_limit:
|
||||
description: Maximum number of stars to display
|
||||
type: number
|
||||
default: 4
|
||||
min: 1
|
||||
max: 100
|
||||
32
source/plugins/stars/queries/stars.graphql
Normal file
32
source/plugins/stars/queries/stars.graphql
Normal file
@@ -0,0 +1,32 @@
|
||||
query StarsDefault {
|
||||
user(login: "$login") {
|
||||
starredRepositories(first: $limit, orderBy: {field: STARRED_AT, direction: DESC}) {
|
||||
edges {
|
||||
starredAt
|
||||
node {
|
||||
description
|
||||
forkCount
|
||||
isFork
|
||||
issues {
|
||||
totalCount
|
||||
}
|
||||
nameWithOwner
|
||||
openGraphImageUrl
|
||||
licenseInfo {
|
||||
nickname
|
||||
spdxId
|
||||
name
|
||||
}
|
||||
pullRequests {
|
||||
totalCount
|
||||
}
|
||||
stargazerCount
|
||||
primaryLanguage {
|
||||
color
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
source/plugins/topics/README.md
Normal file
30
source/plugins/topics/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
### 📌 Starred topics
|
||||
|
||||
The *topics* plugin displays your [starred topics](https://github.com/stars?filter=topics).
|
||||
Check out [GitHub topics](https://github.com/topics) to search interesting topics.
|
||||
|
||||
<table>
|
||||
<td align="center">
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.topics.svg">
|
||||
<details open><summary>Mastered and known technologies version</summary>
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.topics.mastered.svg">
|
||||
</details>
|
||||
<img width="900" height="1" alt="">
|
||||
</td>
|
||||
</table>
|
||||
|
||||
This uses puppeteer to navigate through your starred topics page.
|
||||
|
||||
#### ℹ️ Examples workflows
|
||||
|
||||
[➡️ Available options for this plugin](metadata.yml)
|
||||
|
||||
```yaml
|
||||
- uses: lowlighter/metrics@latest
|
||||
with:
|
||||
# ... other options
|
||||
plugin_topics: yes
|
||||
plugin_topics_sort: stars # Sort by most starred topics
|
||||
plugin_topics_mode: mastered # Display icons instead of labels
|
||||
plugin_topics_limit: 0 # Disable limitations
|
||||
```
|
||||
@@ -1,24 +1,15 @@
|
||||
//Setup
|
||||
export default async function ({login, imports, q, account}, {enabled = false} = {}) {
|
||||
export default async function ({login, data, imports, q, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.topics))
|
||||
return null
|
||||
if (account === "organization")
|
||||
throw {error:{message:"Not available for organizations"}}
|
||||
//Parameters override
|
||||
let {"topics.sort":sort = "stars", "topics.mode":mode = "starred", "topics.limit":limit} = q
|
||||
//Shuffle
|
||||
const shuffle = (sort === "random")
|
||||
//Sort method
|
||||
sort = {starred:"created", activity:"updated", stars:"stars", random:"created"}[sort] ?? "starred"
|
||||
//Limit
|
||||
if (!Number.isFinite(limit))
|
||||
limit = (mode === "mastered" ? 0 : 15)
|
||||
limit = Math.max(0, Math.min(20, Number(limit)))
|
||||
//Mode
|
||||
mode = ["starred", "mastered"].includes(mode) ? mode : "starred"
|
||||
|
||||
//Load inputs
|
||||
let {sort, mode, limit} = imports.metadata.plugins.topics.inputs({data, account, q})
|
||||
const shuffle = (sort === "random")
|
||||
|
||||
//Start puppeteer and navigate to topics
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > searching starred topics`)
|
||||
let topics = []
|
||||
@@ -26,6 +17,7 @@
|
||||
const browser = await imports.puppeteer.launch({headless:true, executablePath:process.env.PUPPETEER_BROWSER_PATH, args:["--no-sandbox", "--disable-extensions", "--disable-setuid-sandbox", "--disable-dev-shm-usage"]})
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > started ${await browser.version()}`)
|
||||
const page = await browser.newPage()
|
||||
|
||||
//Iterate through pages
|
||||
for (let i = 1; i <= 100; i++) {
|
||||
//Load page
|
||||
@@ -47,14 +39,17 @@
|
||||
}
|
||||
topics.push(...starred)
|
||||
}
|
||||
|
||||
//Close browser
|
||||
console.debug(`metrics/compute/${login}/plugins > music > closing browser`)
|
||||
await browser.close()
|
||||
|
||||
//Shuffle topics
|
||||
if (shuffle) {
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > shuffling topics`)
|
||||
topics = imports.shuffle(topics)
|
||||
}
|
||||
|
||||
//Limit topics (starred mode)
|
||||
if ((mode === "starred")&&(limit > 0)) {
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > keeping only ${limit} topics`)
|
||||
@@ -62,6 +57,7 @@
|
||||
if (removed.length)
|
||||
topics.push({name:`And ${removed.length} more...`, description:removed.map(({name}) => name).join(", "), icon:null})
|
||||
}
|
||||
|
||||
//Convert icons to base64
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > loading artworks`)
|
||||
for (const topic of topics) {
|
||||
@@ -72,16 +68,19 @@
|
||||
//Escape HTML description
|
||||
topic.description = imports.htmlescape(topic.description)
|
||||
}
|
||||
|
||||
//Filter topics with icon (mastered mode)
|
||||
if (mode === "mastered") {
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > filtering topics with icon`)
|
||||
topics = topics.filter(({icon}) => icon)
|
||||
}
|
||||
|
||||
//Limit topics (mastered mode)
|
||||
if ((mode === "mastered")&&(limit > 0)) {
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > keeping only ${limit} topics`)
|
||||
topics.splice(limit)
|
||||
}
|
||||
|
||||
//Results
|
||||
return {mode, list:topics}
|
||||
}
|
||||
|
||||
41
source/plugins/topics/metadata.yml
Normal file
41
source/plugins/topics/metadata.yml
Normal file
@@ -0,0 +1,41 @@
|
||||
name: "📌 Starred topics"
|
||||
cost: N/A
|
||||
supports:
|
||||
- user
|
||||
inputs:
|
||||
|
||||
# Enable or disable plugin
|
||||
plugin_topics:
|
||||
description: Display starred topics
|
||||
type: boolean
|
||||
default: no
|
||||
|
||||
# Plugin mode
|
||||
plugin_topics_mode:
|
||||
description: Plugin mode
|
||||
type: string
|
||||
default: starred
|
||||
values:
|
||||
- starred # Display starred topics as labels
|
||||
- mastered # Display starred topics as mastered/known technologies icons
|
||||
|
||||
# Topics sorting order
|
||||
plugin_topics_sort:
|
||||
description: Sorting method of starred topics
|
||||
type: string
|
||||
default: stars
|
||||
values:
|
||||
- stars # Sort topics by stargazers
|
||||
- activity # Sort topics by recent activity
|
||||
- starred # Sort topics by the date you starred them
|
||||
- random # Sort topics randomly
|
||||
|
||||
# Number of topics to display
|
||||
# Set to 0 to disable limitations
|
||||
# When in "starred" mode, additional topics will be grouped into an ellipsis
|
||||
plugin_topics_limit:
|
||||
description: Maximum number of topics to display
|
||||
type: number
|
||||
default: 15
|
||||
min: 0
|
||||
max: 20
|
||||
26
source/plugins/traffic/README.md
Normal file
26
source/plugins/traffic/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
### 🧮 Repositories traffic
|
||||
|
||||
⚠️ This plugin requires a personal token with repo scope.
|
||||
|
||||
The repositories *traffic* plugin displays the number of page views across your repositories.
|
||||
|
||||
<table>
|
||||
<td align="center">
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.traffic.svg">
|
||||
</td>
|
||||
</table>
|
||||
|
||||
Because of GitHub REST API limitation, provided token requires full `repo` scope to access traffic informations.
|
||||
|
||||

|
||||
|
||||
#### ℹ️ Examples workflows
|
||||
|
||||
[➡️ Available options for this plugin](metadata.yml)
|
||||
|
||||
```yaml
|
||||
- uses: lowlighter/metrics@latest
|
||||
with:
|
||||
# ... other options
|
||||
plugin_traffic: yes
|
||||
```
|
||||
@@ -1,19 +1,26 @@
|
||||
//Setup
|
||||
export default async function ({login, imports, data, rest, q}, {enabled = false} = {}) {
|
||||
export default async function ({login, imports, data, rest, q, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.traffic))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
imports.metadata.plugins.traffic.inputs({data, account, q})
|
||||
|
||||
//Repositories
|
||||
const repositories = data.user.repositories.nodes.map(({name:repo, owner:{login:owner}}) => ({repo, owner})) ?? []
|
||||
|
||||
//Get views stats from repositories
|
||||
console.debug(`metrics/compute/${login}/plugins > traffic > querying api`)
|
||||
const views = {count:0, uniques:0}
|
||||
const response = await Promise.all(repositories.map(async ({repo, owner}) => await rest.repos.getViews({owner, repo})))
|
||||
|
||||
//Compute views
|
||||
console.debug(`metrics/compute/${login}/plugins > traffic > computing stats`)
|
||||
response.filter(({data}) => data).map(({data:{count, uniques}}) => (views.count += count, views.uniques += uniques))
|
||||
|
||||
//Results
|
||||
return {views}
|
||||
}
|
||||
|
||||
13
source/plugins/traffic/metadata.yml
Normal file
13
source/plugins/traffic/metadata.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
name: "🧮 Repositories traffic"
|
||||
cost: 1 REST request per repository
|
||||
supports:
|
||||
- user
|
||||
- organization
|
||||
- repository
|
||||
inputs:
|
||||
|
||||
# Enable or disable plugin
|
||||
plugin_traffic:
|
||||
description: Display repositories traffic metrics
|
||||
type: boolean
|
||||
default: no
|
||||
36
source/plugins/tweets/README.md
Normal file
36
source/plugins/tweets/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
### 🐤 Tweets
|
||||
|
||||
The recent *tweets* plugin displays your latest tweets from your [Twitter](https://twitter.com) account.
|
||||
|
||||
<table>
|
||||
<td align="center">
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.tweets.svg">
|
||||
<img width="900" height="1" alt="">
|
||||
</td>
|
||||
</table>
|
||||
|
||||
<details>
|
||||
<summary>💬 Obtaining a Twitter token</summary>
|
||||
|
||||
To get a Twitter token, you'll need to apply to the [developer program](https://apps.twitter.com).
|
||||
It's a bit tedious, but it seems that requests are approved quite quickly.
|
||||
|
||||
Create an app from your [developer dashboard](https://developer.twitter.com/en/portal/dashboard) and register your bearer token in your repository secrets.
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
#### ℹ️ Examples workflows
|
||||
|
||||
[➡️ Available options for this plugin](metadata.yml)
|
||||
|
||||
```yaml
|
||||
- uses: lowlighter/metrics@latest
|
||||
with:
|
||||
# ... other options
|
||||
plugin_tweets: yes
|
||||
plugin_tweets_token: ${{ secrets.TWITTER_TOKEN }} # Required
|
||||
plugin_tweets_limit: 2 # Limit to 2 tweets
|
||||
plugin_tweets_user: .user.twitter # Defaults to your GitHub linked twitter username
|
||||
```
|
||||
@@ -1,30 +1,34 @@
|
||||
//Setup
|
||||
export default async function ({login, imports, data, q}, {enabled = false, token = null} = {}) {
|
||||
export default async function ({login, imports, data, q, account}, {enabled = false, token = ""} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.tweets))
|
||||
return null
|
||||
//Parameters override
|
||||
let {"tweets.limit":limit = 2, "tweets.user":username = data.user.twitterUsername} = q
|
||||
//Limit
|
||||
limit = Math.max(1, Math.min(10, Number(limit)))
|
||||
|
||||
//Load inputs
|
||||
let {limit, user:username} = imports.metadata.plugins.tweets.inputs({data, account, q})
|
||||
|
||||
//Load user profile
|
||||
console.debug(`metrics/compute/${login}/plugins > tweets > loading twitter profile (@${username})`)
|
||||
const {data:{data:profile = null}} = await imports.axios.get(`https://api.twitter.com/2/users/by/username/${username}?user.fields=profile_image_url,verified`, {headers:{Authorization:`Bearer ${token}`}})
|
||||
//Load tweets
|
||||
console.debug(`metrics/compute/${login}/plugins > tweets > querying api`)
|
||||
const {data:{data:tweets = []}} = await imports.axios.get(`https://api.twitter.com/2/tweets/search/recent?query=from:${username}&tweet.fields=created_at&expansions=entities.mentions.username`, {headers:{Authorization:`Bearer ${token}`}})
|
||||
|
||||
//Load profile image
|
||||
if (profile?.profile_image_url) {
|
||||
console.debug(`metrics/compute/${login}/plugins > tweets > loading profile image`)
|
||||
profile.profile_image = await imports.imgb64(profile.profile_image_url)
|
||||
}
|
||||
|
||||
//Load tweets
|
||||
console.debug(`metrics/compute/${login}/plugins > tweets > querying api`)
|
||||
const {data:{data:tweets = []}} = await imports.axios.get(`https://api.twitter.com/2/tweets/search/recent?query=from:${username}&tweet.fields=created_at&expansions=entities.mentions.username`, {headers:{Authorization:`Bearer ${token}`}})
|
||||
|
||||
//Limit tweets
|
||||
if (limit > 0) {
|
||||
console.debug(`metrics/compute/${login}/plugins > tweets > keeping only ${limit} tweets`)
|
||||
tweets.splice(limit)
|
||||
}
|
||||
|
||||
//Format tweets
|
||||
await Promise.all(tweets.map(async tweet => {
|
||||
//Mentions
|
||||
@@ -44,6 +48,7 @@
|
||||
.replace(/https?:[/][/](t.co[/]\w+)/g, ` <span class="link">$1</span> `)
|
||||
, {"&":true})
|
||||
}))
|
||||
|
||||
//Result
|
||||
return {username, profile, list:tweets}
|
||||
}
|
||||
|
||||
33
source/plugins/tweets/metadata.yml
Normal file
33
source/plugins/tweets/metadata.yml
Normal file
@@ -0,0 +1,33 @@
|
||||
name: "🐤 Latest tweets"
|
||||
cost: N/A
|
||||
supports:
|
||||
- user
|
||||
- organization
|
||||
inputs:
|
||||
|
||||
# Enable or disable plugin
|
||||
plugin_tweets:
|
||||
description: Display recent tweets
|
||||
type: boolean
|
||||
default: no
|
||||
|
||||
# Twitter API token
|
||||
# See https://apps.twitter.com for more informations
|
||||
plugin_tweets_token:
|
||||
description: Twitter API token
|
||||
type: token
|
||||
default: ""
|
||||
|
||||
# Number of tweets to display
|
||||
plugin_tweets_limit:
|
||||
description: Maximum number of tweets to display
|
||||
type: number
|
||||
default: 2
|
||||
min: 1
|
||||
max: 10
|
||||
|
||||
# Twitter username
|
||||
plugin_tweets_user:
|
||||
description: Twitter username
|
||||
type: string
|
||||
default: .user.twitter
|
||||
Reference in New Issue
Block a user